Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/main/java/com/github/iotexproject/antenna/action/Envelop.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ public class Envelop {
private StakeTransferOwnership stakeTransferOwnership;
private CandidateRegister candidateRegister;
private CandidateBasicInfo candidateUpdate;
private SetVoterRewardOptIn setVoterRewardOptIn;
private SetVoterRewardDestination setVoterRewardDestination;
private StartSubChain startSubChain;
private StopSubChain stopSubChain;
private PutBlock putBlock;
Expand Down Expand Up @@ -141,6 +143,17 @@ public static Envelop deserialize(byte[] bytes) throws IllegalArgumentException
if (core.getCandidateUpdate().toByteArray().length > 0) {
envelop.setCandidateUpdate(core.getCandidateUpdate());
}
// hasX(), not toByteArray().length: SetVoterRewardOptIn carries no
// payload -- the sender is the delegate -- so it serializes to zero
// bytes and a length check drops it silently. The oneof's presence
// bit is the only thing that distinguishes "opt in" from "no action
// set at all".
if (core.hasSetVoterRewardOptIn()) {
envelop.setSetVoterRewardOptIn(core.getSetVoterRewardOptIn());
}
if (core.hasSetVoterRewardDestination()) {
envelop.setSetVoterRewardDestination(core.getSetVoterRewardDestination());
}

return envelop;
} catch (InvalidProtocolBufferException e) {
Expand Down Expand Up @@ -211,6 +224,12 @@ public ActionCore core() {
if (candidateUpdate != null) {
builder.setCandidateUpdate(candidateUpdate);
}
if (setVoterRewardOptIn != null) {
builder.setSetVoterRewardOptIn(setVoterRewardOptIn);
}
if (setVoterRewardDestination != null) {
builder.setSetVoterRewardDestination(setVoterRewardDestination);
}
if (startSubChain != null) {
builder.setStartSubChain(startSubChain);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import com.github.iotexproject.antenna.rpc.RPCMethod;
import com.github.iotexproject.antenna.utils.Numeric;
import com.github.iotexproject.grpc.api.*;
import com.github.iotexproject.grpc.types.CandidateBasicInfo;
import com.google.protobuf.ByteString;

import java.math.BigInteger;

Expand Down Expand Up @@ -41,6 +43,63 @@ protected Envelop baseEnvelop(ActionRequest request) {
return Envelop.builder().version(1).nonce(request.getNonce()).gasLimit(request.getGasLimit()).gasPrice(request.getGasPrice()).chainID(client.getChainID()).build();
}

/**
* Builds the CandidateBasicInfo shared by candidateRegister and
* candidateUpdate.
*
* It lives here because each of those actions built the message twice --
* once in execute() and once in signedAction() -- from two copies of the
* same code. A field added to one copy and not the other produces an action
* that is correct when sent and wrong when signed offline, or the reverse,
* and nothing in either path notices.
*
* A null or empty BLS field is left unset rather than set to empty bytes,
* so an update that does not mention a key leaves an already registered one
* alone -- which is what the protocol's optional-key branch expects.
*/
protected static CandidateBasicInfo candidateBasicInfo(
String name, String operatorAddress, String rewardAddress, String blsPubKey, String blsPop) {
byte[] pubKey = hexToBytes(blsPubKey);
byte[] pop = hexToBytes(blsPop);
// Unconditionally invalid, in every fork era: the chain rejects it at
// validation, before a receipt exists, so the sender gets a bare RPC
// error with no action hash to look up. Saying so here is cheaper.
if (pubKey.length == 0 && pop.length > 0) {
throw new IllegalArgumentException("blsPop requires blsPubKey");
}
CandidateBasicInfo.Builder builder = CandidateBasicInfo.newBuilder()
.setName(name)
.setOperatorAddress(operatorAddress)
.setRewardAddress(rewardAddress);
if (pubKey.length > 0) {
builder.setBlsPubKey(ByteString.copyFrom(pubKey));
}
if (pop.length > 0) {
builder.setBlsPop(ByteString.copyFrom(pop));
}
return builder.build();
}

/**
* Decodes hex that may carry a 0x prefix.
*
* Numeric.hexStringToByteArray does not strip one: it reads '0' and 'x' as
* a byte, and Character.digit('x', 16) is -1, so the result is silently
* corrupt and the same length a caller would expect. BLS keys are quoted
* with the prefix nearly everywhere they are produced, so the two paths
* that take one go through this instead.
*/
private static byte[] hexToBytes(String hex) {
if (hex == null) {
return new byte[]{};
}
String trimmed = hex.trim();
if (trimmed.startsWith("0x") || trimmed.startsWith("0X")) {
trimmed = trimmed.substring(2);
}
return Numeric.hexStringToByteArray(trimmed);
}

protected String sendAction(Envelop envelop) {
SendActionResponse response = this.client.sendAction(SendActionRequest.newBuilder().setAction(signAction(envelop).action()).build());
return response.getActionHash();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import com.github.iotexproject.antenna.rpc.RPCMethod;
import com.github.iotexproject.antenna.utils.Numeric;
import com.github.iotexproject.grpc.types.Action;
import com.github.iotexproject.grpc.types.CandidateBasicInfo;
import com.github.iotexproject.grpc.types.CandidateRegister;
import com.google.protobuf.ByteString;

Expand All @@ -27,11 +26,12 @@ public CandidateRegisterMethod(RPCMethod client, CandidateRegisterRequest reques
@Override
public String execute() {
envelop.setCandidateRegister(CandidateRegister.newBuilder()
.setCandidate(CandidateBasicInfo.newBuilder()
.setName(request.getName())
.setOperatorAddress(request.getOperatorAddress())
.setRewardAddress(request.getRewardAddress())
.build())
.setCandidate(candidateBasicInfo(
request.getName(),
request.getOperatorAddress(),
request.getRewardAddress(),
request.getBlsPubKey(),
request.getBlsPop()))
.setStakedAmount(request.getStakedAmount())
.setStakedDuration(request.getStakedDuration())
.setAutoStake(request.isAutoStake())
Expand All @@ -43,11 +43,12 @@ public String execute() {

public Action signedAction() {
envelop.setCandidateRegister(CandidateRegister.newBuilder()
.setCandidate(CandidateBasicInfo.newBuilder()
.setName(request.getName())
.setOperatorAddress(request.getOperatorAddress())
.setRewardAddress(request.getRewardAddress())
.build())
.setCandidate(candidateBasicInfo(
request.getName(),
request.getOperatorAddress(),
request.getRewardAddress(),
request.getBlsPubKey(),
request.getBlsPop()))
.setStakedAmount(request.getStakedAmount())
.setStakedDuration(request.getStakedDuration())
.setAutoStake(request.isAutoStake())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import com.github.iotexproject.antenna.protocol.CandidateUpdateRequest;
import com.github.iotexproject.antenna.rpc.RPCMethod;
import com.github.iotexproject.grpc.types.Action;
import com.github.iotexproject.grpc.types.CandidateBasicInfo;

/**
* CandidateUpdate method.
Expand All @@ -23,20 +22,22 @@ public CandidateUpdateMethod(RPCMethod client, CandidateUpdateRequest request) {

@Override
public String execute() {
envelop.setCandidateUpdate(CandidateBasicInfo.newBuilder()
.setName(request.getName())
.setOperatorAddress(request.getOperatorAddress())
.setRewardAddress(request.getRewardAddress())
.build());
envelop.setCandidateUpdate(candidateBasicInfo(
request.getName(),
request.getOperatorAddress(),
request.getRewardAddress(),
request.getBlsPubKey(),
request.getBlsPop()));
return sendAction(envelop);
}

public Action signedAction() {
envelop.setCandidateUpdate(CandidateBasicInfo.newBuilder()
.setName(request.getName())
.setOperatorAddress(request.getOperatorAddress())
.setRewardAddress(request.getRewardAddress())
.build());
envelop.setCandidateUpdate(candidateBasicInfo(
request.getName(),
request.getOperatorAddress(),
request.getRewardAddress(),
request.getBlsPubKey(),
request.getBlsPop()));
return signAction(envelop).action();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.github.iotexproject.antenna.action.method;

import com.github.iotexproject.antenna.action.Envelop;
import com.github.iotexproject.antenna.crypto.Bech32;
import com.github.iotexproject.antenna.protocol.SetVoterRewardDestinationRequest;
import com.github.iotexproject.antenna.rpc.RPCMethod;
import com.github.iotexproject.grpc.types.Action;
import com.github.iotexproject.grpc.types.SetVoterRewardDestination;
import com.google.protobuf.ByteString;

/**
* SetVoterRewardDestination method.
*
* <p>The protocol reads the recipient with address.FromBytes, so the io1...
* string is decoded to its 20 raw bytes here. Sending the bech32 text instead
* produces an action that signs and is then rejected on decode.
*/
public class SetVoterRewardDestinationMethod extends AbstractMethod {
private final SetVoterRewardDestinationRequest request;
private final Envelop envelop;

public SetVoterRewardDestinationMethod(RPCMethod client, SetVoterRewardDestinationRequest request) {
super(client, request.getAccount());
this.request = request;
this.envelop = baseEnvelop(request);
}

private SetVoterRewardDestination payload() {
byte[] dec = Bech32.decode(request.getRecipient()).data;
byte[] raw = Bech32.convertBits(dec, 0, dec.length, 5, 8, false);
return SetVoterRewardDestination.newBuilder()
.setRecipient(ByteString.copyFrom(raw))
.build();
}

@Override
public String execute() {
envelop.setSetVoterRewardDestination(payload());
return sendAction(envelop);
}

public Action signedAction() {
envelop.setSetVoterRewardDestination(payload());
return signAction(envelop).action();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.github.iotexproject.antenna.action.method;

import com.github.iotexproject.antenna.action.Envelop;
import com.github.iotexproject.antenna.protocol.SetVoterRewardOptInRequest;
import com.github.iotexproject.antenna.rpc.RPCMethod;
import com.github.iotexproject.grpc.types.Action;
import com.github.iotexproject.grpc.types.SetVoterRewardOptIn;

/**
* SetVoterRewardOptIn method.
*
* <p>Opts the sending delegate into IIP-59 protocol-native voter reward
* distribution. The message is empty by design -- the sender is the delegate --
* so it must be set on the envelope explicitly rather than left to a
* "non-empty payload" check, which would drop it.
*/
public class SetVoterRewardOptInMethod extends AbstractMethod {
private final Envelop envelop;

public SetVoterRewardOptInMethod(RPCMethod client, SetVoterRewardOptInRequest request) {
super(client, request.getAccount());
this.envelop = baseEnvelop(request);
}

@Override
public String execute() {
envelop.setSetVoterRewardOptIn(SetVoterRewardOptIn.newBuilder().build());
return sendAction(envelop);
}

public Action signedAction() {
envelop.setSetVoterRewardOptIn(SetVoterRewardOptIn.newBuilder().build());
return signAction(envelop).action();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*
* @author XuePing Yang
*/
@Data

Check warning on line 10 in src/main/java/com/github/iotexproject/antenna/protocol/CandidateRegisterRequest.java

View workflow job for this annotation

GitHub Actions / build

Generating equals/hashCode implementation but without a call to superclass, even though this class does not extend java.lang.Object. If this is intentional, add '@EqualsAndHashCode(callSuper=false)' to your type.
public class CandidateRegisterRequest extends ActionRequest {
private String name;
private String operatorAddress;
Expand All @@ -18,4 +18,18 @@
private boolean autoStake;
private String ownerAddress;
private String payload;

/**
* BLS12-381 public key, hex, with or without a 0x prefix. Optional: leave
* it null to register or update without one, and to leave an already
* registered key untouched.
*/
private String blsPubKey;
/**
* Proof of possession for blsPubKey, hex, with or without a 0x prefix.
* Zanzibar verifies it against the candidate's OWNER address, not the
* sender's and not the operator's, and rejects the action with
* ErrUnauthorizedOperator (203) otherwise.
*/
private String blsPop;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,23 @@
*
* @author XuePing Yang
*/
@Data

Check warning on line 10 in src/main/java/com/github/iotexproject/antenna/protocol/CandidateUpdateRequest.java

View workflow job for this annotation

GitHub Actions / build

Generating equals/hashCode implementation but without a call to superclass, even though this class does not extend java.lang.Object. If this is intentional, add '@EqualsAndHashCode(callSuper=false)' to your type.
public class CandidateUpdateRequest extends ActionRequest {
private String name;
private String operatorAddress;
private String rewardAddress;

/**
* BLS12-381 public key, hex, with or without a 0x prefix. Optional: leave
* it null to register or update without one, and to leave an already
* registered key untouched.
*/
private String blsPubKey;
/**
* Proof of possession for blsPubKey, hex, with or without a 0x prefix.
* Zanzibar verifies it against the candidate's OWNER address, not the
* sender's and not the operator's, and rejects the action with
* ErrUnauthorizedOperator (203) otherwise.
*/
private String blsPop;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.github.iotexproject.antenna.protocol;

import lombok.Data;

/**
* setVoterRewardDestination request.
*
* <p>Redirects the sender's own IIP-59 voter rewards. Passing the sender's own
* address clears the override.
*
* <p>The protocol resolves the destination live at drain time rather than
* snapshotting it at the era freeze, so a change made mid-era applies to that
* era's payout. A compound (AutoDeposit) bucket still takes priority over it.
*/
@Data

Check warning on line 15 in src/main/java/com/github/iotexproject/antenna/protocol/SetVoterRewardDestinationRequest.java

View workflow job for this annotation

GitHub Actions / build

Generating equals/hashCode implementation but without a call to superclass, even though this class does not extend java.lang.Object. If this is intentional, add '@EqualsAndHashCode(callSuper=false)' to your type.
public class SetVoterRewardDestinationRequest extends ActionRequest {
/** Recipient in io1... form; converted to raw bytes on the wire. */
private String recipient;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.github.iotexproject.antenna.protocol;

import lombok.Data;

/**
* setVoterRewardOptIn request.
*
* <p>Carries no fields of its own: the sender is the delegate opting in. It is
* one-way -- the protocol has no counterpart action to opt back out.
*
* <p>Opting in with no reward portions published on the DelegateProfile
* contract is silently harmful rather than rejected. The protocol cannot tell
* "unset" from "explicit zero", falls back to a 100% commission, and pays every
* voter zero while producing successful distributions. Publish portions first.
*/
@Data

Check warning on line 16 in src/main/java/com/github/iotexproject/antenna/protocol/SetVoterRewardOptInRequest.java

View workflow job for this annotation

GitHub Actions / build

Generating equals/hashCode implementation but without a call to superclass, even though this class does not extend java.lang.Object. If this is intentional, add '@EqualsAndHashCode(callSuper=false)' to your type.
public class SetVoterRewardOptInRequest extends ActionRequest {
}
Loading
Loading