Skip to content
Open
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
76 changes: 76 additions & 0 deletions action/candidate_register.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@
// _candidateRegisterInterface is the interface of the abi encoding of stake action
_candidateRegisterMethod abi.Method
_candidateRegisterWithBLSMethod abi.Method
// _candidateRegisterWithBLSAndPoPMethod is the V2 ABI entry that adds the
// BLS proof-of-possession parameter alongside the existing WithBLS fields.
// Required post-fork (EnforceBLSPoP gate) because the handler rejects
// registrations whose blsPop is missing. The pre-fork WithBLS method is
// retained so existing tooling that has not yet adopted PoP continues to
// compile registrations — those txs will still be rejected at the handler
// post-fork, but coexistence avoids breaking the function-selector ID of
// the legacy method for any client tracking it.
_candidateRegisterWithBLSAndPoPMethod abi.Method
_candidateRegisteredEvent abi.Event
_stakedEvent abi.Event
_candidateActivatedEvent abi.Event
Expand Down Expand Up @@ -67,6 +76,11 @@
autoStake bool
payload []byte
blsPubKey []byte
// blsPop is the proof-of-possession signature over
// BLSPopSigningRoot(blsPubKey, ownerAddress). Required at handler
// time once EnforceBLSPoP is active; carried alongside blsPubKey so
// it always travels with the registration.
blsPop []byte
}

func init() {
Expand All @@ -80,6 +94,10 @@
if !ok {
panic("fail to load the method")
}
_candidateRegisterWithBLSAndPoPMethod, ok = abi.Methods["candidateRegisterWithBLSAndPoP"]
if !ok {
panic("fail to load the candidateRegisterWithBLSAndPoP method")
}
_candidateRegisteredEvent, ok = abi.Events["CandidateRegistered"]
if !ok {
panic("fail to load the event")
Expand Down Expand Up @@ -149,11 +167,15 @@
}

// NewCandidateRegisterWithBLS creates a CandidateRegister instance with BLS public key
// and the corresponding proof-of-possession. blsPop must be a 96-byte BLS
// signature over BLSPopSigningRoot(blsPubKey, ownerAddress); the handler
// enforces this once EnforceBLSPoP is active.
func NewCandidateRegisterWithBLS(
name, operatorAddrStr, rewardAddrStr, ownerAddrStr, amountStr string,
duration uint32,
autoStake bool,
blsPubKey []byte,
blsPop []byte,
payload []byte,
) (*CandidateRegister, error) {
cr, err := NewCandidateRegister(name, operatorAddrStr, rewardAddrStr, ownerAddrStr, amountStr, duration, autoStake, payload)
Expand All @@ -168,6 +190,10 @@
cr.amount = nil
cr.blsPubKey = make([]byte, len(blsPubKey))
copy(cr.blsPubKey, blsPubKey)
if len(blsPop) > 0 {
cr.blsPop = make([]byte, len(blsPop))
copy(cr.blsPop, blsPop)
}
return cr, nil
}

Expand Down Expand Up @@ -215,6 +241,13 @@
return cr.blsPubKey
}

// BLSPop returns the BLS proof-of-possession that accompanies the
// blsPubKey. Empty for legacy registrations and for pre-fork
// CandidateRegister actions; required once EnforceBLSPoP is active.
func (cr *CandidateRegister) BLSPop() []byte {
return cr.blsPop
}

// Serialize returns a raw byte stream of the CandidateRegister struct
func (cr *CandidateRegister) Serialize() []byte {
return byteutil.Must(proto.Marshal(cr.Proto()))
Expand Down Expand Up @@ -249,6 +282,10 @@
case cr.WithBLS():
act.Candidate.BlsPubKey = make([]byte, len(cr.blsPubKey))
copy(act.Candidate.BlsPubKey, cr.blsPubKey)
if len(cr.blsPop) > 0 {
act.Candidate.BlsPop = make([]byte, len(cr.blsPop))
copy(act.Candidate.BlsPop, cr.blsPop)
}
if cr.value != nil {
act.StakedAmount = cr.value.String()
}
Expand All @@ -262,7 +299,7 @@
}

// LoadProto converts a protobuf's Action to CandidateRegister
func (cr *CandidateRegister) LoadProto(pbAct *iotextypes.CandidateRegister) error {

Check failure on line 302 in action/candidate_register.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=iotexproject_iotex-core&issues=AZ60ePWbLnQ0n5Zdw37D&open=AZ60ePWbLnQ0n5Zdw37D&pullRequest=4854
if pbAct == nil {
return ErrNilProto
}
Expand All @@ -288,6 +325,10 @@
if withBLS {
cr.blsPubKey = make([]byte, len(pbAct.Candidate.GetBlsPubKey()))
copy(cr.blsPubKey, pbAct.Candidate.GetBlsPubKey())
if pop := pbAct.Candidate.GetBlsPop(); len(pop) > 0 {
cr.blsPop = make([]byte, len(pop))
copy(cr.blsPop, pop)
}
}
if len(pbAct.GetStakedAmount()) > 0 {
amount, ok := new(big.Int).SetString(pbAct.GetStakedAmount(), 10)
Expand Down Expand Up @@ -347,7 +388,27 @@
return nil, ErrAddress
}
switch {
case cr.WithBLS() && len(cr.blsPop) > 0:
// Post-fork path: blsPop is required, encode with the V2 method so
// the function-selector ID committed in the calldata signals
// "PoP-carrying registration" to all decoders.
data, err := _candidateRegisterWithBLSAndPoPMethod.Inputs.Pack(
cr.name,
common.BytesToAddress(cr.operatorAddress.Bytes()),
common.BytesToAddress(cr.rewardAddress.Bytes()),
common.BytesToAddress(cr.ownerAddress.Bytes()),
cr.duration,
cr.autoStake,
cr.blsPubKey,
cr.blsPop,
cr.payload)
if err != nil {
return nil, err
}
return append(_candidateRegisterWithBLSAndPoPMethod.ID, data...), nil
case cr.WithBLS():
// Legacy WithBLS without PoP. Pre-fork still works; post-fork the
// handler rejects this for lacking proof-of-possession.
data, err := _candidateRegisterWithBLSMethod.Inputs.Pack(
cr.name,
common.BytesToAddress(cr.operatorAddress.Bytes()),
Expand Down Expand Up @@ -491,12 +552,17 @@
if len(data) <= 4 {
return nil, errDecodeFailure
}
withPoP := false
switch {
case bytes.Equal(_candidateRegisterMethod.ID, data[:4]):
method = _candidateRegisterMethod
case bytes.Equal(_candidateRegisterWithBLSMethod.ID, data[:4]):
method = _candidateRegisterWithBLSMethod
withBLS = true
case bytes.Equal(_candidateRegisterWithBLSAndPoPMethod.ID, data[:4]):
method = _candidateRegisterWithBLSAndPoPMethod
withBLS = true
withPoP = true
default:
return nil, errDecodeFailure
}
Expand Down Expand Up @@ -540,6 +606,16 @@
if err != nil {
return nil, errors.Wrap(err, "failed to parse BLS public key")
}
if withPoP {
pop, ok := paramsMap["blsPop"].([]byte)
if !ok {
return nil, errors.Wrapf(errDecodeFailure, "invalid blsPop %+v", paramsMap["blsPop"])
}
if len(pop) == 0 {
return nil, errors.Wrap(errDecodeFailure, "blsPop is empty")
}
cr.blsPop = pop
}
} else {
if cr.amount, ok = paramsMap["amount"].(*big.Int); !ok {
return nil, errDecodeFailure
Expand Down
64 changes: 63 additions & 1 deletion action/candidate_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
// _candidateUpdateMethod is the interface of the abi encoding of stake action
_candidateUpdateMethod abi.Method
_candidateUpdateWithBLSMethod abi.Method
// _candidateUpdateWithBLSAndPoPMethod is the V2 ABI entry that adds the
// BLS proof-of-possession parameter alongside the existing WithBLS
// fields. Required post-fork (EnforceBLSPoP gate) — the handler rejects
// updates that rotate the blsPubKey without a fresh PoP.
_candidateUpdateWithBLSAndPoPMethod abi.Method
_candidateUpdateWithBLSEvent abi.Event
_ EthCompatibleAction = (*CandidateUpdate)(nil)
)
Expand All @@ -41,6 +46,11 @@
operatorAddress address.Address
rewardAddress address.Address
blsPubKey []byte
// blsPop is the proof-of-possession for blsPubKey. Required at
// handler time once EnforceBLSPoP is active: any update that
// introduces or rotates the BLS key must carry a fresh PoP to
// prevent rogue-key attacks.
blsPop []byte
}

// CandidateUpdateOption defines the method to customize CandidateUpdate
Expand Down Expand Up @@ -69,6 +79,10 @@
if !ok {
panic("fail to load the method")
}
_candidateUpdateWithBLSAndPoPMethod, ok = NativeStakingContractABI().Methods["candidateUpdateWithBLSAndPoP"]
if !ok {
panic("fail to load the candidateUpdateWithBLSAndPoP method")
}
_candidateUpdateWithBLSEvent, ok = NativeStakingContractABI().Events["CandidateUpdated"]
if !ok {
panic("fail to load the event")
Expand Down Expand Up @@ -99,7 +113,9 @@
}

// NewCandidateUpdateWithBLS creates a CandidateUpdate instance with BLS public key
func NewCandidateUpdateWithBLS(name, operatorAddrStr, rewardAddrStr string, pubkey []byte) (*CandidateUpdate, error) {
// and proof-of-possession. blsPop may be empty for pre-fork callers; the
// handler enforces non-empty PoP once EnforceBLSPoP is active.
func NewCandidateUpdateWithBLS(name, operatorAddrStr, rewardAddrStr string, pubkey []byte, pop []byte) (*CandidateUpdate, error) {
cu, err := NewCandidateUpdate(name, operatorAddrStr, rewardAddrStr)
if err != nil {
return nil, err
Expand All @@ -110,6 +126,10 @@
}
cu.blsPubKey = make([]byte, len(pubkey))
copy(cu.blsPubKey, pubkey)
if len(pop) > 0 {
cu.blsPop = make([]byte, len(pop))
copy(cu.blsPop, pop)
}
return cu, nil
}

Expand All @@ -127,6 +147,13 @@
return cu.blsPubKey
}

// BLSPop returns the proof-of-possession for the BLS pubkey carried
// by this update. Empty for updates that do not rotate the BLS key
// and for pre-fork updates; required once EnforceBLSPoP is active.
func (cu *CandidateUpdate) BLSPop() []byte {
return cu.blsPop
}

// WithBLS returns true if the candidate update action is with BLS public key
func (cu *CandidateUpdate) WithBLS() bool {
return len(cu.blsPubKey) > 0
Expand Down Expand Up @@ -159,6 +186,10 @@
act.BlsPubKey = make([]byte, len(cu.blsPubKey))
copy(act.BlsPubKey, cu.blsPubKey)
}
if len(cu.blsPop) > 0 {
act.BlsPop = make([]byte, len(cu.blsPop))
copy(act.BlsPop, cu.blsPop)
}
return act
}

Expand Down Expand Up @@ -188,6 +219,10 @@
if len(pbAct.GetBlsPubKey()) > 0 {
cu.blsPubKey = make([]byte, len(pbAct.GetBlsPubKey()))
copy(cu.blsPubKey, pbAct.GetBlsPubKey())
if pop := pbAct.GetBlsPop(); len(pop) > 0 {
cu.blsPop = make([]byte, len(pop))
copy(cu.blsPop, pop)
}
}
return nil
}
Expand All @@ -214,7 +249,19 @@
return nil, ErrAddress
}
switch {
case cu.WithBLS() && len(cu.blsPop) > 0:
// Post-fork path: rotate the BLS pubkey with a fresh PoP. V2
// selector signals the calldata carries proof-of-possession.
data, err := _candidateUpdateWithBLSAndPoPMethod.Inputs.Pack(cu.name,
common.BytesToAddress(cu.operatorAddress.Bytes()),
common.BytesToAddress(cu.rewardAddress.Bytes()), cu.blsPubKey, cu.blsPop)
if err != nil {
return nil, err
}
return append(_candidateUpdateWithBLSAndPoPMethod.ID, data...), nil
case cu.WithBLS():
// Legacy WithBLS without PoP — works pre-fork; post-fork the
// handler rejects this for lacking proof-of-possession.
data, err := _candidateUpdateWithBLSMethod.Inputs.Pack(cu.name,
common.BytesToAddress(cu.operatorAddress.Bytes()),
common.BytesToAddress(cu.rewardAddress.Bytes()), cu.blsPubKey)
Expand All @@ -234,7 +281,7 @@
}

// NewCandidateUpdateFromABIBinary decodes data into CandidateUpdate action
func NewCandidateUpdateFromABIBinary(data []byte) (*CandidateUpdate, error) {

Check failure on line 284 in action/candidate_update.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 21 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=iotexproject_iotex-core&issues=AZ7N_xEOt4w2Ip9YJizy&open=AZ7N_xEOt4w2Ip9YJizy&pullRequest=4854
var (
paramsMap = map[string]interface{}{}
ok bool
Expand All @@ -247,12 +294,17 @@
if len(data) <= 4 {
return nil, errDecodeFailure
}
withPoP := false
switch {
case bytes.Equal(_candidateUpdateMethod.ID, data[:4]):
method = &_candidateUpdateMethod
case bytes.Equal(_candidateUpdateWithBLSMethod.ID, data[:4]):
method = &_candidateUpdateWithBLSMethod
withBLS = true
case bytes.Equal(_candidateUpdateWithBLSAndPoPMethod.ID, data[:4]):
method = &_candidateUpdateWithBLSAndPoPMethod
withBLS = true
withPoP = true
default:
return nil, errors.Wrapf(errDecodeFailure, "unknown method prefix %x", data[:4])
}
Expand All @@ -279,6 +331,16 @@
if err != nil {
return nil, errors.Wrap(err, "failed to parse BLS public key")
}
if withPoP {
pop, ok := paramsMap["blsPop"].([]byte)
if !ok {
return nil, errors.Wrapf(errDecodeFailure, "blsPop is not []byte: %v", paramsMap["blsPop"])
}
if len(pop) == 0 {
return nil, errors.Wrap(errDecodeFailure, "blsPop is empty")
}
cu.blsPop = pop
}
}
return &cu, nil
}
Expand Down
22 changes: 20 additions & 2 deletions action/candidateregister_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ func TestCandidateRegister(t *testing.T) {
blsPubKey := blsPrivKey.PublicKey().Bytes()
for _, test := range candidateRegisterTestParams {
test.blsPubKey = blsPubKey
cr, err := NewCandidateRegisterWithBLS(test.Name, test.OperatorAddrStr, test.RewardAddrStr, test.OwnerAddrStr, test.AmountStr, test.Duration, test.AutoStake, test.blsPubKey, test.Payload)
cr, err := NewCandidateRegisterWithBLS(test.Name, test.OperatorAddrStr, test.RewardAddrStr, test.OwnerAddrStr, test.AmountStr, test.Duration, test.AutoStake, test.blsPubKey, nil, test.Payload)
require.Equal(test.Expected, errors.Cause(err))
if err != nil {
continue
Expand All @@ -167,6 +167,8 @@ func TestCandidateRegisterABIEncodeAndDecode(t *testing.T) {
require.Equal(test.AutoStake, stake.AutoStake())
if stake.WithBLS() {
require.Equal(input.BLSPubKey(), stake.BLSPubKey())
require.Equal(input.BLSPop(), stake.BLSPop(),
"PoP must round-trip through the ABI encode/decode path")
} else {
require.Equal(test.AmountStr, stake.Amount().String())
}
Expand All @@ -190,10 +192,26 @@ func TestCandidateRegisterABIEncodeAndDecode(t *testing.T) {
t.Run("with public key", func(t *testing.T) {
pk, err := crypto.GenerateBLS12381PrivateKey(identityset.PrivateKey(0).Bytes())
require.NoError(err)
stake, err := NewCandidateRegisterWithBLS(test.Name, test.OperatorAddrStr, test.RewardAddrStr, test.OwnerAddrStr, test.AmountStr, test.Duration, test.AutoStake, pk.PublicKey().Bytes(), test.Payload)
stake, err := NewCandidateRegisterWithBLS(test.Name, test.OperatorAddrStr, test.RewardAddrStr, test.OwnerAddrStr, test.AmountStr, test.Duration, test.AutoStake, pk.PublicKey().Bytes(), nil, test.Payload)
require.NoError(err)
encode(stake)
})
t.Run("with public key and PoP", func(t *testing.T) {
// V2 selector path: ensure the candidateRegisterWithBLSAndPoP ABI
// entry actually round-trips the blsPop field through Pack /
// Unpack. Catches the bug envestcc flagged where the web3 path
// silently dropped PoP.
pk, err := crypto.GenerateBLS12381PrivateKey(identityset.PrivateKey(0).Bytes())
require.NoError(err)
pop := make([]byte, crypto.BLSAggregateSignatureLength)
for i := range pop {
pop[i] = byte(i + 1) // any non-empty bytes; we're only testing the codec here
}
stake, err := NewCandidateRegisterWithBLS(test.Name, test.OperatorAddrStr, test.RewardAddrStr, test.OwnerAddrStr, test.AmountStr, test.Duration, test.AutoStake, pk.PublicKey().Bytes(), pop, test.Payload)
require.NoError(err)
require.Equal(pop, stake.BLSPop())
encode(stake)
})

}

Expand Down
24 changes: 23 additions & 1 deletion action/candidateupdate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func TestCandidateUpdate(t *testing.T) {
require := require.New(t)
blsPrivKey, err := crypto.GenerateBLS12381PrivateKey(identityset.PrivateKey(0).Bytes())
require.NoError(err)
cu, err := NewCandidateUpdateWithBLS(_cuName, _cuOperatorAddrStr, _cuRewardAddrStr, blsPrivKey.PublicKey().Bytes())
cu, err := NewCandidateUpdateWithBLS(_cuName, _cuOperatorAddrStr, _cuRewardAddrStr, blsPrivKey.PublicKey().Bytes(), nil)
require.NoError(err)
elp := (&EnvelopeBuilder{}).SetNonce(_cuNonce).SetGasLimit(_cuGasLimit).
SetGasPrice(_cuGasPrice).SetAction(cu).Build()
Expand Down Expand Up @@ -87,4 +87,26 @@ func TestCandidateUpdate(t *testing.T) {
_, err = cu.EthData()
require.Equal(ErrAddress, err)
})
t.Run("ABI encode with PoP", func(t *testing.T) {
// V2 selector path: round-trip blsPop through the
// candidateUpdateWithBLSAndPoP ABI entry. Guards against
// re-occurrence of the web3-path bug where the PoP field was
// silently dropped because the ABI method had no slot for it.
pop := make([]byte, crypto.BLSAggregateSignatureLength)
for i := range pop {
pop[i] = byte(i + 1)
}
cuWithPoP, err := NewCandidateUpdateWithBLS(_cuName, _cuOperatorAddrStr, _cuRewardAddrStr, blsPrivKey.PublicKey().Bytes(), pop)
require.NoError(err)
data, err := cuWithPoP.EthData()
require.NoError(err)
decoded, err := NewCandidateUpdateFromABIBinary(data)
require.NoError(err)
require.Equal(_cuName, decoded.Name())
require.Equal(_cuOperatorAddrStr, decoded.OperatorAddress().String())
require.Equal(_cuRewardAddrStr, decoded.RewardAddress().String())
require.Equal(blsPrivKey.PublicKey().Bytes(), decoded.BLSPubKey())
require.Equal(pop, decoded.BLSPop(),
"PoP must round-trip through the V2 ABI codec")
})
}
Loading
Loading