diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8dc98a8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,73 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Go library (`github.com/ssvlabs/dkg-spec`) defining the specification for SSV's Distributed Key Generation (DKG) protocol. Operators use BLS threshold cryptography to generate shared validator keys for Ethereum — no single operator holds the full private key. + +## Build & Test Commands + +```bash +# Generate SSZ encoding (types_encoding.go) and EIP-1271 bindings +# Requires: abigen (geth tools) for eip1271 contract generation +go generate ./... + +# Build +go build -v ./... + +# Run all tests (tests live in testing/ package, not ./...) +go test ./testing/ ./crypto/ + +# Run a single test +go test ./testing/ -run TestValidateResults +go test ./crypto/ -run TestVerifyDepositData +``` + +**Important**: `go test` at root runs nothing useful. Tests are in `testing/` and `crypto/` packages. + +## Architecture + +### Three DKG Operations + +All operations follow the same pattern: **initiator** sends a request, **operators** execute the ceremony, each operator produces a `Result` containing a `SignedProof`. + +1. **Init** (`init.go`, `operator.go:Init`) — Fresh DKG ceremony creating new BLS key shares +2. **Reshare** (`reshare.go`, `operator.go:Reshare`) — Redistribute shares to a new operator set. Requires owner ECDSA/EIP-1271 signature and proofs from the previous ceremony +3. **Resign** (`resign.go`, `operator.go:Resign`) — Re-sign with corrected nonces without generating new keys. Also requires owner signature + +### Key Data Flow + +`Init/Reshare/Resign` message → operator validates → DKG ceremony (stubbed in spec) → `BuildResult()` → `Result` containing: +- `DepositPartialSignature` — BLS partial sig over ETH2 deposit data +- `OwnerNoncePartialSignature` — BLS partial sig over `owner:nonce` hash +- `SignedProof` — RSA-signed proof linking validator pubkey, encrypted share, share pubkey, and owner + +`ValidateResults()` in `result.go` is the main verification entry point: recovers the master public key from shares, reconstructs master signatures, and verifies deposit data against the Ethereum network fork. + +### Cryptography Layers (`crypto/`) + +- **BLS** (`bls.go`) — Threshold key recovery and partial signature verification using `herumi/bls-eth-go-binary`. Must call `InitBLS()` before use. +- **RSA** (`rsa.go`) — Operator key pairs for encrypting shares and signing proofs (PSS signatures, PKCS1v15 encryption). Operator public keys are base64-encoded PEM. +- **Beacon** (`beacon.go`) — ETH2 deposit data computation and verification. Network determined by fork version bytes. Withdrawal credentials must be 32 bytes with a valid prefix: 0x01 (ETH1) or 0x02 (compounding). Use `WithdrawalCredentials(prefix, addr)` to construct them from a prefix byte and a 20-byte address. +- **Owner Signature** (`signature.go`) — Verifies owner authorization via either EOA (ECDSA with EIP-155 chain ID support) or smart contract wallet (EIP-1271). + +### Encoding + +All types use SSZ serialization. `types_encoding.go` is auto-generated from `types.go` via `fastssz/sszgen`. The `SSZMarshaller` interface is used for bulk message hashing (reshare/resign flows). + +### Valid Cluster Sizes + +Only 4 fixed operator counts are supported: **4** (t=3), **7** (t=5), **10** (t=7), **13** (t=9). Threshold follows `t = 2f+1` where `n = 3f+1`. + +### Testing Structure + +- `testing/` — Integration tests for init, result, reshare, resign validation +- `testing/fixtures/` — Deterministic test data: hardcoded RSA operator keys, BLS shares, pre-computed signatures and proofs for 4/7/10/13 operator configurations +- `testing/stubs/` — Mock `ETHClient` for EIP-1271/EOA signature verification +- `crypto/` — Unit tests for beacon deposit computation and BLS signature verification + +### Generated Files (do not edit manually) + +- `types_encoding.go` — SSZ marshaling from `go generate` on `types.go` +- `eip1271/eip1271.go` — Contract bindings from `abigen` on `eip1271/abi.abi` diff --git a/crypto/beacon.go b/crypto/beacon.go index b981e46..f32cc77 100644 --- a/crypto/beacon.go +++ b/crypto/beacon.go @@ -11,17 +11,31 @@ import ( const ( // https://eips.ethereum.org/EIPS/eip-7251 - MIN_ACTIVATION_BALANCE phase0.Gwei = 32000000000 - MAX_EFFECTIVE_BALANCE phase0.Gwei = 2048000000000 - ETH1WithdrawalPrefixByte = byte(1) + MIN_ACTIVATION_BALANCE phase0.Gwei = 32000000000 + MAX_EFFECTIVE_BALANCE phase0.Gwei = 2048000000000 + ETH1WithdrawalPrefix = byte(1) + CompoundingWithdrawalPrefix = byte(2) ) -func ETH1WithdrawalCredentials(withdrawalAddr []byte) []byte { - withdrawalCredentials := make([]byte, 32) - copy(withdrawalCredentials[:1], []byte{ETH1WithdrawalPrefixByte}) - // withdrawalCredentials[1:12] == b'\x00' * 11 // this is not needed since cells are zeroed anyway - copy(withdrawalCredentials[12:], withdrawalAddr) - return withdrawalCredentials +// WithdrawalCredentials constructs 32-byte withdrawal credentials from a prefix byte and a 20-byte address. +func WithdrawalCredentials(prefix byte, withdrawalAddr [20]byte) []byte { + creds := make([]byte, 32) + creds[0] = prefix + copy(creds[12:], withdrawalAddr[:]) + return creds +} + +// ValidateWithdrawalCredentials checks that credentials are exactly 32 bytes with a valid prefix (0x01 or 0x02). +// Bytes [1:12] (zero padding) are not enforced — the Ethereum beacon chain accepts any padding, +// and WithdrawalCredentials() always zero-pads. +func ValidateWithdrawalCredentials(creds []byte) error { + if len(creds) != 32 { + return fmt.Errorf("withdrawal credentials must be 32 bytes, got %d", len(creds)) + } + if creds[0] != ETH1WithdrawalPrefix && creds[0] != CompoundingWithdrawalPrefix { + return fmt.Errorf("invalid withdrawal credential prefix: 0x%02x", creds[0]) + } + return nil } func ComputeDepositMessageSigningRoot(network core.Network, message *phase0.DepositMessage) (phase0.Root, error) { @@ -91,8 +105,12 @@ func DepositDataRootForFork( if err != nil { return phase0.Root{}, err } + if err := ValidateWithdrawalCredentials(withdrawalCredentials); err != nil { + return phase0.Root{}, err + } return ComputeDepositMessageSigningRoot(network, &phase0.DepositMessage{ PublicKey: phase0.BLSPubKey(validatorPK), Amount: amount, - WithdrawalCredentials: ETH1WithdrawalCredentials(withdrawalCredentials)}) + WithdrawalCredentials: withdrawalCredentials, + }) } diff --git a/crypto/beacon_test.go b/crypto/beacon_test.go index 175906e..1b42655 100644 --- a/crypto/beacon_test.go +++ b/crypto/beacon_test.go @@ -10,23 +10,38 @@ import ( "github.com/stretchr/testify/require" ) -func TestETH1WithdrawalCredentials(t *testing.T) { - t.Run("eth1 withdrawal cred from string", func(t *testing.T) { +func TestWithdrawalCredentials(t *testing.T) { + t.Run("0x01 from hex address", func(t *testing.T) { eth1Address := common.HexToAddress("d999bc994e0274235b65ca72ec430b8de3eb7df9") - require.EqualValues(t, ETH1WithdrawalCredentials(eth1Address[:]), []byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xd9, 0x99, 0xbc, 0x99, 0x4e, 0x2, 0x74, 0x23, 0x5b, 0x65, 0xca, 0x72, 0xec, 0x43, 0xb, 0x8d, 0xe3, 0xeb, 0x7d, 0xf9}) + require.EqualValues(t, WithdrawalCredentials(ETH1WithdrawalPrefix, eth1Address), []byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xd9, 0x99, 0xbc, 0x99, 0x4e, 0x2, 0x74, 0x23, 0x5b, 0x65, 0xca, 0x72, 0xec, 0x43, 0xb, 0x8d, 0xe3, 0xeb, 0x7d, 0xf9}) }) - t.Run("eth1 withdrawal cred from bytes", func(t *testing.T) { + t.Run("0x01 from byte address", func(t *testing.T) { eth1Address := common.Address{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20} - require.EqualValues(t, ETH1WithdrawalCredentials(eth1Address[:]), []byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}) + require.EqualValues(t, WithdrawalCredentials(ETH1WithdrawalPrefix, eth1Address), []byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}) + }) + + t.Run("0x02 compounding", func(t *testing.T) { + addr := common.Address{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20} + creds := WithdrawalCredentials(CompoundingWithdrawalPrefix, addr) + + require.Len(t, creds, 32) + require.Equal(t, byte(2), creds[0], "first byte should be 0x02") + for i := 1; i < 12; i++ { + require.Equal(t, byte(0), creds[i], "padding bytes should be zero") + } + require.Equal(t, addr[:], creds[12:], "address should be in bytes 12-31") }) } func TestComputeDepositMessageSigningRoot(t *testing.T) { + // Effective 20-byte address used across signing root tests. + addr := [20]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8} + t.Run("mainnet", func(t *testing.T) { r, err := ComputeDepositMessageSigningRoot(core.MainNetwork, &phase0.DepositMessage{ PublicKey: phase0.BLSPubKey([]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}), - WithdrawalCredentials: ETH1WithdrawalCredentials([]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}), + WithdrawalCredentials: WithdrawalCredentials(ETH1WithdrawalPrefix, addr), Amount: 32000000000, }) require.NoError(t, err) @@ -36,7 +51,7 @@ func TestComputeDepositMessageSigningRoot(t *testing.T) { t.Run("holesky", func(t *testing.T) { r, err := ComputeDepositMessageSigningRoot(core.HoleskyNetwork, &phase0.DepositMessage{ PublicKey: phase0.BLSPubKey([]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}), - WithdrawalCredentials: ETH1WithdrawalCredentials([]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}), + WithdrawalCredentials: WithdrawalCredentials(ETH1WithdrawalPrefix, addr), Amount: 32000000000, }) require.NoError(t, err) @@ -45,11 +60,13 @@ func TestComputeDepositMessageSigningRoot(t *testing.T) { } func TestDepositDataRootForFork(t *testing.T) { + addr := [20]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8} + t.Run("mainnet", func(t *testing.T) { r, err := DepositDataRootForFork( phase0.Version{0, 0, 0, 0}, []byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, - []byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, + WithdrawalCredentials(ETH1WithdrawalPrefix, addr), 32000000000, ) require.NoError(t, err) @@ -60,7 +77,7 @@ func TestDepositDataRootForFork(t *testing.T) { r, err := DepositDataRootForFork( phase0.Version{0x01, 0x01, 0x70, 0x00}, []byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, - []byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, + WithdrawalCredentials(ETH1WithdrawalPrefix, addr), 32000000000, ) require.NoError(t, err) @@ -68,6 +85,110 @@ func TestDepositDataRootForFork(t *testing.T) { }) } +func TestValidateWithdrawalCredentials(t *testing.T) { + tests := []struct { + name string + input []byte + wantErr string + }{ + { + name: "valid 32-byte 0x01", + input: WithdrawalCredentials(ETH1WithdrawalPrefix, [20]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}), + }, + { + name: "valid 32-byte 0x02", + input: WithdrawalCredentials(CompoundingWithdrawalPrefix, [20]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}), + }, + { + name: "non-zero padding accepted", + input: func() []byte { + c := WithdrawalCredentials(ETH1WithdrawalPrefix, [20]byte{}) + c[5] = 0xFF + return c + }(), + }, + { + name: "32-byte 0x00 prefix rejected", + input: make([]byte, 32), + wantErr: "invalid withdrawal credential prefix: 0x00", + }, + { + name: "32-byte 0xFF prefix rejected", + input: append([]byte{0xFF}, make([]byte, 31)...), + wantErr: "invalid withdrawal credential prefix: 0xff", + }, + { + name: "wrong length 20 bytes rejected", + input: make([]byte, 20), + wantErr: "withdrawal credentials must be 32 bytes, got 20", + }, + { + name: "wrong length 15 bytes", + input: make([]byte, 15), + wantErr: "withdrawal credentials must be 32 bytes, got 15", + }, + { + name: "wrong length 0 bytes", + input: []byte{}, + wantErr: "withdrawal credentials must be 32 bytes, got 0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateWithdrawalCredentials(tt.input) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + }) + } +} + +func TestDepositDataRootForFork_0x02(t *testing.T) { + t.Run("mainnet 0x02", func(t *testing.T) { + creds := WithdrawalCredentials(CompoundingWithdrawalPrefix, [20]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}) + _, err := DepositDataRootForFork( + phase0.Version{0, 0, 0, 0}, + []byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, + creds, + 32000000000, + ) + require.NoError(t, err) + }) +} + +func TestVerifyDepositData_0x02(t *testing.T) { + InitBLS() + sk := &bls.SecretKey{} + require.NoError(t, sk.SetHexString("11e35da0958187d89cd6f7cc2b07a0a3f6225ad1e2b089d12e9b08f7f171c1c9")) + + pk := phase0.BLSPubKey{} + copy(pk[:], sk.GetPublicKey().Serialize()) + + creds := WithdrawalCredentials(CompoundingWithdrawalPrefix, [20]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}) + + r, err := ComputeDepositMessageSigningRoot(core.MainNetwork, &phase0.DepositMessage{ + PublicKey: pk, + WithdrawalCredentials: creds, + Amount: 32000000000, + }) + require.NoError(t, err) + + sig := phase0.BLSSignature{} + copy(sig[:], sk.SignByte(r[:]).Serialize()) + + depositData := &phase0.DepositData{ + PublicKey: pk, + WithdrawalCredentials: creds, + Amount: 32000000000, + Signature: sig, + } + + require.NoError(t, VerifyDepositData(core.MainNetwork, depositData)) +} + func TestVerifyDepositData(t *testing.T) { t.Run("mainnet", func(t *testing.T) { InitBLS() diff --git a/init.go b/init.go index efe7172..91ca8b0 100644 --- a/init.go +++ b/init.go @@ -19,6 +19,9 @@ func ValidateInitMessage(init *Init) error { if !ValidAmountSet(phase0.Gwei(init.Amount)) { return fmt.Errorf("amount should be in range between 32 ETH and 2048 ETH") } + if err := crypto.ValidateWithdrawalCredentials(init.WithdrawalCredentials); err != nil { + return fmt.Errorf("invalid withdrawal credentials: %w", err) + } return nil } diff --git a/reshare.go b/reshare.go index ab37b81..bbc1a71 100644 --- a/reshare.go +++ b/reshare.go @@ -6,6 +6,8 @@ import ( "sort" "github.com/attestantio/go-eth2-client/spec/phase0" + + "github.com/ssvlabs/dkg-spec/crypto" ) // ValidateReshareMessage returns nil if re-share message is valid @@ -22,6 +24,9 @@ func ValidateReshareMessage( if !bytes.Equal(reshare.Owner[:], proof.Proof.Owner[:]) { return fmt.Errorf("invalid owner address") } + if err := crypto.ValidateWithdrawalCredentials(reshare.WithdrawalCredentials); err != nil { + return fmt.Errorf("invalid withdrawal credentials: %w", err) + } if err := ValidateCeremonyProof(reshare.ValidatorPubKey, operator, *proof); err != nil { return err diff --git a/resign.go b/resign.go index f86261a..e95a876 100644 --- a/resign.go +++ b/resign.go @@ -4,6 +4,8 @@ import ( "fmt" "github.com/attestantio/go-eth2-client/spec/phase0" + + "github.com/ssvlabs/dkg-spec/crypto" ) // ValidateResignMessage returns nil if re-sign message is valid @@ -15,9 +17,11 @@ func ValidateResignMessage( if !ValidAmountSet(phase0.Gwei(resign.Amount)) { return fmt.Errorf("amount should be in range between 32 ETH and 2048 ETH") } + if err := crypto.ValidateWithdrawalCredentials(resign.WithdrawalCredentials); err != nil { + return fmt.Errorf("invalid withdrawal credentials: %w", err) + } if err := ValidateCeremonyProof(resign.ValidatorPubKey, operator, *proof); err != nil { return err } - return nil } diff --git a/result.go b/result.go index 30041b2..645264a 100644 --- a/result.go +++ b/result.go @@ -130,10 +130,13 @@ func ValidateResults( if err != nil { return nil, nil, nil, err } + if err := crypto.ValidateWithdrawalCredentials(withdrawalCredentials); err != nil { + return nil, nil, nil, err + } depositData := &phase0.DepositData{ PublicKey: phase0.BLSPubKey(validatorRecoveredPK.Serialize()), Amount: phase0.Gwei(amount), - WithdrawalCredentials: crypto.ETH1WithdrawalCredentials(withdrawalCredentials), + WithdrawalCredentials: withdrawalCredentials, Signature: phase0.BLSSignature(masterDepositSig.Serialize()), } err = crypto.VerifyDepositData(network, depositData) @@ -292,10 +295,14 @@ func VerifyPartialDepositDataSignatures( return err } + if err := crypto.ValidateWithdrawalCredentials(withdrawalCredentials); err != nil { + return err + } shareRoot, err := crypto.ComputeDepositMessageSigningRoot(network, &phase0.DepositMessage{ PublicKey: phase0.BLSPubKey(validatorPubKey), Amount: amount, - WithdrawalCredentials: crypto.ETH1WithdrawalCredentials(withdrawalCredentials)}) + WithdrawalCredentials: withdrawalCredentials, + }) if err != nil { return fmt.Errorf("failed to compute deposit data root: %w", err) } diff --git a/testing/fixtures/common.go b/testing/fixtures/common.go index 803ddfe..4d85094 100644 --- a/testing/fixtures/common.go +++ b/testing/fixtures/common.go @@ -14,13 +14,14 @@ import ( ) var ( - TestWithdrawalCred = make([]byte, 40) - TestFork = [4]byte{0, 0, 0, 0} - TestNonce = uint64(0) - TestOwnerAddress = common.Address{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20} - TestOwnerNewAddress = common.Address{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 21} - TestRequestID = [24]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24} - TestAmount = crypto.MIN_ACTIVATION_BALANCE + TestWithdrawalCred = crypto.WithdrawalCredentials(crypto.ETH1WithdrawalPrefix, [20]byte{}) + TestWithdrawalCred0x02 = crypto.WithdrawalCredentials(crypto.CompoundingWithdrawalPrefix, TestOwnerAddress) + TestFork = [4]byte{0, 0, 0, 0} + TestNonce = uint64(0) + TestOwnerAddress = common.Address{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20} + TestOwnerNewAddress = common.Address{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 21} + TestRequestID = [24]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24} + TestAmount = crypto.MIN_ACTIVATION_BALANCE ) func GenerateOperators(amount int) []*spec.Operator { diff --git a/testing/fixtures/common_test.go b/testing/fixtures/common_test.go index 83f7433..110ed69 100644 --- a/testing/fixtures/common_test.go +++ b/testing/fixtures/common_test.go @@ -122,7 +122,7 @@ func TestSignDeposit(t *testing.T) { shareRoot, err := crypto.ComputeDepositMessageSigningRoot(network, &phase0.DepositMessage{ PublicKey: phase0.BLSPubKey(ShareSK(TestValidator13Operators).GetPublicKey().Serialize()), Amount: crypto.MIN_ACTIVATION_BALANCE, - WithdrawalCredentials: crypto.ETH1WithdrawalCredentials(TestWithdrawalCred)}) + WithdrawalCredentials: TestWithdrawalCred}) require.NoError(t, err) sig := ShareSK(TestValidator13OperatorsShare1).SignByte(shareRoot[:]) diff --git a/testing/fixtures/reshare.go b/testing/fixtures/reshare.go index 4e82ae4..646c702 100644 --- a/testing/fixtures/reshare.go +++ b/testing/fixtures/reshare.go @@ -7,8 +7,9 @@ import ( var ( TestReshare4Operators = spec.Reshare{ - ValidatorPubKey: ShareSK(TestValidator4Operators).GetPublicKey().Serialize(), - OldOperators: GenerateOperators(4), + ValidatorPubKey: ShareSK(TestValidator4Operators).GetPublicKey().Serialize(), + WithdrawalCredentials: TestWithdrawalCred, + OldOperators: GenerateOperators(4), NewOperators: []*spec.Operator{ GenerateOperators(4)[0], GenerateOperators(4)[1], @@ -22,8 +23,9 @@ var ( Amount: uint64(crypto.MIN_ACTIVATION_BALANCE), } TestReshare7Operators = spec.Reshare{ - ValidatorPubKey: ShareSK(TestValidator7Operators).GetPublicKey().Serialize(), - OldOperators: GenerateOperators(7), + ValidatorPubKey: ShareSK(TestValidator7Operators).GetPublicKey().Serialize(), + WithdrawalCredentials: TestWithdrawalCred, + OldOperators: GenerateOperators(7), NewOperators: []*spec.Operator{ GenerateOperators(7)[0], GenerateOperators(7)[1], @@ -40,8 +42,9 @@ var ( Amount: uint64(crypto.MIN_ACTIVATION_BALANCE), } TestReshare10Operators = spec.Reshare{ - ValidatorPubKey: ShareSK(TestValidator10Operators).GetPublicKey().Serialize(), - OldOperators: GenerateOperators(10), + ValidatorPubKey: ShareSK(TestValidator10Operators).GetPublicKey().Serialize(), + WithdrawalCredentials: TestWithdrawalCred, + OldOperators: GenerateOperators(10), NewOperators: []*spec.Operator{ GenerateOperators(10)[0], GenerateOperators(10)[1], @@ -61,8 +64,9 @@ var ( Amount: uint64(crypto.MIN_ACTIVATION_BALANCE), } TestReshare13Operators = spec.Reshare{ - ValidatorPubKey: ShareSK(TestValidator13Operators).GetPublicKey().Serialize(), - OldOperators: GenerateOperators(13), + ValidatorPubKey: ShareSK(TestValidator13Operators).GetPublicKey().Serialize(), + WithdrawalCredentials: TestWithdrawalCred, + OldOperators: GenerateOperators(13), NewOperators: []*spec.Operator{ GenerateOperators(13)[0], GenerateOperators(13)[1], diff --git a/testing/fixtures/resign.go b/testing/fixtures/resign.go index 20bcc81..c8c2873 100644 --- a/testing/fixtures/resign.go +++ b/testing/fixtures/resign.go @@ -7,27 +7,31 @@ import ( var ( TestResign4Operators = spec.Resign{ - ValidatorPubKey: ShareSK(TestValidator4Operators).GetPublicKey().Serialize(), - Owner: TestOwnerAddress, - Nonce: 1, - Amount: uint64(crypto.MIN_ACTIVATION_BALANCE), + ValidatorPubKey: ShareSK(TestValidator4Operators).GetPublicKey().Serialize(), + WithdrawalCredentials: TestWithdrawalCred, + Owner: TestOwnerAddress, + Nonce: 1, + Amount: uint64(crypto.MIN_ACTIVATION_BALANCE), } TestResign7Operators = spec.Resign{ - ValidatorPubKey: ShareSK(TestValidator7Operators).GetPublicKey().Serialize(), - Owner: TestOwnerAddress, - Nonce: 1, - Amount: uint64(crypto.MIN_ACTIVATION_BALANCE), + ValidatorPubKey: ShareSK(TestValidator7Operators).GetPublicKey().Serialize(), + WithdrawalCredentials: TestWithdrawalCred, + Owner: TestOwnerAddress, + Nonce: 1, + Amount: uint64(crypto.MIN_ACTIVATION_BALANCE), } TestResign10Operators = spec.Resign{ - ValidatorPubKey: ShareSK(TestValidator10Operators).GetPublicKey().Serialize(), - Owner: TestOwnerAddress, - Nonce: 1, - Amount: uint64(crypto.MIN_ACTIVATION_BALANCE), + ValidatorPubKey: ShareSK(TestValidator10Operators).GetPublicKey().Serialize(), + WithdrawalCredentials: TestWithdrawalCred, + Owner: TestOwnerAddress, + Nonce: 1, + Amount: uint64(crypto.MIN_ACTIVATION_BALANCE), } TestResign13Operators = spec.Resign{ - ValidatorPubKey: ShareSK(TestValidator13Operators).GetPublicKey().Serialize(), - Owner: TestOwnerAddress, - Nonce: 1, - Amount: uint64(crypto.MIN_ACTIVATION_BALANCE), + ValidatorPubKey: ShareSK(TestValidator13Operators).GetPublicKey().Serialize(), + WithdrawalCredentials: TestWithdrawalCred, + Owner: TestOwnerAddress, + Nonce: 1, + Amount: uint64(crypto.MIN_ACTIVATION_BALANCE), } ) diff --git a/testing/init_test.go b/testing/init_test.go index 18fdd13..835cf8f 100644 --- a/testing/init_test.go +++ b/testing/init_test.go @@ -153,6 +153,41 @@ func TestValidateInitMessage(t *testing.T) { Amount: uint64(crypto.MIN_ACTIVATION_BALANCE), }), "threshold set is invalid") }) + t.Run("valid 32-byte 0x02 credentials", func(t *testing.T) { + require.NoError(t, spec.ValidateInitMessage(&spec.Init{ + Operators: fixtures.GenerateOperators(4), + T: 3, + WithdrawalCredentials: fixtures.TestWithdrawalCred0x02, + Fork: fixtures.TestFork, + Owner: fixtures.TestOwnerAddress, + Nonce: 0, + Amount: uint64(crypto.MIN_ACTIVATION_BALANCE), + })) + }) + t.Run("invalid 32-byte 0x03 prefix", func(t *testing.T) { + badCreds := make([]byte, 32) + badCreds[0] = 0x03 + require.ErrorContains(t, spec.ValidateInitMessage(&spec.Init{ + Operators: fixtures.GenerateOperators(4), + T: 3, + WithdrawalCredentials: badCreds, + Fork: fixtures.TestFork, + Owner: fixtures.TestOwnerAddress, + Nonce: 0, + Amount: uint64(crypto.MIN_ACTIVATION_BALANCE), + }), "invalid withdrawal credentials") + }) + t.Run("invalid 15-byte credentials", func(t *testing.T) { + require.ErrorContains(t, spec.ValidateInitMessage(&spec.Init{ + Operators: fixtures.GenerateOperators(4), + T: 3, + WithdrawalCredentials: make([]byte, 15), + Fork: fixtures.TestFork, + Owner: fixtures.TestOwnerAddress, + Nonce: 0, + Amount: uint64(crypto.MIN_ACTIVATION_BALANCE), + }), "invalid withdrawal credentials") + }) t.Run("amount < 32 ETH", func(t *testing.T) { require.EqualError(t, spec.ValidateInitMessage(&spec.Init{ Operators: fixtures.GenerateOperators(4), diff --git a/testing/reshare_test.go b/testing/reshare_test.go index cb2fd98..53d772b 100644 --- a/testing/reshare_test.go +++ b/testing/reshare_test.go @@ -228,8 +228,9 @@ func TestValidateReshare(t *testing.T) { t.Run("reshare 4->7 operators", func(t *testing.T) { require.NoError(t, spec.ValidateReshareMessage( &spec.Reshare{ - ValidatorPubKey: fixtures.ShareSK(fixtures.TestValidator4Operators).GetPublicKey().Serialize(), - OldOperators: fixtures.GenerateOperators(4), + ValidatorPubKey: fixtures.ShareSK(fixtures.TestValidator4Operators).GetPublicKey().Serialize(), + WithdrawalCredentials: fixtures.TestWithdrawalCred, + OldOperators: fixtures.GenerateOperators(4), NewOperators: []*spec.Operator{ fixtures.GenerateOperators(7)[0], fixtures.GenerateOperators(7)[1], @@ -253,14 +254,15 @@ func TestValidateReshare(t *testing.T) { t.Run("reshare 7->4 operators", func(t *testing.T) { require.NoError(t, spec.ValidateReshareMessage( &spec.Reshare{ - ValidatorPubKey: fixtures.ShareSK(fixtures.TestValidator7Operators).GetPublicKey().Serialize(), - OldOperators: fixtures.GenerateOperators(7), - NewOperators: fixtures.GenerateOperators(4), - OldT: 5, - NewT: 3, - Owner: fixtures.TestOwnerAddress, - Nonce: 1, - Amount: uint64(crypto.MIN_ACTIVATION_BALANCE), + ValidatorPubKey: fixtures.ShareSK(fixtures.TestValidator7Operators).GetPublicKey().Serialize(), + WithdrawalCredentials: fixtures.TestWithdrawalCred, + OldOperators: fixtures.GenerateOperators(7), + NewOperators: fixtures.GenerateOperators(4), + OldT: 5, + NewT: 3, + Owner: fixtures.TestOwnerAddress, + Nonce: 1, + Amount: uint64(crypto.MIN_ACTIVATION_BALANCE), }, fixtures.GenerateOperators(7)[0], &fixtures.TestOperator1Proof7Operators, @@ -323,7 +325,8 @@ func TestValidateReshare(t *testing.T) { t.Run("invalid proof", func(t *testing.T) { require.EqualError(t, spec.ValidateReshareMessage( &spec.Reshare{ - ValidatorPubKey: fixtures.ShareSK(fixtures.TestValidator4Operators).GetPublicKey().Serialize(), + ValidatorPubKey: fixtures.ShareSK(fixtures.TestValidator4Operators).GetPublicKey().Serialize(), + WithdrawalCredentials: fixtures.TestWithdrawalCred, OldOperators: []*spec.Operator{ fixtures.GenerateOperators(4)[0], fixtures.GenerateOperators(4)[1], @@ -350,7 +353,8 @@ func TestValidateReshare(t *testing.T) { t.Run("new operators not unique", func(t *testing.T) { require.EqualError(t, spec.ValidateReshareMessage( &spec.Reshare{ - ValidatorPubKey: fixtures.ShareSK(fixtures.TestValidator4Operators).GetPublicKey().Serialize(), + ValidatorPubKey: fixtures.ShareSK(fixtures.TestValidator4Operators).GetPublicKey().Serialize(), + WithdrawalCredentials: fixtures.TestWithdrawalCred, OldOperators: []*spec.Operator{ fixtures.GenerateOperators(4)[0], fixtures.GenerateOperators(4)[1], @@ -377,7 +381,8 @@ func TestValidateReshare(t *testing.T) { t.Run("new operators same as old", func(t *testing.T) { require.EqualError(t, spec.ValidateReshareMessage( &spec.Reshare{ - ValidatorPubKey: fixtures.ShareSK(fixtures.TestValidator4Operators).GetPublicKey().Serialize(), + ValidatorPubKey: fixtures.ShareSK(fixtures.TestValidator4Operators).GetPublicKey().Serialize(), + WithdrawalCredentials: fixtures.TestWithdrawalCred, OldOperators: []*spec.Operator{ fixtures.GenerateOperators(4)[0], fixtures.GenerateOperators(4)[1], @@ -404,7 +409,8 @@ func TestValidateReshare(t *testing.T) { t.Run("invalid old threshold", func(t *testing.T) { require.EqualError(t, spec.ValidateReshareMessage( &spec.Reshare{ - ValidatorPubKey: fixtures.ShareSK(fixtures.TestValidator4Operators).GetPublicKey().Serialize(), + ValidatorPubKey: fixtures.ShareSK(fixtures.TestValidator4Operators).GetPublicKey().Serialize(), + WithdrawalCredentials: fixtures.TestWithdrawalCred, OldOperators: []*spec.Operator{ fixtures.GenerateOperators(4)[0], fixtures.GenerateOperators(4)[1], @@ -431,7 +437,8 @@ func TestValidateReshare(t *testing.T) { t.Run("invalid new threshold", func(t *testing.T) { require.EqualError(t, spec.ValidateReshareMessage( &spec.Reshare{ - ValidatorPubKey: fixtures.ShareSK(fixtures.TestValidator4Operators).GetPublicKey().Serialize(), + ValidatorPubKey: fixtures.ShareSK(fixtures.TestValidator4Operators).GetPublicKey().Serialize(), + WithdrawalCredentials: fixtures.TestWithdrawalCred, OldOperators: []*spec.Operator{ fixtures.GenerateOperators(4)[0], fixtures.GenerateOperators(4)[1], @@ -457,8 +464,9 @@ func TestValidateReshare(t *testing.T) { t.Run("valid amount 32 ETH", func(t *testing.T) { require.NoError(t, spec.ValidateReshareMessage( &spec.Reshare{ - ValidatorPubKey: fixtures.ShareSK(fixtures.TestValidator4Operators).GetPublicKey().Serialize(), - OldOperators: fixtures.GenerateOperators(4), + ValidatorPubKey: fixtures.ShareSK(fixtures.TestValidator4Operators).GetPublicKey().Serialize(), + WithdrawalCredentials: fixtures.TestWithdrawalCred, + OldOperators: fixtures.GenerateOperators(4), NewOperators: []*spec.Operator{ fixtures.GenerateOperators(7)[0], fixtures.GenerateOperators(7)[1], @@ -481,8 +489,9 @@ func TestValidateReshare(t *testing.T) { t.Run("valid amount 2048 ETH", func(t *testing.T) { require.NoError(t, spec.ValidateReshareMessage( &spec.Reshare{ - ValidatorPubKey: fixtures.ShareSK(fixtures.TestValidator4Operators).GetPublicKey().Serialize(), - OldOperators: fixtures.GenerateOperators(4), + ValidatorPubKey: fixtures.ShareSK(fixtures.TestValidator4Operators).GetPublicKey().Serialize(), + WithdrawalCredentials: fixtures.TestWithdrawalCred, + OldOperators: fixtures.GenerateOperators(4), NewOperators: []*spec.Operator{ fixtures.GenerateOperators(7)[0], fixtures.GenerateOperators(7)[1], @@ -505,8 +514,9 @@ func TestValidateReshare(t *testing.T) { t.Run("amount < 32 ETH", func(t *testing.T) { require.EqualError(t, spec.ValidateReshareMessage( &spec.Reshare{ - ValidatorPubKey: fixtures.ShareSK(fixtures.TestValidator4Operators).GetPublicKey().Serialize(), - OldOperators: fixtures.GenerateOperators(4), + ValidatorPubKey: fixtures.ShareSK(fixtures.TestValidator4Operators).GetPublicKey().Serialize(), + WithdrawalCredentials: fixtures.TestWithdrawalCred, + OldOperators: fixtures.GenerateOperators(4), NewOperators: []*spec.Operator{ fixtures.GenerateOperators(7)[0], fixtures.GenerateOperators(7)[1], @@ -529,8 +539,9 @@ func TestValidateReshare(t *testing.T) { t.Run("amount > 2048 ETH", func(t *testing.T) { require.EqualError(t, spec.ValidateReshareMessage( &spec.Reshare{ - ValidatorPubKey: fixtures.ShareSK(fixtures.TestValidator4Operators).GetPublicKey().Serialize(), - OldOperators: fixtures.GenerateOperators(4), + ValidatorPubKey: fixtures.ShareSK(fixtures.TestValidator4Operators).GetPublicKey().Serialize(), + WithdrawalCredentials: fixtures.TestWithdrawalCred, + OldOperators: fixtures.GenerateOperators(4), NewOperators: []*spec.Operator{ fixtures.GenerateOperators(7)[0], fixtures.GenerateOperators(7)[1], diff --git a/testing/resign_test.go b/testing/resign_test.go index 2e87a8e..ae9d768 100644 --- a/testing/resign_test.go +++ b/testing/resign_test.go @@ -241,10 +241,11 @@ func TestValidateResign(t *testing.T) { t.Run("invalid proof", func(t *testing.T) { require.EqualError(t, spec.ValidateResignMessage( &spec.Resign{ - ValidatorPubKey: fixtures.ShareSK(fixtures.TestValidator4Operators).GetPublicKey().Serialize(), - Owner: fixtures.TestOwnerAddress, - Nonce: 1, - Amount: uint64(crypto.MIN_ACTIVATION_BALANCE), + ValidatorPubKey: fixtures.ShareSK(fixtures.TestValidator4Operators).GetPublicKey().Serialize(), + WithdrawalCredentials: fixtures.TestWithdrawalCred, + Owner: fixtures.TestOwnerAddress, + Nonce: 1, + Amount: uint64(crypto.MIN_ACTIVATION_BALANCE), }, fixtures.GenerateOperators(4)[0], &fixtures.TestOperator2Proof4Operators, diff --git a/testing/result_test.go b/testing/result_test.go index 7b4fea9..e85fa6a 100644 --- a/testing/result_test.go +++ b/testing/result_test.go @@ -329,6 +329,75 @@ func TestValidateResults(t *testing.T) { }) } +func TestBuildAndValidateResults_0x02(t *testing.T) { + type operatorSet struct { + name string + count int + valSK string + shareSKs []string + operators func() []*spec.Operator + } + sets := []operatorSet{ + { + name: "4 operators", + count: 4, + valSK: fixtures.TestValidator4Operators, + shareSKs: []string{ + fixtures.TestValidator4OperatorsShare1, + fixtures.TestValidator4OperatorsShare2, + fixtures.TestValidator4OperatorsShare3, + fixtures.TestValidator4OperatorsShare4, + }, + operators: func() []*spec.Operator { return fixtures.GenerateOperators(4) }, + }, + } + + operatorSKs := []string{ + fixtures.TestOperator1SK, + fixtures.TestOperator2SK, + fixtures.TestOperator3SK, + fixtures.TestOperator4SK, + } + + for _, s := range sets { + t.Run(s.name, func(t *testing.T) { + valPK := fixtures.ShareSK(s.valSK).GetPublicKey().Serialize() + ops := s.operators() + + results := make([]*spec.Result, s.count) + for i := 0; i < s.count; i++ { + var err error + results[i], err = spec.BuildResult( + ops[i].ID, + fixtures.TestRequestID, + fixtures.ShareSK(s.shareSKs[i]), + fixtures.OperatorSK(operatorSKs[i]), + valPK, + fixtures.TestOwnerAddress, + fixtures.TestWithdrawalCred0x02, + fixtures.TestFork, + fixtures.TestNonce, + fixtures.TestAmount, + ) + require.NoError(t, err) + } + + _, _, _, err := spec.ValidateResults( + ops, + fixtures.TestWithdrawalCred0x02, + valPK, + fixtures.TestFork, + fixtures.TestOwnerAddress, + fixtures.TestNonce, + fixtures.TestAmount, + fixtures.TestRequestID, + results, + ) + require.NoError(t, err) + }) + } +} + func TestValidateResult(t *testing.T) { t.Run("valid 4 operators", func(t *testing.T) { require.NoError(t, spec.ValidateResult(