Skip to content

feat(action): add IIP-59 voter reward actions and BLS candidate fields - #38

Open
envestcc wants to merge 2 commits into
masterfrom
feat/zanzibar-voter-reward-actions
Open

feat(action): add IIP-59 voter reward actions and BLS candidate fields#38
envestcc wants to merge 2 commits into
masterfrom
feat/zanzibar-voter-reward-actions

Conversation

@envestcc

@envestcc envestcc commented Sep 8, 2026

Copy link
Copy Markdown
Member

The vendored proto stopped at ActionCore field 53, so this SDK could not build any Zanzibar action and silently dropped the BLS fields on candidate registration.

What's added

Matched field-by-field against iotex-proto v0.6.13:

field
54 candidateDeactivate proto only
55 scheduleCandidateDeactivation proto only
57 setVoterRewardOptIn + Request/Method wrapper
58 setVoterRewardDestination + Request/Method wrapper
CandidateBasicInfo.blsPubKey / blsPop proto only

Two things worth reviewing

SetVoterRewardOptIn needed a different presence check. It carries no fields — the sender is the delegate — so it serializes to zero bytes, and the toByteArray().length > 0 test every other Envelop branch uses is false for it. Deserializing an opt-in through that path drops it, turning a delegate's one-way, irreversible action into a silent no-op. The new branches read the oneof presence bit instead. There's a test for exactly this.

The destination goes on the wire as 20 raw bytes, decoded from io1... via Bech32, because the protocol reads it with address.FromBytes. Sending the bech32 string produces an action that signs cleanly and is rejected on decode.

setCommissionRate (56) is deliberately absent

It exists in iotex-proto v0.6.13, but no core release implements it — iotex-proto#168 is still open, and a testnet node answers no applicable action to handle proto type for it. I found this while doing the same work in antenna-go (#115), where I had already written the wrapper before a testnet round-trip caught it.

Codegen

Regenerated with protoc 34.1, matching the Protobuf Java Version: 4.34.1 already in the tree and the protobuf-java 4.34.1 the pom pins. The local default here is 35.1, whose gencode fails protobuf-java's runtime version check — worth knowing before anyone regenerates.

Testing

4 new tests, covering the two failure modes that are invisible until a node sees them: which oneof field each action occupies, and that the empty opt-in survives an Envelop round trip.

Full suite: same 9 pre-existing errors as master, in the same four classes (ContractTest, KeystoreTest, IOTXTest, RawBlockTest — all network- or key-dependent). Verified by stashing this change and re-running.

Not verified here

No testnet round trip from Java. antenna-go's equivalent actions were submitted to testnet and accepted (decoded and validated, rejected only for an empty balance), and the wire encoding is the same — but that is an inference, not a Java-side observation. If you want it closed properly, a signed submission from Java with a funded key is the missing step, and it would also exercise the TX_CONTAINER signing path that PR #34 introduced.

The vendored proto stopped at ActionCore field 53, so this SDK could not build
any Zanzibar action and silently dropped the BLS fields on candidate
registration.

Added to proto/types/action.proto, matched field-by-field against iotex-proto
v0.6.13: candidateDeactivate (54), scheduleCandidateDeactivation (55),
setVoterRewardOptIn (57), setVoterRewardDestination (58), and blsPubKey/blsPop
on CandidateBasicInfo. Wrappers are provided for the two IIP-59 actions;
the other two are proto-only for now.

setCommissionRate (56) is deliberately absent. It exists in iotex-proto v0.6.13
but no core release implements it -- iotex-proto#168 is still open, and a
testnet node answers "no applicable action to handle proto type" for it.

Envelop needed a different presence check for the opt-in. SetVoterRewardOptIn
carries no fields -- the sender is the delegate -- so it serializes to zero
bytes, and the toByteArray().length > 0 test every other branch uses is false
for it. Deserializing an opt-in through that path drops it, turning a delegate's
one-way action into a silent no-op. The new branches read the oneof presence
bit instead.

The destination is decoded from io1... to its 20 raw bytes before it goes on
the wire, because the protocol reads it with address.FromBytes; sending the
bech32 text produces an action that signs and is then rejected on decode.

Generated code was regenerated with protoc 34.1 to match the "Protobuf Java
Version: 4.34.1" already in the tree and the protobuf-java 4.34.1 the pom pins.
The local default is 35.1, whose gencode fails the runtime's version check.

Four tests cover the two failure modes that are invisible until a node sees
them: which oneof field each action occupies, and that the empty opt-in
survives an Envelop round trip. Full suite has the same 9 pre-existing errors
as master, in the same four network- and key-dependent classes.
The wire encoding was previously argued from antenna-go's TestNet run rather
than observed from Java. This closes that, and exercises the TX_CONTAINER
signing path PR #34 introduced.

Both actions were submitted from a funded non-delegate account. Receipts at
height 47,511,663:

  SetVoterRewardDestination  status 1    (Success)
  SetVoterRewardOptIn        status 205  (ErrCandidateNotExist)

205 is the outcome to want here, not a problem. The action reached the staking
handler and was rejected on a protocol rule -- the sender is not a candidate --
which proves it was decoded and dispatched correctly. A wrapper that filed the
action under the wrong oneof field would have failed earlier and differently,
with "no applicable action to handle proto type", the way SetCommissionRate
does.

The opt-in was deliberately not sent from a delegate. It is one-way: the
protocol has no counterpart action to opt back out, so running it against a
real delegate would have permanently changed it to prove a point about
encoding. TestNet's endorsetest was verified unchanged afterwards.

The test skips unless IOTEX_TEST_KEY is set, so it is inert in CI and in any
checkout without a funded key. No key is committed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant