diff --git a/cluster/cluster_internal_test.go b/cluster/cluster_internal_test.go index 6a0527048..f8430b3e0 100644 --- a/cluster/cluster_internal_test.go +++ b/cluster/cluster_internal_test.go @@ -35,6 +35,19 @@ func TestDefinitionVerify(t *testing.T) { secret1, op1 := randomOperator(t) secret3, creator := randomCreator(t) + t.Run("verify definition v1.5 solo", func(t *testing.T) { + definition := randomDefinition(t, creator, Operator{}, Operator{}, + WithVersion(v1_5), + WithMultiVAddrs(RandomValidatorAddresses(2)), + ) + + definition, err = signCreator(secret3, definition) + require.NoError(t, err) + + err = definition.VerifySignatures() + require.NoError(t, err) + }) + t.Run("verify definition v1.5", func(t *testing.T) { definition := randomDefinition(t, creator, op0, op1, WithVersion(v1_5), diff --git a/cluster/distvalidator.go b/cluster/distvalidator.go index aa75ad79a..a0e1a55c0 100644 --- a/cluster/distvalidator.go +++ b/cluster/distvalidator.go @@ -29,9 +29,6 @@ type DistValidator struct { // PubShares are the public keys corresponding to each node's secret key share. // It can be used to verify a partial signature created by any node in the cluster. PubShares [][]byte `json:"public_shares,omitempty" ssz:"CompositeList[256],Bytes48" lock_hash:"1"` - - // FeeRecipientAddress Ethereum address override for this validator, defaults to definition withdrawal address. - FeeRecipientAddress []byte `json:"fee_recipient_address,omitempty" ssz:"Bytes20" lock_hash:"2"` } // PublicKey returns the validator BLS group public key. @@ -67,9 +64,8 @@ func distValidatorsFromV1x1(distValidators []distValidatorJSONv1x1) []DistValida var resp []DistValidator for _, dv := range distValidators { resp = append(resp, DistValidator{ - PubKey: dv.PubKey, - PubShares: dv.PubShares, - FeeRecipientAddress: dv.FeeRecipientAddress, + PubKey: dv.PubKey, + PubShares: dv.PubShares, }) } @@ -80,9 +76,8 @@ func distValidatorsToV1x1(distValidators []DistValidator) []distValidatorJSONv1x var resp []distValidatorJSONv1x1 for _, dv := range distValidators { resp = append(resp, distValidatorJSONv1x1{ - PubKey: dv.PubKey, - PubShares: dv.PubShares, - FeeRecipientAddress: dv.FeeRecipientAddress, + PubKey: dv.PubKey, + PubShares: dv.PubShares, }) } @@ -97,9 +92,8 @@ func distValidatorsFromV1x2orLater(distValidators []distValidatorJSONv1x2) []Dis shares = append(shares, share) } resp = append(resp, DistValidator{ - PubKey: dv.PubKey, - PubShares: shares, - FeeRecipientAddress: dv.FeeRecipientAddress, + PubKey: dv.PubKey, + PubShares: shares, }) } @@ -115,9 +109,8 @@ func distValidatorsToV1x2orLater(distValidators []DistValidator) []distValidator } resp = append(resp, distValidatorJSONv1x2{ - PubKey: dv.PubKey, - PubShares: shares, - FeeRecipientAddress: dv.FeeRecipientAddress, + PubKey: dv.PubKey, + PubShares: shares, }) } diff --git a/cluster/examples/cluster-definition-003.json b/cluster/examples/cluster-definition-003.json new file mode 100644 index 000000000..ba2bf0845 --- /dev/null +++ b/cluster/examples/cluster-definition-003.json @@ -0,0 +1,40 @@ +{ + "name": "solo flow", + "creator": { + "address": "0x65BA46f30Ac78DeDAc801F3787263B15E4E662C8", + "config_signature": "0x16123625db5eeaf6de350e2812b843bb2b792f219af919046ce4e96113ffd8f5517e93d7cb934be2a2466a153a2a0a87bc7fd340b84348bc6a03b0b1ec26c08f01" + }, + "operators": [ + { + "address": "", + "enr": "", + "config_signature": "", + "enr_signature": "" + }, + { + "address": "", + "enr": "", + "config_signature": "", + "enr_signature": "" + } + ], + "uuid": "52FDFC07-2182-654F-163F-5F0F9A621D72", + "version": "v1.5.0", + "timestamp": "2023-01-26T16:59:53+02:00", + "num_validators": 2, + "threshold": 2, + "validators": [ + { + "fee_recipient_address": "0x8da97239e9b517df4c248ff447bfc21032fa1f82", + "withdrawal_address": "0xac78bb58e615c380444f2413fe990511e38010d6" + }, + { + "fee_recipient_address": "0x85b72c6b5ee8e49f29fcba5eb6b26ce3e2840d3b", + "withdrawal_address": "0x354e962c2e8817ce8fb389b2fe3ef5b280cc6ddc" + } + ], + "dkg_algorithm": "default", + "fork_version": "0x90000069", + "config_hash": "0x7d79b11219f4145bbd317d73fa22168a9309a52ddda6fea92808c90f28570196", + "definition_hash": "0x277e4840485aa41af0af975c13ce39d99bc939a797589f16473f5d4aeab14e88" +} diff --git a/cluster/helpers.go b/cluster/helpers.go index 8f1c9ec63..76182b6a7 100644 --- a/cluster/helpers.go +++ b/cluster/helpers.go @@ -220,6 +220,38 @@ func putByteList(h ssz.HashWalker, b []byte, limit int, field string) error { return nil } +// putByteList appends b as a ssz fixed size byte array of length n. +func putBytesN(h ssz.HashWalker, b []byte, n int) error { + if len(b) > n { + return errors.New("bytes too long", z.Int("n", n), z.Int("l", len(b))) + } + + h.PutBytes(leftPad(b, n)) + + return nil +} + +// putHexBytes20 appends a 20 byte fixed size byte ssz array from the 0xhex address. +func putHexBytes20(h ssz.HashWalker, addr string) error { + b, err := from0xHex(addr, addressLen) + if err != nil { + return err + } + + h.PutBytes(leftPad(b, addressLen)) + + return nil +} + +// leftPad returns the byte slice left padded with zero to ensure a length of at least l. +func leftPad(b []byte, l int) []byte { + for len(b) < l { + b = append([]byte{0x00}, b...) + } + + return b +} + // to0xHex returns the bytes as a 0x prefixed hex string. func to0xHex(b []byte) string { if len(b) == 0 { diff --git a/cluster/helpers_internal_test.go b/cluster/helpers_internal_test.go index 5cdc06750..b6fba7f53 100644 --- a/cluster/helpers_internal_test.go +++ b/cluster/helpers_internal_test.go @@ -29,6 +29,14 @@ import ( "github.com/obolnetwork/charon/testutil" ) +func TestLeftPad(t *testing.T) { + b := []byte{0x01, 0x02} + require.Equal(t, []byte{0x01, 0x02}, leftPad(b, 1)) + require.Equal(t, []byte{0x01, 0x02}, leftPad(b, 2)) + require.Equal(t, []byte{0x00, 0x01, 0x02}, leftPad(b, 3)) + require.Equal(t, []byte{0x00, 0x00, 0x01, 0x02}, leftPad(b, 4)) +} + func TestVerifySig(t *testing.T) { secret, err := crypto.GenerateKey() require.NoError(t, err) diff --git a/cluster/lock.go b/cluster/lock.go index dc6812853..1b8c662f7 100644 --- a/cluster/lock.go +++ b/cluster/lock.go @@ -212,6 +212,12 @@ func unmarshalLockV1x0or1(data []byte) (lock Lock, err error) { return Lock{}, errors.Wrap(err, "unmarshal definition") } + for _, validator := range lockJSON.Validators { + if len(validator.FeeRecipientAddress) > 0 { + return Lock{}, errors.New("distributed validator fee recipient not supported anymore") + } + } + lock = Lock{ Definition: lockJSON.Definition, Validators: distValidatorsFromV1x1(lockJSON.Validators), @@ -228,6 +234,12 @@ func unmarshalLockV1x2orLater(data []byte) (lock Lock, err error) { return Lock{}, errors.Wrap(err, "unmarshal definition") } + for _, validator := range lockJSON.Validators { + if len(validator.FeeRecipientAddress) > 0 { + return Lock{}, errors.New("distributed validator fee recipient not supported anymore") + } + } + lock = Lock{ Definition: lockJSON.Definition, Validators: distValidatorsFromV1x2orLater(lockJSON.Validators), diff --git a/cluster/ssz.go b/cluster/ssz.go index 1a562c404..fca0ab483 100644 --- a/cluster/ssz.go +++ b/cluster/ssz.go @@ -31,6 +31,10 @@ const ( sszMaxDKGAlgorithm = 32 sszMaxOperators = 256 sszMaxValidators = 65536 + sszLenForkVersion = 4 + sszLenK1Sig = 65 + sszLenHash = 32 + sszLenPubKey = 48 ) // getDefinitionHashFunc returns the function to hash a definition based on the provided version. @@ -313,7 +317,9 @@ func hashDefinitionV1x5(d Definition, hh ssz.HashWalker, configOnly bool) error } // Field (7) 'ForkVersion' Bytes4 - hh.PutBytes(d.ForkVersion) + if err := putBytesN(hh, d.ForkVersion, sszLenForkVersion); err != nil { + return err + } // Field (8) 'Operators' CompositeList[256] { @@ -323,11 +329,9 @@ func hashDefinitionV1x5(d Definition, hh ssz.HashWalker, configOnly bool) error operatorIdx := hh.Index() // Field (0) 'Address' Bytes20 - addrBytes, err := from0xHex(o.Address, addressLen) - if err != nil { + if err := putHexBytes20(hh, o.Address); err != nil { return err } - hh.PutBytes(addrBytes) if !configOnly { // Field (1) 'ENR' ByteList[1024] @@ -336,10 +340,14 @@ func hashDefinitionV1x5(d Definition, hh ssz.HashWalker, configOnly bool) error } // Field (2) 'ConfigSignature' Bytes65 - hh.PutBytes(o.ConfigSignature) + if err := putBytesN(hh, o.ConfigSignature, sszLenK1Sig); err != nil { + return err + } // Field (3) 'ENRSignature' Bytes65 - hh.PutBytes(o.ENRSignature) + if err := putBytesN(hh, o.ENRSignature, sszLenK1Sig); err != nil { + return err + } } hh.Merkleize(operatorIdx) @@ -352,15 +360,15 @@ func hashDefinitionV1x5(d Definition, hh ssz.HashWalker, configOnly bool) error creatorIdx := hh.Index() // Field (0) 'Address' Bytes20 - addrBytes, err := from0xHex(d.Creator.Address, addressLen) - if err != nil { + if err := putHexBytes20(hh, d.Creator.Address); err != nil { return err } - hh.PutBytes(addrBytes) if !configOnly { // Field (1) 'ConfigSignature' Bytes65 - hh.PutBytes(d.Creator.ConfigSignature) + if err := putBytesN(hh, d.Creator.ConfigSignature, sszLenK1Sig); err != nil { + return err + } } hh.Merkleize(creatorIdx) } @@ -372,22 +380,16 @@ func hashDefinitionV1x5(d Definition, hh ssz.HashWalker, configOnly bool) error for _, v := range d.ValidatorAddresses { validatorIdx := hh.Index() - feeRecipientAddress, err := from0xHex(v.FeeRecipientAddress, addressLen) - if err != nil { + // Field (0) 'FeeRecipientAddress' Bytes20 + if err := putHexBytes20(hh, v.FeeRecipientAddress); err != nil { return err } - withdrawalAddress, err := from0xHex(v.WithdrawalAddress, addressLen) - if err != nil { + // Field (1) 'WithdrawalAddress' Bytes20 + if err := putHexBytes20(hh, v.WithdrawalAddress); err != nil { return err } - // Field (0) 'FeeRecipientAddress' Bytes20 - hh.PutBytes(feeRecipientAddress) - - // Field (1) 'WithdrawalAddress' Bytes20 - hh.PutBytes(withdrawalAddress) - hh.Merkleize(validatorIdx) } hh.MerkleizeWithMixin(validatorsIdx, num, sszMaxValidators) @@ -395,7 +397,9 @@ func hashDefinitionV1x5(d Definition, hh ssz.HashWalker, configOnly bool) error if !configOnly { // Field (11) 'ConfigHash' Bytes32 - hh.PutBytes(d.ConfigHash) + if err := putBytesN(hh, d.ConfigHash, sszLenHash); err != nil { + return err + } } hh.Merkleize(indx) @@ -408,8 +412,10 @@ func hashLock(l Lock) ([32]byte, error) { var hashFunc func(Lock, ssz.HashWalker) error if isV1x0(l.Version) || isV1x1(l.Version) || isV1x2(l.Version) { hashFunc = hashLockLegacy - } else if isAnyVersion(l.Version, v1_3, v1_4, v1_5) { //nolint:revive // Early return not applicable to else if - hashFunc = hashLockV1x3orLater + } else if isAnyVersion(l.Version, v1_3, v1_4) { + hashFunc = hashLockV1x3or4 + } else if isAnyVersion(l.Version, v1_5) { //nolint:revive // Early return not applicable to else if + hashFunc = hashLockV1x5 } else { return [32]byte{}, errors.New("unknown version") } @@ -429,8 +435,8 @@ func hashLock(l Lock) ([32]byte, error) { return resp, nil } -// hashLockV1x3orLater hashes the latest lock hash. -func hashLockV1x3orLater(l Lock, hh ssz.HashWalker) error { +// hashLockV1x3or4 hashes the version v1.3 or v1.4 of the lock. +func hashLockV1x3or4(l Lock, hh ssz.HashWalker) error { indx := hh.Index() defHashFunc, err := getDefinitionHashFunc(l.Version) @@ -448,7 +454,38 @@ func hashLockV1x3orLater(l Lock, hh ssz.HashWalker) error { subIndx := hh.Index() num := uint64(len(l.Validators)) for _, validator := range l.Validators { - if err := hashValidatorV1x3OrLater(validator, hh); err != nil { + if err := hashValidatorV1x3Or4(validator, hh); err != nil { + return err + } + } + hh.MerkleizeWithMixin(subIndx, num, sszMaxValidators) + } + + hh.Merkleize(indx) + + return nil +} + +// hashLockV1x5 hashes the version v1.5 of the lock. +func hashLockV1x5(l Lock, hh ssz.HashWalker) error { + indx := hh.Index() + + defHashFunc, err := getDefinitionHashFunc(l.Version) + if err != nil { + return err + } + + // Field (0) 'Definition' Composite + if err := defHashFunc(l.Definition, hh, false); err != nil { + return err + } + + // Field (1) 'Validators' CompositeList[65536] + { + subIndx := hh.Index() + num := uint64(len(l.Validators)) + for _, validator := range l.Validators { + if err := hashValidatorV1x5(validator, hh); err != nil { return err } } @@ -460,8 +497,8 @@ func hashLockV1x3orLater(l Lock, hh ssz.HashWalker) error { return nil } -// hashValidatorV1x3 hashes the distributed validator. -func hashValidatorV1x3OrLater(v DistValidator, hh ssz.HashWalker) error { +// hashValidatorV1x3Or4 hashes the distributed validator v1.3 or v1.4. +func hashValidatorV1x3Or4(v DistValidator, hh ssz.HashWalker) error { indx := hh.Index() // Field (0) 'PubKey' Bytes48 @@ -478,7 +515,34 @@ func hashValidatorV1x3OrLater(v DistValidator, hh ssz.HashWalker) error { } // Field (2) 'FeeRecipientAddress' Bytes20 - hh.PutBytes(v.FeeRecipientAddress) + hh.PutBytes(nil) + + hh.Merkleize(indx) + + return nil +} + +// hashValidatorV1x5 hashes the distributed validator v1.5. +func hashValidatorV1x5(v DistValidator, hh ssz.HashWalker) error { + indx := hh.Index() + + // Field (0) 'PubKey' Bytes48 + if err := putBytesN(hh, v.PubKey, sszLenPubKey); err != nil { + return err + } + + // Field (1) 'Pubshares' CompositeList[256] + { + subIndx := hh.Index() + num := uint64(len(v.PubShares)) + for _, pubshare := range v.PubShares { + // Bytes48 + if err := putBytesN(hh, pubshare, sszLenPubKey); err != nil { + return err + } + } + hh.MerkleizeWithMixin(subIndx, num, sszMaxOperators) + } hh.Merkleize(indx) @@ -529,7 +593,7 @@ func hashValidatorLegacy(v DistValidator, hh ssz.HashWalker) error { } // Field (2) 'FeeRecipientAddress' - hh.PutBytes([]byte(to0xHex(v.FeeRecipientAddress))) + hh.PutBytes(nil) hh.Merkleize(indx)