diff --git a/app/obolapi/exit_model_test.go b/app/obolapi/exit_model_test.go index c8eaea1df..e2ce32b9e 100644 --- a/app/obolapi/exit_model_test.go +++ b/app/obolapi/exit_model_test.go @@ -3,8 +3,10 @@ package obolapi_test import ( + "encoding/hex" "testing" + eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" ssz "github.com/ferranbt/fastssz" "github.com/stretchr/testify/require" @@ -12,7 +14,7 @@ import ( "github.com/obolnetwork/charon/testutil" ) -func Test_PartialExitRequest(t *testing.T) { +func TestPartialExitRequest(t *testing.T) { pr := obolapi.PartialExitRequest{ UnsignedPartialExitRequest: obolapi.UnsignedPartialExitRequest{ PartialExits: []obolapi.ExitBlob{ @@ -48,7 +50,7 @@ func Test_PartialExitRequest(t *testing.T) { require.NotEmpty(t, htr) } -func Test_UnsignedPartialExitRequest(t *testing.T) { +func TestUnsignedPartialExitRequest(t *testing.T) { pr := obolapi.UnsignedPartialExitRequest{ PartialExits: []obolapi.ExitBlob{ { @@ -72,7 +74,7 @@ func Test_UnsignedPartialExitRequest(t *testing.T) { require.NotEmpty(t, htr) } -func Test_PartialExits(t *testing.T) { +func TestPartialExits(t *testing.T) { pr := obolapi.PartialExits{ { PublicKey: string(testutil.RandomCorePubKey(t)), @@ -97,7 +99,7 @@ func Test_PartialExits(t *testing.T) { require.NotEmpty(t, htr) } -func Test_FullExitAuthBlob(t *testing.T) { +func TestFullExitAuthBlob(t *testing.T) { vp, err := testutil.RandomCorePubKey(t).Bytes() require.NoError(t, err) @@ -120,7 +122,7 @@ func Test_FullExitAuthBlob(t *testing.T) { require.NotEmpty(t, htr) } -func Test_ExitBlob(t *testing.T) { +func TestExitBlob(t *testing.T) { pr := obolapi.ExitBlob{ PublicKey: string(testutil.RandomCorePubKey(t)), SignedExitMessage: *testutil.RandomExit(), @@ -138,3 +140,201 @@ func Test_ExitBlob(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, htr) } + +func TestExitBlobHashTreeRoot(t *testing.T) { + zeroPubkey := "0x" + hex.EncodeToString(make([]byte, 48)) + + nonZeroPubkey := make([]byte, 48) + nonZeroPubkey[0] = 0xab + nonZeroPubkeyHex := "0x" + hex.EncodeToString(nonZeroPubkey) + + nonZeroSig := eth2p0.BLSSignature{} + nonZeroSig[0] = 0x01 + + tests := []struct { + name string + input obolapi.ExitBlob + expected string + }{ + { + name: "zeros", + input: obolapi.ExitBlob{ + PublicKey: zeroPubkey, + SignedExitMessage: eth2p0.SignedVoluntaryExit{ + Message: ð2p0.VoluntaryExit{}, + Signature: eth2p0.BLSSignature{}, + }, + }, + expected: "65595314b41aeacd2f0469c979f93cdff0aeb5cfa1c290d2a4084cbc9855de48", + }, + { + name: "epoch1_validator3", + input: obolapi.ExitBlob{ + PublicKey: nonZeroPubkeyHex, + SignedExitMessage: eth2p0.SignedVoluntaryExit{ + Message: ð2p0.VoluntaryExit{Epoch: 1, ValidatorIndex: 3}, + Signature: nonZeroSig, + }, + }, + expected: "004ff00a261f09275d978c524ee23ad02629eb2fc94d16d3a3cab9035247d28c", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.input.HashTreeRoot() + require.NoError(t, err) + require.Equal(t, tt.expected, hex.EncodeToString(got[:])) + }) + } +} + +func TestPartialExitsHashTreeRoot(t *testing.T) { + zeroPubkey := "0x" + hex.EncodeToString(make([]byte, 48)) + + nonZeroPubkey := make([]byte, 48) + nonZeroPubkey[0] = 0xab + nonZeroSig := eth2p0.BLSSignature{} + nonZeroSig[0] = 0x01 + + exitZero := obolapi.ExitBlob{ + PublicKey: zeroPubkey, + SignedExitMessage: eth2p0.SignedVoluntaryExit{ + Message: ð2p0.VoluntaryExit{}, + Signature: eth2p0.BLSSignature{}, + }, + } + exitNonZero := obolapi.ExitBlob{ + PublicKey: "0x" + hex.EncodeToString(nonZeroPubkey), + SignedExitMessage: eth2p0.SignedVoluntaryExit{ + Message: ð2p0.VoluntaryExit{Epoch: 1, ValidatorIndex: 3}, + Signature: nonZeroSig, + }, + } + + tests := []struct { + name string + input obolapi.PartialExits + expected string + }{ + { + name: "empty", + input: obolapi.PartialExits{}, + expected: "6080a24df6cb76f31cacdf4419ac9bf0ac092087f40ec93f10c4608f967ca23a", + }, + { + name: "one_exit", + input: obolapi.PartialExits{exitZero}, + expected: "243b6b384bcf8dc6e72369247c6b0fa784c98077c15b402a9e4ea3335e479a12", + }, + { + name: "two_exits", + input: obolapi.PartialExits{exitZero, exitNonZero}, + expected: "7e7ee8b65d37f3e5bdd859c150c8bc7f329c1c873cb2042636c6f25f730af037", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.input.HashTreeRoot() + require.NoError(t, err) + require.Equal(t, tt.expected, hex.EncodeToString(got[:])) + }) + } +} + +func TestUnsignedPartialExitRequestHashTreeRoot(t *testing.T) { + zeroPubkey := "0x" + hex.EncodeToString(make([]byte, 48)) + + nonZeroPubkey := make([]byte, 48) + nonZeroPubkey[0] = 0xab + nonZeroSig := eth2p0.BLSSignature{} + nonZeroSig[0] = 0x01 + + exitZero := obolapi.ExitBlob{ + PublicKey: zeroPubkey, + SignedExitMessage: eth2p0.SignedVoluntaryExit{ + Message: ð2p0.VoluntaryExit{}, + Signature: eth2p0.BLSSignature{}, + }, + } + exitNonZero := obolapi.ExitBlob{ + PublicKey: "0x" + hex.EncodeToString(nonZeroPubkey), + SignedExitMessage: eth2p0.SignedVoluntaryExit{ + Message: ð2p0.VoluntaryExit{Epoch: 1, ValidatorIndex: 3}, + Signature: nonZeroSig, + }, + } + + tests := []struct { + name string + input obolapi.UnsignedPartialExitRequest + expected string + }{ + { + name: "empty_share0", + input: obolapi.UnsignedPartialExitRequest{PartialExits: obolapi.PartialExits{}, ShareIdx: 0}, + expected: "b0dd7cbd107e45dbfa315f1d5da8ff11d50dcffbe38a4d2473583437ab18a408", + }, + { + name: "one_exit_share2", + input: obolapi.UnsignedPartialExitRequest{PartialExits: obolapi.PartialExits{exitZero}, ShareIdx: 2}, + expected: "821eb5ff317540ae8e3e3c819b879a90bf42ad11be959f60c670f44f88cfa6a2", + }, + { + name: "two_exits_share1", + input: obolapi.UnsignedPartialExitRequest{PartialExits: obolapi.PartialExits{exitZero, exitNonZero}, ShareIdx: 1}, + expected: "44a05ee801102e3b179cf2f70163fd2e2ede5bb53545336cf5912438b9e80125", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.input.HashTreeRoot() + require.NoError(t, err) + require.Equal(t, tt.expected, hex.EncodeToString(got[:])) + }) + } +} + +func TestFullExitAuthBlobHashTreeRoot(t *testing.T) { + nonZeroLockHash := make([]byte, 32) + nonZeroLockHash[0] = 0xde + nonZeroLockHash[31] = 0xad + + nonZeroValidatorPubkey := make([]byte, 48) + nonZeroValidatorPubkey[0] = 0x11 + + tests := []struct { + name string + input obolapi.FullExitAuthBlob + expected string + }{ + { + name: "zeros", + input: obolapi.FullExitAuthBlob{ + LockHash: make([]byte, 32), + ValidatorPubkey: make([]byte, 48), + ShareIndex: 0, + }, + expected: "bad1ebffe915f474f39873c538915f5cb1b246dfc5dc98eed668aac9292f1351", + }, + { + name: "non_zero", + input: obolapi.FullExitAuthBlob{ + LockHash: nonZeroLockHash, + ValidatorPubkey: nonZeroValidatorPubkey, + ShareIndex: 7, + }, + expected: "876b5e058873adfcd3cdadf87332d0fd2e21311e35df96b2c42529ae313b9a5b", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.input.HashTreeRoot() + require.NoError(t, err) + require.Equal(t, tt.expected, hex.EncodeToString(got[:])) + }) + } +} diff --git a/cluster/helpers_internal_test.go b/cluster/helpers_internal_test.go index d03d10718..e2125b2a0 100644 --- a/cluster/helpers_internal_test.go +++ b/cluster/helpers_internal_test.go @@ -5,6 +5,7 @@ package cluster import ( "context" crand "crypto/rand" + "encoding/hex" "fmt" "math/rand" "net/http" @@ -15,6 +16,7 @@ import ( "testing" k1 "github.com/decred/dcrd/dcrec/secp256k1/v4" + ssz "github.com/ferranbt/fastssz" "github.com/google/uuid" "github.com/stretchr/testify/require" @@ -23,6 +25,229 @@ import ( "github.com/obolnetwork/charon/testutil" ) +// hashRootOf calls fn on a fresh ssz.HashWalker and returns the HashRoot as a hex string. +func hashRootOf(t *testing.T, fn func(ssz.HashWalker) error) string { + t.Helper() + + hh := ssz.DefaultHasherPool.Get() + defer ssz.DefaultHasherPool.Put(hh) + + require.NoError(t, fn(hh)) + + h, err := hh.HashRoot() + require.NoError(t, err) + + return hex.EncodeToString(h[:]) +} + +// hashRootWithPrefix simulates the real usage pattern: open an index, call setup to +// write a prior field, then call fn to append the field under test, merkleize, and +// return the HashRoot. This mirrors how these helpers are called inside hashDefinition*. +func hashRootWithPrefix(t *testing.T, setup func(ssz.HashWalker), fn func(ssz.HashWalker) error) string { + t.Helper() + + hh := ssz.DefaultHasherPool.Get() + defer ssz.DefaultHasherPool.Put(hh) + + indx := hh.Index() + setup(hh) + require.NoError(t, fn(hh)) + hh.Merkleize(indx) + + h, err := hh.HashRoot() + require.NoError(t, err) + + return hex.EncodeToString(h[:]) +} + +func TestPutByteList(t *testing.T) { + tests := []struct { + name string + setup func(ssz.HashWalker) // nil = empty walker, non-nil = pre-populated + b []byte + limit int + expected string + }{ + { + name: "empty_limit64", + b: []byte{}, + limit: 64, + expected: "7a0501f5957bdf9cb3a8ff4966f02265f968658b7a9c62642cba1165e86642f5", + }, + { + name: "hello_limit256", + b: []byte("hello"), + limit: 256, + expected: "d714c994fb91ed0c822936ddab0934529ab7816e60dc94027e77a4188e2e4459", + }, + { + name: "32bytes_limit64", + b: make([]byte, 32), + limit: 64, + expected: "e36306f41e65a19bc26226df4c969ef1ae6ac2e29edf4038761d553854385723", + }, + { + name: "all0xff_limit32", + b: []byte{0xff, 0xff, 0xff, 0xff}, + limit: 32, + expected: "a729ed14d00c6e5f58c31993077a42444a53cc9bed4cbf64e43afd4c29a38880", + }, + { + name: "after_uint64_42", + setup: func(hh ssz.HashWalker) { hh.PutUint64(42) }, + b: []byte("hello"), + limit: 256, + expected: "bbc79cd2fdb2cc4810be1c265d80e601221f69df244a956b222db5454dd7d439", + }, + { + name: "after_bytes4", + setup: func(hh ssz.HashWalker) { hh.PutBytes([]byte{0x01, 0x02, 0x03, 0x04}) }, + b: []byte{0xff}, + limit: 32, + expected: "e27cef6249214493f3e7ade1aa0af8130029eedfc7c17d928ed8af341a5511e9", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fn := func(hh ssz.HashWalker) error { return putByteList(hh, tt.b, tt.limit, "field") } + + var got string + if tt.setup != nil { + got = hashRootWithPrefix(t, tt.setup, fn) + } else { + got = hashRootOf(t, fn) + } + + require.Equal(t, tt.expected, got) + }) + } +} + +func TestPutBytesN(t *testing.T) { + tests := []struct { + name string + setup func(ssz.HashWalker) // nil = empty walker, non-nil = pre-populated + b []byte + n int + expected string + }{ + { + name: "nil_n32", + b: nil, + n: 32, + expected: "0000000000000000000000000000000000000000000000000000000000000000", + }, + { + name: "nil_n20", + b: nil, + n: 20, + expected: "0000000000000000000000000000000000000000000000000000000000000000", + }, + { + name: "4bytes_n4", + b: []byte{0x01, 0x02, 0x03, 0x04}, + n: 4, + expected: "0102030400000000000000000000000000000000000000000000000000000000", + }, + { + name: "short_n32", + b: []byte{0xab, 0xcd}, + n: 32, + expected: "000000000000000000000000000000000000000000000000000000000000abcd", + }, + { + name: "full_n4", + b: []byte{0xde, 0xad, 0xbe, 0xef}, + n: 4, + expected: "deadbeef00000000000000000000000000000000000000000000000000000000", + }, + { + name: "after_uint64_1", + setup: func(hh ssz.HashWalker) { hh.PutUint64(1) }, + b: []byte{0xab, 0xcd}, + n: 32, + expected: "392b769bc14dd0593bbb9d28f2fa7c4ec6eb113ef750db4046b7136c5844c95d", + }, + { + name: "after_bytes20", + setup: func(hh ssz.HashWalker) { hh.PutBytes(make([]byte, 20)) }, + b: []byte{0x01, 0x02, 0x03, 0x04}, + n: 4, + expected: "c092c674a087720f7136046e41d587e355a5f359b3940336f8e4e9dde2ffe236", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fn := func(hh ssz.HashWalker) error { return putBytesN(hh, tt.b, tt.n) } + + var got string + if tt.setup != nil { + got = hashRootWithPrefix(t, tt.setup, fn) + } else { + got = hashRootOf(t, fn) + } + + require.Equal(t, tt.expected, got) + }) + } +} + +func TestPutHexBytes20(t *testing.T) { + tests := []struct { + name string + setup func(ssz.HashWalker) // nil = empty walker, non-nil = pre-populated + addr string + expected string + }{ + { + name: "zero_addr", + addr: "0x0000000000000000000000000000000000000000", + expected: "0000000000000000000000000000000000000000000000000000000000000000", + }, + { + name: "all_ones", + addr: "0x1111111111111111111111111111111111111111", + expected: "1111111111111111111111111111111111111111000000000000000000000000", + }, + { + name: "mixed", + addr: "0xabcdef0123456789abcdef0123456789abcdef01", + expected: "abcdef0123456789abcdef0123456789abcdef01000000000000000000000000", + }, + { + name: "after_uint64_99", + setup: func(hh ssz.HashWalker) { hh.PutUint64(99) }, + addr: "0x1111111111111111111111111111111111111111", + expected: "2051384e282f804390dac5c19fcfd1e4cacca29abacb0f07cd6c17995388f5b0", + }, + { + name: "after_bytelist", + setup: func(hh ssz.HashWalker) { + _ = putByteList(hh, []byte("prefix"), 256, "f") + }, + addr: "0xabcdef0123456789abcdef0123456789abcdef01", + expected: "c3105b794bd2b28cc863326f5ffa8baad9e1c1d7d424556e188961ca59f69100", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fn := func(hh ssz.HashWalker) error { return putHexBytes20(hh, tt.addr) } + + var got string + if tt.setup != nil { + got = hashRootWithPrefix(t, tt.setup, fn) + } else { + got = hashRootOf(t, fn) + } + + require.Equal(t, tt.expected, got) + }) + } +} + func TestLeftPad(t *testing.T) { b := []byte{0x01, 0x02} require.Equal(t, []byte{0x01, 0x02}, leftPad(b, 1)) diff --git a/cluster/ssz_internal_test.go b/cluster/ssz_internal_test.go index ea5062267..9820cac3d 100644 --- a/cluster/ssz_internal_test.go +++ b/cluster/ssz_internal_test.go @@ -3,16 +3,257 @@ package cluster import ( + "encoding/hex" "testing" eth2v1 "github.com/attestantio/go-eth2-client/api/v1" "github.com/attestantio/go-eth2-client/spec/bellatrix" + eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" ssz "github.com/ferranbt/fastssz" "github.com/stretchr/testify/require" "github.com/obolnetwork/charon/core" ) +const ( + testAddr1 = "0x1111111111111111111111111111111111111111" + testAddr2 = "0x2222222222222222222222222222222222222222" + testAddr3 = "0x3333333333333333333333333333333333333333" + testFee = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + testWithd = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +) + +// baseDef returns a realistic 3-operator, 2-validator Definition for the given version. +// All validators share the same fee recipient and withdrawal address so that +// LegacyValidatorAddresses() succeeds for legacy/v1.3/v1.4 hash functions. +func baseDef(version string) Definition { + return Definition{ + UUID: "f4c00e58-1a54-4e55-8d76-2b2a6d4c3f9a", + Name: "test-cluster-3of4", + Version: version, + Timestamp: "2024-06-15T12:00:00Z", + NumValidators: 2, + Threshold: 3, + DKGAlgorithm: "default", + ForkVersion: []byte{0x00, 0x00, 0x10, 0x20}, + Operators: []Operator{ + {Address: testAddr1, ENR: "enr:-Iu4QNHNMf"}, + {Address: testAddr2, ENR: "enr:-Iu4QABCDef"}, + {Address: testAddr3, ENR: "enr:-Iu4QXYZghi"}, + }, + Creator: Creator{Address: testAddr1}, + ValidatorAddresses: []ValidatorAddresses{ + {FeeRecipientAddress: testFee, WithdrawalAddress: testWithd}, + {FeeRecipientAddress: testFee, WithdrawalAddress: testWithd}, + }, + } +} + +// runHashDef calls the version-appropriate hashDefinition* function on a fresh +// ssz.HashWalker and returns the resulting HashRoot as a hex string. +func runHashDef(t *testing.T, d Definition, configOnly bool) string { + t.Helper() + + fn, err := getDefinitionHashFunc(d.Version) + require.NoError(t, err) + + hh := ssz.DefaultHasherPool.Get() + defer ssz.DefaultHasherPool.Put(hh) + + require.NoError(t, fn(d, hh, configOnly)) + + h, err := hh.HashRoot() + require.NoError(t, err) + + return hex.EncodeToString(h[:]) +} + +func TestHashDefinitionLegacy(t *testing.T) { + tests := []struct { + name string + d Definition + configOnly bool + expected string + }{ + { + name: "v1.0_config_no_timestamp", + d: func() Definition { + d := baseDef(v1_0) + d.Timestamp = "" // v1.0 had no timestamp + + return d + }(), + configOnly: true, + expected: "90336d34257fd9b1c2b6b119d008e7f0cff112e91455e24b0666e23aae480216", + }, + { + name: "v1.2_config", + d: baseDef(v1_2), + configOnly: true, + expected: "36e332cad485463ba6deb041fd5b3949fd7a09e3c1eb456bb3d9f97b8ae31673", + }, + { + name: "v1.2_definition", + d: baseDef(v1_2), + configOnly: false, + expected: "c81916384a24056a596f2d87daf6f2693c12f9df9601122436dfbce0c0e51ed2", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expected, runHashDef(t, tt.d, tt.configOnly)) + }) + } +} + +func TestHashDefinitionV1x3or4(t *testing.T) { + tests := []struct { + name string + d Definition + configOnly bool + expected string + }{ + { + name: "v1.3_config", + d: baseDef(v1_3), + configOnly: true, + expected: "e314abc0c7bc2dbf22a4c6c044481880675e47eed15bf7b734ce2a275dfbf81a", + }, + { + name: "v1.3_definition", + d: baseDef(v1_3), + configOnly: false, + expected: "6fb8988d9ecfc3abab94b79e1abd07564004a1da3b59a84d3158b2a001ee2fd5", + }, + { + name: "v1.4_config", + d: baseDef(v1_4), + configOnly: true, + expected: "068e5c2c4262e4fef6f99f858a3d7ceb2243bc411d9523d445a38752da68dec7", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expected, runHashDef(t, tt.d, tt.configOnly)) + }) + } +} + +func TestHashDefinitionV1x5to7(t *testing.T) { + tests := []struct { + name string + d Definition + configOnly bool + expected string + }{ + { + name: "v1.5_config", + d: baseDef(v1_5), + configOnly: true, + expected: "16612ab44e0e3605ab74f2d711afe6db71ab673967aa3be4ee565f3aa64415a9", + }, + { + name: "v1.7_config", + d: baseDef(v1_7), + configOnly: true, + expected: "c0ade8b834d7731f4076d364472be55974a9cb23ed97cc828bf5661b1df1c564", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expected, runHashDef(t, tt.d, tt.configOnly)) + }) + } +} + +func TestHashDefinitionV1x8(t *testing.T) { + tests := []struct { + name string + d Definition + configOnly bool + expected string + }{ + { + name: "v1.8_config_with_deposit", + d: func() Definition { + d := baseDef(v1_8) + d.DepositAmounts = []eth2p0.Gwei{32000000000} + + return d + }(), + configOnly: true, + expected: "cbdb72b363a735e1f469cbd7983734d524001a68dcf3ab71153d69892fa0e36c", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expected, runHashDef(t, tt.d, tt.configOnly)) + }) + } +} + +func TestHashDefinitionV1x9(t *testing.T) { + tests := []struct { + name string + d Definition + configOnly bool + expected string + }{ + { + name: "v1.9_config_with_consensus_protocol", + d: func() Definition { + d := baseDef(v1_9) + d.DepositAmounts = []eth2p0.Gwei{32000000000} + d.ConsensusProtocol = "abft" + + return d + }(), + configOnly: true, + expected: "8c55a5a65d8f8dbecaf6fbd39a11c2e701ce3aba1dd7fdd212a35c2e196db34f", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expected, runHashDef(t, tt.d, tt.configOnly)) + }) + } +} + +func TestHashDefinitionV1x10(t *testing.T) { + tests := []struct { + name string + d Definition + configOnly bool + expected string + }{ + { + name: "v1.10_config_with_gas_and_compounding", + d: func() Definition { + d := baseDef(v1_10) + d.DepositAmounts = []eth2p0.Gwei{32000000000} + d.ConsensusProtocol = "abft" + d.TargetGasLimit = 30000000 + d.Compounding = true + + return d + }(), + configOnly: true, + expected: "5e16fdd44a25d37ebce15ea9d2069ec9d0930ce8d3a0e9091f82edae5969b306", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expected, runHashDef(t, tt.d, tt.configOnly)) + }) + } +} + func TestHashBuilderRegistration(t *testing.T) { const network = "goerli" diff --git a/core/priority/prioritiser_internal_test.go b/core/priority/prioritiser_internal_test.go new file mode 100644 index 000000000..bf575e2de --- /dev/null +++ b/core/priority/prioritiser_internal_test.go @@ -0,0 +1,65 @@ +// Copyright © 2022-2026 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package priority + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + + pbv1 "github.com/obolnetwork/charon/core/corepb/v1" +) + +func TestHashProto(t *testing.T) { + tests := []struct { + name string + msg proto.Message + expected string + }{ + { + name: "empty_priority_msg", + msg: &pbv1.PriorityMsg{}, + expected: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + { + name: "priority_msg_peer1", + msg: &pbv1.PriorityMsg{PeerId: "peer1"}, + expected: "0x1a05706565723100000000000000000000000000000000000000000000000000", + }, + { + name: "priority_msg_peer2", + msg: &pbv1.PriorityMsg{PeerId: "peer2"}, + expected: "0x1a05706565723200000000000000000000000000000000000000000000000000", + }, + { + name: "priority_msg_with_signature", + msg: &pbv1.PriorityMsg{ + PeerId: "peer1", + Signature: []byte{0xde, 0xad, 0xbe, 0xef}, + }, + expected: "0x1a0570656572312204deadbeef00000000000000000000000000000000000000", + }, + { + name: "empty_priority_result", + msg: &pbv1.PriorityResult{}, + expected: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + { + name: "priority_result_with_msgs", + msg: &pbv1.PriorityResult{ + Msgs: []*pbv1.PriorityMsg{{PeerId: "peer1"}}, + }, + expected: "0x0a071a0570656572310000000000000000000000000000000000000000000000", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := hashProto(tt.msg) + require.NoError(t, err) + require.Equal(t, tt.expected, "0x"+hex.EncodeToString(got[:])) + }) + } +} diff --git a/core/proto_internal_test.go b/core/proto_internal_test.go new file mode 100644 index 000000000..b6a3fdc32 --- /dev/null +++ b/core/proto_internal_test.go @@ -0,0 +1,214 @@ +// Copyright © 2022-2026 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package core + +import ( + "encoding/hex" + "encoding/json" + "testing" + + eth2v1 "github.com/attestantio/go-eth2-client/api/v1" + eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/stretchr/testify/require" +) + +// TestMarshal tests the marshal() internal function. +// marshal() uses SSZ when the type implements ssz.Marshaler and SSZ is enabled, +// otherwise falls back to JSON. +func TestMarshal(t *testing.T) { + // adZeros is a zero-valued AttestationData with non-nil Source/Target (required for valid SSZ). + adZeros := AttestationData{ + Data: eth2p0.AttestationData{ + Source: new(eth2p0.Checkpoint), + Target: new(eth2p0.Checkpoint), + }, + } + + // adValid is an AttestationData with non-zero fields, including CommitteeLength > 0 + // so that JSON roundtrip validation succeeds. + adValid := AttestationData{ + Data: eth2p0.AttestationData{ + Slot: 10, + Index: 2, + Source: new(eth2p0.Checkpoint), + Target: new(eth2p0.Checkpoint), + }, + Duty: eth2v1.AttesterDuty{ + Slot: 10, + CommitteeIndex: 2, + CommitteeLength: 128, + CommitteesAtSlot: 1, + ValidatorCommitteeIndex: 5, + }, + } + + tests := []struct { + name string + value any + expected string + disableSSZ bool + hexCompare bool // true → compare hex(result); false → compare string(result) + }{ + { + name: "attestation_data_zeros/ssz", + value: adZeros, + hexCompare: true, + expected: "08000000880000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + }, + { + name: "attestation_data_nozero/ssz", + value: adValid, + hexCompare: true, + expected: "08000000880000000a000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000200000000000000800000000000000001000000000000000500000000000000", + }, + { + name: "signature/json", + value: Signature{0xde, 0xad, 0xbe, 0xef}, + hexCompare: false, + expected: `"3q2+7w=="`, + }, + { + name: "attestation_data_zeros/json_ssz_disabled", + value: adZeros, + disableSSZ: true, + hexCompare: false, + expected: `{"attestation_data":{"slot":"0","index":"0","beacon_block_root":"0x0000000000000000000000000000000000000000000000000000000000000000","source":{"epoch":"0","root":"0x0000000000000000000000000000000000000000000000000000000000000000"},"target":{"epoch":"0","root":"0x0000000000000000000000000000000000000000000000000000000000000000"}},"attestation_duty":{"pubkey":"0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","slot":"0","validator_index":"0","committee_index":"0","committee_length":"0","committees_at_slot":"0","validator_committee_index":"0"}}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.disableSSZ { + DisableSSZMarshallingForT(t) + } + + b, err := marshal(tt.value) + require.NoError(t, err) + + if tt.hexCompare { + require.Equal(t, tt.expected, hex.EncodeToString(b)) + } else { + require.Equal(t, tt.expected, string(b)) + } + }) + } +} + +// TestUnmarshal tests the unmarshal() internal function. +// unmarshal() tries SSZ first (if the type implements ssz.Unmarshaler), +// falling back to JSON when SSZ fails and the data starts with '{'. +func TestUnmarshal(t *testing.T) { + // adZeros is a zero-valued AttestationData with non-nil Source/Target (required for valid SSZ). + adZeros := AttestationData{ + Data: eth2p0.AttestationData{ + Source: new(eth2p0.Checkpoint), + Target: new(eth2p0.Checkpoint), + }, + } + + // adValid is an AttestationData with non-zero fields, including CommitteeLength > 0 + // so that JSON roundtrip validation succeeds. + adValid := AttestationData{ + Data: eth2p0.AttestationData{ + Slot: 10, + Index: 2, + Source: new(eth2p0.Checkpoint), + Target: new(eth2p0.Checkpoint), + }, + Duty: eth2v1.AttesterDuty{ + Slot: 10, + CommitteeIndex: 2, + CommitteeLength: 128, + CommitteesAtSlot: 1, + ValidatorCommitteeIndex: 5, + }, + } + + adZerosSSZ, err := adZeros.MarshalSSZ() + require.NoError(t, err) + + adValidSSZ, err := adValid.MarshalSSZ() + require.NoError(t, err) + + adValidJSON, err := json.Marshal(adValid) + require.NoError(t, err) + + sigBytes := Signature{0xde, 0xad, 0xbe, 0xef} + sigJSON, err := json.Marshal(sigBytes) + require.NoError(t, err) + + tests := []struct { + name string + data []byte + target func() any + check func(t *testing.T, got any) + errContains string // non-empty → expect error containing this string + }{ + { + name: "attestation_data_zeros/ssz", + data: adZerosSSZ, + target: func() any { return new(AttestationData) }, + check: func(t *testing.T, got any) { + t.Helper() + require.Equal(t, adZeros, *got.(*AttestationData)) + }, + }, + { + name: "attestation_data_nozero/ssz", + data: adValidSSZ, + target: func() any { return new(AttestationData) }, + check: func(t *testing.T, got any) { + t.Helper() + require.Equal(t, adValid, *got.(*AttestationData)) + }, + }, + { + name: "signature/json", + data: sigJSON, + target: func() any { return new(Signature) }, + check: func(t *testing.T, got any) { + t.Helper() + require.Equal(t, sigBytes, *got.(*Signature)) + }, + }, + { + // SSZ type falls back to JSON when SSZ unmarshal fails and data starts with '{'. + name: "attestation_data/json_fallback", + data: adValidJSON, + target: func() any { return new(AttestationData) }, + check: func(t *testing.T, got any) { + t.Helper() + require.Equal(t, adValid, *got.(*AttestationData)) + }, + }, + { + // Non-JSON, non-SSZ bytes produce an SSZ error (no JSON fallback attempted). + name: "error/invalid_ssz_no_json_prefix", + data: []byte{0x01, 0x02, 0x03}, + target: func() any { return new(AttestationData) }, + errContains: "unmarshal ssz", + }, + { + // Invalid JSON for a non-SSZ type produces a JSON error. + name: "error/invalid_json_for_json_type", + data: []byte(`not-json`), + target: func() any { return new(Signature) }, + errContains: "unmarshal json", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := tt.target() + err := unmarshal(tt.data, v) + + if tt.errContains != "" { + require.ErrorContains(t, err, tt.errContains) + return + } + + require.NoError(t, err) + tt.check(t, v) + }) + } +} diff --git a/core/signeddata_test.go b/core/signeddata_test.go index 410cb88c1..d50c66dcd 100644 --- a/core/signeddata_test.go +++ b/core/signeddata_test.go @@ -3,10 +3,12 @@ package core_test import ( + "bytes" "encoding/json" "fmt" "testing" + bitfield "github.com/OffchainLabs/go-bitfield" eth2api "github.com/attestantio/go-eth2-client/api" eth2bellatrix "github.com/attestantio/go-eth2-client/api/v1/bellatrix" eth2capella "github.com/attestantio/go-eth2-client/api/v1/capella" @@ -20,6 +22,7 @@ import ( "github.com/attestantio/go-eth2-client/spec/deneb" "github.com/attestantio/go-eth2-client/spec/electra" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/holiman/uint256" "github.com/stretchr/testify/require" "github.com/obolnetwork/charon/core" @@ -1092,3 +1095,252 @@ func TestVersionedSignedAggregateAndProofUtilFunctions(t *testing.T) { // require.NoError(t, err) // require.Equal(t, expectedStdHashStr, hex.EncodeToString(realHashStd[:])) // } + +func TestCloneSSZMarshaler(t *testing.T) { + var sig96 eth2p0.BLSSignature + for i := range sig96 { + sig96[i] = 0xcd + } + + var root eth2p0.Root + for i := range root { + root[i] = 0xab + } + + attestationData := ð2p0.AttestationData{ + Slot: 1, + Index: 2, + BeaconBlockRoot: root, + Source: ð2p0.Checkpoint{Epoch: 1, Root: root}, + Target: ð2p0.Checkpoint{Epoch: 2, Root: root}, + } + + aggBits := bitfield.Bitlist{0x03} + + phase0Att := ð2p0.Attestation{ + AggregationBits: aggBits, + Data: attestationData, + Signature: sig96, + } + + tests := []struct { + name string + value interface { + MarshalSSZ() ([]byte, error) + } + unmarshal func([]byte) (any, error) + expected string + }{ + { + name: "Attestation", + value: core.Attestation{ + Attestation: *phase0Att, + }, + unmarshal: func(b []byte) (any, error) { + var v core.Attestation + return v, v.UnmarshalSSZ(b) + }, + expected: "0xe400000001000000000000000200000000000000abababababababababababababababababababababababababababababababab0100000000000000abababababababababababababababababababababababababababababababab0200000000000000ababababababababababababababababababababababababababababababababcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd03", + }, + { + name: "SignedAggregateAndProof", + value: core.SignedAggregateAndProof{ + SignedAggregateAndProof: eth2p0.SignedAggregateAndProof{ + Message: ð2p0.AggregateAndProof{ + AggregatorIndex: 3, + Aggregate: phase0Att, + SelectionProof: sig96, + }, + Signature: sig96, + }, + }, + unmarshal: func(b []byte) (any, error) { + var v core.SignedAggregateAndProof + return v, v.UnmarshalSSZ(b) + }, + expected: "0x64000000cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd03000000000000006c000000cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcde400000001000000000000000200000000000000abababababababababababababababababababababababababababababababab0100000000000000abababababababababababababababababababababababababababababababab0200000000000000ababababababababababababababababababababababababababababababababcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd03", + }, + { + name: "SignedSyncMessage", + value: core.SignedSyncMessage{ + SyncCommitteeMessage: altair.SyncCommitteeMessage{ + Slot: 1, + BeaconBlockRoot: root, + ValidatorIndex: 2, + Signature: sig96, + }, + }, + unmarshal: func(b []byte) (any, error) { + var v core.SignedSyncMessage + return v, v.UnmarshalSSZ(b) + }, + expected: "0x0100000000000000abababababababababababababababababababababababababababababababab0200000000000000cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", + }, + { + name: "SyncContributionAndProof", + value: core.SyncContributionAndProof{ + ContributionAndProof: altair.ContributionAndProof{ + AggregatorIndex: 1, + Contribution: &altair.SyncCommitteeContribution{ + Slot: 1, + BeaconBlockRoot: root, + SubcommitteeIndex: 0, + AggregationBits: bitfield.Bitvector128(bytes.Repeat([]byte{0x01}, 16)), + Signature: sig96, + }, + SelectionProof: sig96, + }, + }, + unmarshal: func(b []byte) (any, error) { + var v core.SyncContributionAndProof + return v, v.UnmarshalSSZ(b) + }, + expected: "0x01000000000000000100000000000000abababababababababababababababababababababababababababababababab000000000000000001010101010101010101010101010101cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", + }, + { + name: "SignedSyncContributionAndProof", + value: core.SignedSyncContributionAndProof{ + SignedContributionAndProof: altair.SignedContributionAndProof{ + Message: &altair.ContributionAndProof{ + AggregatorIndex: 1, + Contribution: &altair.SyncCommitteeContribution{ + Slot: 1, + BeaconBlockRoot: root, + SubcommitteeIndex: 0, + AggregationBits: bitfield.Bitvector128(bytes.Repeat([]byte{0x01}, 16)), + Signature: sig96, + }, + SelectionProof: sig96, + }, + Signature: sig96, + }, + }, + unmarshal: func(b []byte) (any, error) { + var v core.SignedSyncContributionAndProof + return v, v.UnmarshalSSZ(b) + }, + expected: "0x01000000000000000100000000000000abababababababababababababababababababababababababababababababab000000000000000001010101010101010101010101010101cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", + }, + { + name: "VersionedAttestation/fulu", + value: core.VersionedAttestation{ + VersionedAttestation: eth2spec.VersionedAttestation{ + Version: eth2spec.DataVersionFulu, + Fulu: &electra.Attestation{ + AggregationBits: aggBits, + Data: attestationData, + Signature: sig96, + CommitteeBits: bitfield.Bitvector64(bytes.Repeat([]byte{0x01}, 8)), + }, + }, + }, + unmarshal: func(b []byte) (any, error) { + var v core.VersionedAttestation + return v, v.UnmarshalSSZ(b) + }, + expected: "0x06000000000000000c000000ec00000001000000000000000200000000000000abababababababababababababababababababababababababababababababab0100000000000000abababababababababababababababababababababababababababababababab0200000000000000ababababababababababababababababababababababababababababababababcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd010101010101010103", + }, + { + name: "VersionedSignedAggregateAndProof/fulu", + value: core.VersionedSignedAggregateAndProof{ + VersionedSignedAggregateAndProof: eth2spec.VersionedSignedAggregateAndProof{ + Version: eth2spec.DataVersionFulu, + Fulu: &electra.SignedAggregateAndProof{ + Message: &electra.AggregateAndProof{ + AggregatorIndex: 3, + Aggregate: &electra.Attestation{ + AggregationBits: aggBits, + Data: attestationData, + Signature: sig96, + CommitteeBits: bitfield.Bitvector64(bytes.Repeat([]byte{0x01}, 8)), + }, + SelectionProof: sig96, + }, + Signature: sig96, + }, + }, + }, + unmarshal: func(b []byte) (any, error) { + var v core.VersionedSignedAggregateAndProof + return v, v.UnmarshalSSZ(b) + }, + expected: "0x06000000000000000c00000064000000cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd03000000000000006c000000cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdec00000001000000000000000200000000000000abababababababababababababababababababababababababababababababab0100000000000000abababababababababababababababababababababababababababababababab0200000000000000ababababababababababababababababababababababababababababababababcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd010101010101010103", + }, + { + name: "VersionedSignedProposal/fulu", + value: core.VersionedSignedProposal{ + VersionedSignedProposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionFulu, + Fulu: ð2fulu.SignedBlockContents{ + SignedBlock: &electra.SignedBeaconBlock{ + Message: &electra.BeaconBlock{ + Slot: 1, + ProposerIndex: 2, + ParentRoot: root, + StateRoot: root, + Body: &electra.BeaconBlockBody{ + RANDAOReveal: sig96, + ETH1Data: ð2p0.ETH1Data{ + DepositRoot: root, + DepositCount: 0, + BlockHash: root[:], + }, + Graffiti: [32]byte{0x01}, + ProposerSlashings: []*eth2p0.ProposerSlashing{}, + AttesterSlashings: []*electra.AttesterSlashing{}, + Attestations: []*electra.Attestation{}, + Deposits: []*eth2p0.Deposit{}, + VoluntaryExits: []*eth2p0.SignedVoluntaryExit{}, + SyncAggregate: &altair.SyncAggregate{ + SyncCommitteeBits: bitfield.Bitvector512(bytes.Repeat([]byte{0x01}, 64)), + SyncCommitteeSignature: sig96, + }, + ExecutionPayload: &deneb.ExecutionPayload{ + ParentHash: eth2p0.Hash32(root), + FeeRecipient: bellatrix.ExecutionAddress{0xab}, + StateRoot: root, + ReceiptsRoot: root, + LogsBloom: [256]byte{0x01}, + PrevRandao: root, + BaseFeePerGas: new(uint256.Int), + BlockHash: eth2p0.Hash32(root), + ExtraData: []byte{}, + Transactions: []bellatrix.Transaction{}, + Withdrawals: []*capella.Withdrawal{}, + }, + BLSToExecutionChanges: []*capella.SignedBLSToExecutionChange{}, + BlobKZGCommitments: []deneb.KZGCommitment{}, + ExecutionRequests: &electra.ExecutionRequests{ + Deposits: []*electra.DepositRequest{}, + Withdrawals: []*electra.WithdrawalRequest{}, + Consolidations: []*electra.ConsolidationRequest{}, + }, + }, + }, + Signature: sig96, + }, + KZGProofs: []deneb.KZGProof{}, + Blobs: []deneb.Blob{}, + }, + }, + }, + unmarshal: func(b []byte) (any, error) { + var v core.VersionedSignedProposal + return v, v.UnmarshalSSZ(b) + }, + expected: "0x0600000000000000000d0000000c0000006c0400006c04000064000000cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd01000000000000000200000000000000abababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab54000000cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdabababababababababababababababababababababababababababababababab0000000000000000abababababababababababababababababababababababababababababababab01000000000000000000000000000000000000000000000000000000000000008c0100008c0100008c0100008c0100008c01000001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd8c0100009c0300009c0300009c030000ababababababababababababababababababababababababababababababababab00000000000000000000000000000000000000abababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000abababababababababababababababababababababababababababababababab0000000000000000000000000000000000000000000000000000000000000000100200000000000000000000000000000000000000000000000000000000000000000000abababababababababababababababababababababababababababababababab1002000010020000000000000000000000000000000000000c0000000c0000000c000000", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, err := tt.value.MarshalSSZ() + require.NoError(t, err) + require.Equal(t, tt.expected, fmt.Sprintf("%#x", b)) + + got, err := tt.unmarshal(b) + require.NoError(t, err) + require.Equal(t, tt.value, got) + }) + } +} diff --git a/core/ssz_test.go b/core/ssz_test.go index 3c112f4d8..a316d59ea 100644 --- a/core/ssz_test.go +++ b/core/ssz_test.go @@ -10,7 +10,9 @@ import ( "testing" "time" + bitfield "github.com/OffchainLabs/go-bitfield" eth2api "github.com/attestantio/go-eth2-client/api" + eth2v1 "github.com/attestantio/go-eth2-client/api/v1" eth2bellatrix "github.com/attestantio/go-eth2-client/api/v1/bellatrix" eth2capella "github.com/attestantio/go-eth2-client/api/v1/capella" eth2deneb "github.com/attestantio/go-eth2-client/api/v1/deneb" @@ -19,6 +21,8 @@ import ( "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/bellatrix" "github.com/attestantio/go-eth2-client/spec/capella" + "github.com/attestantio/go-eth2-client/spec/deneb" + "github.com/attestantio/go-eth2-client/spec/electra" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" ssz "github.com/ferranbt/fastssz" "github.com/stretchr/testify/require" @@ -496,3 +500,808 @@ func TestValIdxVersionedAttestation(t *testing.T) { require.Equal(t, val1, val2) } + +func TestAttestationDataSSZ(t *testing.T) { + // Pre-declare fixed-size arrays used across multiple test cases. + var rootAB, rootCD, rootEF eth2p0.Root + for i := range rootAB { + rootAB[i] = 0xab + rootCD[i] = 0xcd + rootEF[i] = 0xef + } + + var pubKey01 eth2p0.BLSPubKey + for i := range pubKey01 { + pubKey01[i] = 0x01 + } + + tests := []struct { + name string + value core.AttestationData + expected string + }{ + { + name: "zeros", + // All fields zero. Source/Target must be non-nil for valid SSZ. + value: core.AttestationData{ + Data: eth2p0.AttestationData{ + Source: new(eth2p0.Checkpoint), + Target: new(eth2p0.Checkpoint), + }, + Duty: eth2v1.AttesterDuty{}, + }, + // offset0=8 (08000000), offset1=136 (88000000), 224 zero bytes. + expected: "0x0800000088000000" + + "0000000000000000" + // Slot (8 bytes) + "0000000000000000" + // Index (8 bytes) + "0000000000000000000000000000000000000000000000000000000000000000" + // BeaconBlockRoot (32 bytes) + "0000000000000000" + // Source.Epoch + "0000000000000000000000000000000000000000000000000000000000000000" + // Source.Root (32 bytes) + "0000000000000000" + // Target.Epoch + "0000000000000000000000000000000000000000000000000000000000000000" + // Target.Root (32 bytes) + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + // PubKey (48 bytes) + "0000000000000000" + // Duty.Slot + "0000000000000000" + // Duty.ValidatorIndex + "0000000000000000" + // Duty.CommitteeIndex + "0000000000000000" + // Duty.CommitteeLength + "0000000000000000" + // Duty.CommitteesAtSlot + "0000000000000000", // Duty.ValidatorCommitteeIndex + }, + { + name: "specific_values", + value: core.AttestationData{ + Data: eth2p0.AttestationData{ + Slot: 1, + Index: 2, + Source: ð2p0.Checkpoint{Epoch: 0}, + Target: ð2p0.Checkpoint{Epoch: 0}, + }, + Duty: eth2v1.AttesterDuty{ + CommitteeIndex: 3, + CommitteeLength: 4, + }, + }, + // offset0=8, offset1=136, AttestationData with Slot=1/Index=2, duty with CommitteeIndex=3/Length=4. + expected: "0x0800000088000000" + + "0100000000000000" + // Slot = 1 + "0200000000000000" + // Index = 2 + "0000000000000000000000000000000000000000000000000000000000000000" + // BeaconBlockRoot (32 bytes) + "0000000000000000" + // Source.Epoch = 0 + "0000000000000000000000000000000000000000000000000000000000000000" + // Source.Root (32 bytes) + "0000000000000000" + // Target.Epoch = 0 + "0000000000000000000000000000000000000000000000000000000000000000" + // Target.Root (32 bytes) + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + // PubKey (48 bytes) + "0000000000000000" + // Duty.Slot = 0 + "0000000000000000" + // Duty.ValidatorIndex = 0 + "0300000000000000" + // Duty.CommitteeIndex = 3 + "0400000000000000" + // Duty.CommitteeLength = 4 + "0000000000000000" + // Duty.CommitteesAtSlot = 0 + "0000000000000000", // Duty.ValidatorCommitteeIndex = 0 + }, + { + name: "all_data_fields", + // Exercises every AttestationData field with non-zero values; Duty is zero. + value: core.AttestationData{ + Data: eth2p0.AttestationData{ + Slot: 12345, + Index: 7, + BeaconBlockRoot: rootAB, + Source: ð2p0.Checkpoint{Epoch: 100, Root: rootCD}, + Target: ð2p0.Checkpoint{Epoch: 101, Root: rootEF}, + }, + Duty: eth2v1.AttesterDuty{}, + }, + // Slot=12345=0x3039, Index=7, roots filled, epochs 100/101. + expected: "0x0800000088000000" + + "3930000000000000" + // Slot = 12345 (0x3039 LE) + "0700000000000000" + // Index = 7 + strings.Repeat("ab", 32) + // BeaconBlockRoot + "6400000000000000" + // Source.Epoch = 100 (0x64) + strings.Repeat("cd", 32) + // Source.Root + "6500000000000000" + // Target.Epoch = 101 (0x65) + strings.Repeat("ef", 32) + // Target.Root + strings.Repeat("00", 48) + // PubKey (zero) + "0000000000000000" + // Duty.Slot = 0 + "0000000000000000" + // Duty.ValidatorIndex = 0 + "0000000000000000" + // Duty.CommitteeIndex = 0 + "0000000000000000" + // Duty.CommitteeLength = 0 + "0000000000000000" + // Duty.CommitteesAtSlot = 0 + "0000000000000000", // Duty.ValidatorCommitteeIndex = 0 + }, + { + name: "all_duty_fields", + // Exercises every AttesterDuty field; AttestationData fields are minimal. + value: core.AttestationData{ + Data: eth2p0.AttestationData{ + Source: new(eth2p0.Checkpoint), + Target: new(eth2p0.Checkpoint), + }, + Duty: eth2v1.AttesterDuty{ + PubKey: pubKey01, + Slot: 9999, + ValidatorIndex: 42, + CommitteeIndex: 3, + CommitteeLength: 128, + CommitteesAtSlot: 64, + ValidatorCommitteeIndex: 7, + }, + }, + // Slot=9999=0x270F, ValidatorIndex=42=0x2A, CommitteeIndex=3, Length=128=0x80, + // CommitteesAtSlot=64=0x40, ValidatorCommitteeIndex=7. + expected: "0x0800000088000000" + + strings.Repeat("00", 8) + // Data.Slot = 0 + strings.Repeat("00", 8) + // Data.Index = 0 + strings.Repeat("00", 32) + // BeaconBlockRoot (zero) + strings.Repeat("00", 8) + // Source.Epoch = 0 + strings.Repeat("00", 32) + // Source.Root (zero) + strings.Repeat("00", 8) + // Target.Epoch = 0 + strings.Repeat("00", 32) + // Target.Root (zero) + strings.Repeat("01", 48) + // PubKey (all 0x01) + "0f27000000000000" + // Duty.Slot = 9999 (0x270F LE) + "2a00000000000000" + // Duty.ValidatorIndex = 42 + "0300000000000000" + // Duty.CommitteeIndex = 3 + "8000000000000000" + // Duty.CommitteeLength = 128 + "4000000000000000" + // Duty.CommitteesAtSlot = 64 + "0700000000000000", // Duty.ValidatorCommitteeIndex = 7 + }, + { + name: "full", + // All fields populated with distinct non-zero values. + value: core.AttestationData{ + Data: eth2p0.AttestationData{ + Slot: 4294967295, // max uint32 + Index: 255, + BeaconBlockRoot: rootAB, + Source: ð2p0.Checkpoint{Epoch: 999, Root: rootCD}, + Target: ð2p0.Checkpoint{Epoch: 1000, Root: rootEF}, + }, + Duty: eth2v1.AttesterDuty{ + PubKey: pubKey01, + Slot: 4294967295, + ValidatorIndex: 1000000, + CommitteeIndex: 511, + CommitteeLength: 1024, + CommitteesAtSlot: 512, + ValidatorCommitteeIndex: 255, + }, + }, + // Slot=4294967295=0xFFFFFFFF, Index=255=0xFF, epochs 999/1000, + // ValidatorIndex=1000000=0xF4240, CommitteeIndex=511=0x1FF, + // CommitteeLength=1024=0x400, CommitteesAtSlot=512=0x200. + expected: "0x0800000088000000" + + "ffffffff00000000" + // Data.Slot = 4294967295 + "ff00000000000000" + // Data.Index = 255 + strings.Repeat("ab", 32) + // BeaconBlockRoot + "e703000000000000" + // Source.Epoch = 999 (0x3E7 LE) + strings.Repeat("cd", 32) + // Source.Root + "e803000000000000" + // Target.Epoch = 1000 (0x3E8 LE) + strings.Repeat("ef", 32) + // Target.Root + strings.Repeat("01", 48) + // PubKey (all 0x01) + "ffffffff00000000" + // Duty.Slot = 4294967295 + "40420f0000000000" + // Duty.ValidatorIndex = 1000000 (0xF4240 LE) + "ff01000000000000" + // Duty.CommitteeIndex = 511 (0x1FF LE) + "0004000000000000" + // Duty.CommitteeLength = 1024 (0x400 LE) + "0002000000000000" + // Duty.CommitteesAtSlot = 512 (0x200 LE) + "ff00000000000000", // Duty.ValidatorCommitteeIndex = 255 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, err := tt.value.MarshalSSZ() + require.NoError(t, err) + require.Equal(t, tt.expected, fmt.Sprintf("%#x", b)) + + var got core.AttestationData + require.NoError(t, got.UnmarshalSSZ(b)) + require.Equal(t, tt.value, got) + }) + } +} + +func TestVersionedAttestationSSZ(t *testing.T) { + phase0Att := ð2p0.Attestation{ + AggregationBits: bitfield.Bitlist{0x01}, + Data: ð2p0.AttestationData{ + Source: new(eth2p0.Checkpoint), + Target: new(eth2p0.Checkpoint), + }, + } + + electraAtt := &electra.Attestation{ + AggregationBits: bitfield.Bitlist{0x01}, + Data: ð2p0.AttestationData{ + Source: new(eth2p0.Checkpoint), + Target: new(eth2p0.Checkpoint), + }, + CommitteeBits: bitfield.Bitvector64(make([]byte, 8)), + } + + v0 := eth2p0.ValidatorIndex(0) + v42 := eth2p0.ValidatorIndex(42) + v999 := eth2p0.ValidatorIndex(999) + + // phase0AttSSZ is the inner SSZ of a minimal phase0.Attestation (229 bytes): + // AggBitsOffset(4) + Data(128) + Signature(96) + AggBits(1). + phase0AttSSZ := "e4000000" + // AggBits offset=228 within attestation + strings.Repeat("00", 128) + // Data (zero) + strings.Repeat("00", 96) + // Signature (zero) + "01" // AggBits={0x01} + + // electraAttSSZ is the inner SSZ of a minimal electra.Attestation (237 bytes): + // AggBitsOffset(4) + Data(128) + CommitteeBits(8) + Signature(96) + AggBits(1). + electraAttSSZ := "ec000000" + // AggBits offset=236 within attestation + strings.Repeat("00", 128) + // Data (zero) + strings.Repeat("00", 8) + // CommitteeBits (zero) + strings.Repeat("00", 96) + // Signature (zero) + "01" // AggBits={0x01} + + tests := []struct { + name string + value core.VersionedAttestation + expected string + }{ + { + name: "phase0_zeros", + value: core.VersionedAttestation{VersionedAttestation: eth2spec.VersionedAttestation{Version: eth2spec.DataVersionPhase0, Phase0: phase0Att}}, + // version(8) + offset=12(4) + phase0 attestation(229) + expected: "0x" + + "0000000000000000" + // version=0 (phase0) + "0c000000" + // outer offset=12 + phase0AttSSZ, + }, + { + name: "electra_zeros", + value: core.VersionedAttestation{VersionedAttestation: eth2spec.VersionedAttestation{Version: eth2spec.DataVersionElectra, Electra: electraAtt}}, + // version(8) + offset=12(4) + electra attestation(237) + expected: "0x" + + "0500000000000000" + // version=5 (electra) + "0c000000" + // outer offset=12 + electraAttSSZ, + }, + { + name: "fulu_zeros", + value: core.VersionedAttestation{VersionedAttestation: eth2spec.VersionedAttestation{Version: eth2spec.DataVersionFulu, Fulu: electraAtt}}, + expected: "0x" + + "0600000000000000" + // version=6 (fulu) + "0c000000" + + electraAttSSZ, + }, + { + name: "phase0_non_zero", + // Slot=1000, Index=5, Source.Epoch=10, Target.Epoch=11, AggBits={0x03}. + value: core.VersionedAttestation{VersionedAttestation: eth2spec.VersionedAttestation{ + Version: eth2spec.DataVersionPhase0, + Phase0: ð2p0.Attestation{ + AggregationBits: bitfield.Bitlist{0x03}, + Data: ð2p0.AttestationData{ + Slot: 1000, + Index: 5, + Source: ð2p0.Checkpoint{Epoch: 10}, + Target: ð2p0.Checkpoint{Epoch: 11}, + }, + }, + }}, + expected: "0x" + + "0000000000000000" + // version=0 + "0c000000" + // outer offset=12 + "e4000000" + // AggBits offset=228 + "e803000000000000" + // Data.Slot=1000 (0x3E8 LE) + "0500000000000000" + // Data.Index=5 + strings.Repeat("00", 32) + // BeaconBlockRoot (zero) + "0a00000000000000" + // Source.Epoch=10 (0x0A LE) + strings.Repeat("00", 32) + // Source.Root (zero) + "0b00000000000000" + // Target.Epoch=11 (0x0B LE) + strings.Repeat("00", 32) + // Target.Root (zero) + strings.Repeat("00", 96) + // Signature (zero) + "03", // AggBits={0x03} + }, + { + name: "electra_non_zero", + // Slot=2000, Index=3, Source.Epoch=20, Target.Epoch=21, AggBits={0x07}. + value: core.VersionedAttestation{VersionedAttestation: eth2spec.VersionedAttestation{ + Version: eth2spec.DataVersionElectra, + Electra: &electra.Attestation{ + AggregationBits: bitfield.Bitlist{0x07}, + Data: ð2p0.AttestationData{ + Slot: 2000, + Index: 3, + Source: ð2p0.Checkpoint{Epoch: 20}, + Target: ð2p0.Checkpoint{Epoch: 21}, + }, + CommitteeBits: bitfield.Bitvector64(make([]byte, 8)), + }, + }}, + expected: "0x" + + "0500000000000000" + // version=5 + "0c000000" + + "ec000000" + // AggBits offset=236 + "d007000000000000" + // Data.Slot=2000 (0x7D0 LE) + "0300000000000000" + // Data.Index=3 + strings.Repeat("00", 32) + + "1400000000000000" + // Source.Epoch=20 (0x14 LE) + strings.Repeat("00", 32) + + "1500000000000000" + // Target.Epoch=21 (0x15 LE) + strings.Repeat("00", 32) + + strings.Repeat("00", 8) + // CommitteeBits (zero) + strings.Repeat("00", 96) + + "07", // AggBits={0x07} + }, + { + name: "phase0_valIdx0", + value: core.VersionedAttestation{VersionedAttestation: eth2spec.VersionedAttestation{Version: eth2spec.DataVersionPhase0, Phase0: phase0Att, ValidatorIndex: &v0}}, + // version(8) + valIdx=0(8) + offset=20(4) + phase0 attestation(229) + expected: "0x" + + "0000000000000000" + // version=0 + "0000000000000000" + // valIdx=0 + "14000000" + // outer offset=20 + phase0AttSSZ, + }, + { + name: "phase0_valIdx42", + value: core.VersionedAttestation{VersionedAttestation: eth2spec.VersionedAttestation{Version: eth2spec.DataVersionPhase0, Phase0: phase0Att, ValidatorIndex: &v42}}, + // version(8) + valIdx=42(8) + offset=20(4) + phase0 attestation(229) + expected: "0x" + + "0000000000000000" + // version=0 + "2a00000000000000" + // valIdx=42 (0x2A LE) + "14000000" + // outer offset=20 + phase0AttSSZ, + }, + { + name: "electra_valIdx999", + value: core.VersionedAttestation{VersionedAttestation: eth2spec.VersionedAttestation{Version: eth2spec.DataVersionElectra, Electra: electraAtt, ValidatorIndex: &v999}}, + // version(8) + valIdx=999(8) + offset=20(4) + electra attestation(237) + expected: "0x" + + "0500000000000000" + // version=5 + "e703000000000000" + // valIdx=999 (0x3E7 LE) + "14000000" + // outer offset=20 + electraAttSSZ, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, err := tt.value.MarshalSSZ() + require.NoError(t, err) + require.Equal(t, tt.expected, fmt.Sprintf("%#x", b)) + + var got core.VersionedAttestation + require.NoError(t, got.UnmarshalSSZ(b)) + require.Equal(t, tt.value, got) + }) + } +} + +func TestVersionedAggregatedAttestationSSZ(t *testing.T) { + phase0Att := ð2p0.Attestation{ + AggregationBits: bitfield.Bitlist{0x01}, + Data: ð2p0.AttestationData{ + Source: new(eth2p0.Checkpoint), + Target: new(eth2p0.Checkpoint), + }, + } + + electraAtt := &electra.Attestation{ + AggregationBits: bitfield.Bitlist{0x01}, + Data: ð2p0.AttestationData{ + Source: new(eth2p0.Checkpoint), + Target: new(eth2p0.Checkpoint), + }, + CommitteeBits: bitfield.Bitvector64(make([]byte, 8)), + } + + phase0AttSSZ := "e4000000" + strings.Repeat("00", 128) + strings.Repeat("00", 96) + "01" + electraAttSSZ := "ec000000" + strings.Repeat("00", 128) + strings.Repeat("00", 8) + strings.Repeat("00", 96) + "01" + + tests := []struct { + name string + value core.VersionedAggregatedAttestation + expected string + }{ + { + name: "phase0_zeros", + value: core.VersionedAggregatedAttestation{VersionedAttestation: eth2spec.VersionedAttestation{Version: eth2spec.DataVersionPhase0, Phase0: phase0Att}}, + expected: "0x" + + "0000000000000000" + // version=0 (phase0) + "0c000000" + // offset=12 + phase0AttSSZ, + }, + { + name: "electra_zeros", + value: core.VersionedAggregatedAttestation{VersionedAttestation: eth2spec.VersionedAttestation{Version: eth2spec.DataVersionElectra, Electra: electraAtt}}, + expected: "0x" + + "0500000000000000" + // version=5 (electra) + "0c000000" + + electraAttSSZ, + }, + { + name: "fulu_zeros", + value: core.VersionedAggregatedAttestation{VersionedAttestation: eth2spec.VersionedAttestation{Version: eth2spec.DataVersionFulu, Fulu: electraAtt}}, + expected: "0x" + + "0600000000000000" + // version=6 (fulu) + "0c000000" + + electraAttSSZ, + }, + { + name: "phase0_non_zero", + // Slot=1000, Index=5, Source.Epoch=10, Target.Epoch=11. + value: core.VersionedAggregatedAttestation{VersionedAttestation: eth2spec.VersionedAttestation{ + Version: eth2spec.DataVersionPhase0, + Phase0: ð2p0.Attestation{ + AggregationBits: bitfield.Bitlist{0x03}, + Data: ð2p0.AttestationData{ + Slot: 1000, + Index: 5, + Source: ð2p0.Checkpoint{Epoch: 10}, + Target: ð2p0.Checkpoint{Epoch: 11}, + }, + }, + }}, + expected: "0x" + + "0000000000000000" + + "0c000000" + + "e4000000" + + "e803000000000000" + // Slot=1000 + "0500000000000000" + // Index=5 + strings.Repeat("00", 32) + + "0a00000000000000" + // Source.Epoch=10 + strings.Repeat("00", 32) + + "0b00000000000000" + // Target.Epoch=11 + strings.Repeat("00", 32) + + strings.Repeat("00", 96) + + "03", // AggBits={0x03} + }, + { + name: "electra_non_zero", + // Slot=2000, Index=3, Source.Epoch=20, Target.Epoch=21. + value: core.VersionedAggregatedAttestation{VersionedAttestation: eth2spec.VersionedAttestation{ + Version: eth2spec.DataVersionElectra, + Electra: &electra.Attestation{ + AggregationBits: bitfield.Bitlist{0x07}, + Data: ð2p0.AttestationData{ + Slot: 2000, + Index: 3, + Source: ð2p0.Checkpoint{Epoch: 20}, + Target: ð2p0.Checkpoint{Epoch: 21}, + }, + CommitteeBits: bitfield.Bitvector64(make([]byte, 8)), + }, + }}, + expected: "0x" + + "0500000000000000" + + "0c000000" + + "ec000000" + + "d007000000000000" + // Slot=2000 + "0300000000000000" + // Index=3 + strings.Repeat("00", 32) + + "1400000000000000" + // Source.Epoch=20 + strings.Repeat("00", 32) + + "1500000000000000" + // Target.Epoch=21 + strings.Repeat("00", 32) + + strings.Repeat("00", 8) + + strings.Repeat("00", 96) + + "07", // AggBits={0x07} + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, err := tt.value.MarshalSSZ() + require.NoError(t, err) + require.Equal(t, tt.expected, fmt.Sprintf("%#x", b)) + + var got core.VersionedAggregatedAttestation + require.NoError(t, got.UnmarshalSSZ(b)) + require.Equal(t, tt.value, got) + }) + } +} + +func TestVersionedSignedAggregateAndProofSSZ(t *testing.T) { + phase0Att := ð2p0.Attestation{ + AggregationBits: bitfield.Bitlist{0x01}, + Data: ð2p0.AttestationData{ + Source: new(eth2p0.Checkpoint), + Target: new(eth2p0.Checkpoint), + }, + } + + electraAtt := &electra.Attestation{ + AggregationBits: bitfield.Bitlist{0x01}, + Data: ð2p0.AttestationData{ + Source: new(eth2p0.Checkpoint), + Target: new(eth2p0.Checkpoint), + }, + CommitteeBits: bitfield.Bitvector64(make([]byte, 8)), + } + + saapPrefix := "64000000" + // Message offset=100 within SAAP + strings.Repeat("00", 96) // Signature (zero) + + aapPrefixZero := "0000000000000000" + // AggregatorIndex=0 + "6c000000" + // Aggregate offset=108 within AAP + strings.Repeat("00", 96) // SelectionProof (zero) + + phase0AttSSZ := "e4000000" + strings.Repeat("00", 128) + strings.Repeat("00", 96) + "01" + electraAttSSZ := "ec000000" + strings.Repeat("00", 128) + strings.Repeat("00", 8) + strings.Repeat("00", 96) + "01" + + tests := []struct { + name string + value core.VersionedSignedAggregateAndProof + expected string + }{ + { + name: "phase0_zeros", + value: core.VersionedSignedAggregateAndProof{VersionedSignedAggregateAndProof: eth2spec.VersionedSignedAggregateAndProof{Version: eth2spec.DataVersionPhase0, Phase0: ð2p0.SignedAggregateAndProof{Message: ð2p0.AggregateAndProof{Aggregate: phase0Att}}}}, + // version(8) + offset=12(4) + SignedAggregateAndProof(437) + expected: "0x" + + "0000000000000000" + // version=0 (phase0) + "0c000000" + // outer offset=12 + saapPrefix + aapPrefixZero + phase0AttSSZ, + }, + { + name: "electra_zeros", + value: core.VersionedSignedAggregateAndProof{VersionedSignedAggregateAndProof: eth2spec.VersionedSignedAggregateAndProof{Version: eth2spec.DataVersionElectra, Electra: &electra.SignedAggregateAndProof{Message: &electra.AggregateAndProof{Aggregate: electraAtt}}}}, + // version(8) + offset=12(4) + SignedAggregateAndProof(445) + expected: "0x" + + "0500000000000000" + // version=5 (electra) + "0c000000" + + saapPrefix + aapPrefixZero + electraAttSSZ, + }, + { + name: "fulu_zeros", + value: core.VersionedSignedAggregateAndProof{VersionedSignedAggregateAndProof: eth2spec.VersionedSignedAggregateAndProof{Version: eth2spec.DataVersionFulu, Fulu: &electra.SignedAggregateAndProof{Message: &electra.AggregateAndProof{Aggregate: electraAtt}}}}, + expected: "0x" + + "0600000000000000" + // version=6 (fulu) + "0c000000" + + saapPrefix + aapPrefixZero + electraAttSSZ, + }, + { + name: "deneb_zeros", + value: core.VersionedSignedAggregateAndProof{VersionedSignedAggregateAndProof: eth2spec.VersionedSignedAggregateAndProof{Version: eth2spec.DataVersionDeneb, Deneb: ð2p0.SignedAggregateAndProof{Message: ð2p0.AggregateAndProof{Aggregate: phase0Att}}}}, + expected: "0x" + + "0400000000000000" + // version=4 (deneb) + "0c000000" + + saapPrefix + aapPrefixZero + phase0AttSSZ, + }, + { + name: "phase0_aggregator_index_7", + value: core.VersionedSignedAggregateAndProof{VersionedSignedAggregateAndProof: eth2spec.VersionedSignedAggregateAndProof{ + Version: eth2spec.DataVersionPhase0, + Phase0: ð2p0.SignedAggregateAndProof{ + Message: ð2p0.AggregateAndProof{ + AggregatorIndex: 7, + Aggregate: ð2p0.Attestation{ + AggregationBits: bitfield.Bitlist{0x03}, + Data: ð2p0.AttestationData{ + Slot: 1000, + Index: 5, + Source: ð2p0.Checkpoint{Epoch: 10}, + Target: ð2p0.Checkpoint{Epoch: 11}, + }, + }, + }, + }, + }}, + expected: "0x" + + "0000000000000000" + // version=0 + "0c000000" + // outer offset=12 + saapPrefix + // SAAP fixed region + "0700000000000000" + // AggregatorIndex=7 + "6c000000" + // Aggregate offset=108 + strings.Repeat("00", 96) + // SelectionProof (zero) + "e4000000" + // AggBits offset=228 + "e803000000000000" + // Data.Slot=1000 + "0500000000000000" + // Data.Index=5 + strings.Repeat("00", 32) + // BeaconBlockRoot + "0a00000000000000" + // Source.Epoch=10 + strings.Repeat("00", 32) + // Source.Root + "0b00000000000000" + // Target.Epoch=11 + strings.Repeat("00", 32) + // Target.Root + strings.Repeat("00", 96) + // Att.Signature + "03", // AggBits={0x03} + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, err := tt.value.MarshalSSZ() + require.NoError(t, err) + require.Equal(t, tt.expected, fmt.Sprintf("%#x", b)) + + var got core.VersionedSignedAggregateAndProof + require.NoError(t, got.UnmarshalSSZ(b)) + require.Equal(t, tt.value, got) + }) + } +} + +func TestVersionedSignedProposalSSZ(t *testing.T) { + phase0Block := ð2p0.SignedBeaconBlock{ + Message: ð2p0.BeaconBlock{ + Body: ð2p0.BeaconBlockBody{ + ETH1Data: ð2p0.ETH1Data{BlockHash: make([]byte, 32)}, + }, + }, + } + + altairBlock := &altair.SignedBeaconBlock{ + Message: &altair.BeaconBlock{ + Body: &altair.BeaconBlockBody{ + ETH1Data: ð2p0.ETH1Data{BlockHash: make([]byte, 32)}, + SyncAggregate: &altair.SyncAggregate{ + SyncCommitteeBits: bitfield.Bitvector512(make([]byte, 64)), + }, + }, + }, + } + + electraBlindedBlock := ð2electra.SignedBlindedBeaconBlock{ + Message: ð2electra.BlindedBeaconBlock{ + Body: ð2electra.BlindedBeaconBlockBody{ + ETH1Data: ð2p0.ETH1Data{BlockHash: make([]byte, 32)}, + SyncAggregate: &altair.SyncAggregate{ + SyncCommitteeBits: bitfield.Bitvector512(make([]byte, 64)), + }, + ExecutionPayloadHeader: testutil.ZeroDenebExecutionPayloadHeader(), + ExecutionRequests: new(electra.ExecutionRequests), + }, + }, + } + + denebBlock := ð2deneb.SignedBlockContents{ + SignedBlock: &deneb.SignedBeaconBlock{ + Message: &deneb.BeaconBlock{ + Body: &deneb.BeaconBlockBody{ + ETH1Data: ð2p0.ETH1Data{BlockHash: make([]byte, 32)}, + SyncAggregate: &altair.SyncAggregate{ + SyncCommitteeBits: bitfield.Bitvector512(make([]byte, 64)), + }, + ExecutionPayload: testutil.ZeroDenebExecutionPayload(), + }, + }, + }, + } + + tests := []struct { + name string + apiProp eth2api.VersionedSignedProposal + expected string + }{ + { + name: "phase0", + apiProp: eth2api.VersionedSignedProposal{Version: eth2spec.DataVersionPhase0, Phase0: phase0Block}, + // Header: version=0, blinded=false, offset=13. Inner=phase0.SignedBeaconBlock (404 bytes). + expected: "0x0000000000000000000d000000640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000540000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc000000dc000000dc000000dc000000dc000000", + }, + { + name: "altair", + apiProp: eth2api.VersionedSignedProposal{Version: eth2spec.DataVersionAltair, Altair: altairBlock}, + // Header: version=1, blinded=false, offset=13. Inner=altair.SignedBeaconBlock (564 bytes). + expected: "0x0100000000000000000d0000006400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007c0100007c0100007c0100007c0100007c01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + }, + { + name: "electra_blinded", + apiProp: eth2api.VersionedSignedProposal{Version: eth2spec.DataVersionElectra, ElectraBlinded: electraBlindedBlock, Blinded: true}, + // Header: version=5, blinded=true, offset=13. + expected: "0x0500000000000000010d0000006400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008c0100008c0100008c0100008c0100008c010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008c010000d4030000d4030000d403000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000480200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000c0000000c000000", + }, + { + name: "fulu_blinded", + apiProp: eth2api.VersionedSignedProposal{Version: eth2spec.DataVersionFulu, FuluBlinded: electraBlindedBlock, Blinded: true}, + // Header: version=6, blinded=true, offset=13. Inner same structure as electra_blinded. + expected: "0x0600000000000000010d0000006400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008c0100008c0100008c0100008c0100008c010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008c010000d4030000d4030000d403000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000480200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000c0000000c000000", + }, + { + name: "deneb", + apiProp: eth2api.VersionedSignedProposal{Version: eth2spec.DataVersionDeneb, Deneb: denebBlock}, + // Header: version=4, blinded=false, offset=13. Inner=eth2deneb.SignedBlockContents (1116 bytes). + expected: "0x0400000000000000000d0000000c0000005c0400005c040000640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000540000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000880100008801000088010000880100008801000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000880100009803000098030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100200001002000000000000000000000000000000000000", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p, err := core.NewVersionedSignedProposal(&tt.apiProp) + require.NoError(t, err) + + b, err := p.MarshalSSZ() + require.NoError(t, err) + require.Equal(t, tt.expected, fmt.Sprintf("%#x", b)) + + p2 := new(core.VersionedSignedProposal) + require.NoError(t, p2.UnmarshalSSZ(b)) + }) + } +} + +func TestVersionedProposalSSZ(t *testing.T) { + phase0Block := ð2p0.BeaconBlock{ + Body: ð2p0.BeaconBlockBody{ + ETH1Data: ð2p0.ETH1Data{BlockHash: make([]byte, 32)}, + }, + } + + altairBlock := &altair.BeaconBlock{ + Body: &altair.BeaconBlockBody{ + ETH1Data: ð2p0.ETH1Data{BlockHash: make([]byte, 32)}, + SyncAggregate: &altair.SyncAggregate{ + SyncCommitteeBits: bitfield.Bitvector512(make([]byte, 64)), + }, + }, + } + + electraBlindedBlock := ð2electra.BlindedBeaconBlock{ + Body: ð2electra.BlindedBeaconBlockBody{ + ETH1Data: ð2p0.ETH1Data{BlockHash: make([]byte, 32)}, + SyncAggregate: &altair.SyncAggregate{ + SyncCommitteeBits: bitfield.Bitvector512(make([]byte, 64)), + }, + ExecutionPayloadHeader: testutil.ZeroDenebExecutionPayloadHeader(), + ExecutionRequests: new(electra.ExecutionRequests), + }, + } + + denebBlock := ð2deneb.BlockContents{ + Block: &deneb.BeaconBlock{ + Body: &deneb.BeaconBlockBody{ + ETH1Data: ð2p0.ETH1Data{BlockHash: make([]byte, 32)}, + SyncAggregate: &altair.SyncAggregate{ + SyncCommitteeBits: bitfield.Bitvector512(make([]byte, 64)), + }, + ExecutionPayload: testutil.ZeroDenebExecutionPayload(), + }, + }, + } + + tests := []struct { + name string + apiProp eth2api.VersionedProposal + expected string + }{ + { + name: "phase0", + apiProp: eth2api.VersionedProposal{Version: eth2spec.DataVersionPhase0, Phase0: phase0Block}, + // Header: version=0, blinded=false, offset=13. Inner=phase0.BeaconBlock (304 bytes). + expected: "0x0000000000000000000d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000540000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc000000dc000000dc000000dc000000dc000000", + }, + { + name: "altair", + apiProp: eth2api.VersionedProposal{Version: eth2spec.DataVersionAltair, Altair: altairBlock}, + // Header: version=1, blinded=false, offset=13. Inner=altair.BeaconBlock (464 bytes). + expected: "0x0100000000000000000d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007c0100007c0100007c0100007c0100007c01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + }, + { + name: "electra_blinded", + apiProp: eth2api.VersionedProposal{Version: eth2spec.DataVersionElectra, ElectraBlinded: electraBlindedBlock, Blinded: true}, + // Header: version=5, blinded=true, offset=13. + expected: "0x0500000000000000010d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008c0100008c0100008c0100008c0100008c010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008c010000d4030000d4030000d403000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000480200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000c0000000c000000", + }, + { + name: "fulu_blinded", + apiProp: eth2api.VersionedProposal{Version: eth2spec.DataVersionFulu, FuluBlinded: electraBlindedBlock, Blinded: true}, + // Header: version=6, blinded=true, offset=13. Inner same structure as electra_blinded. + expected: "0x0600000000000000010d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008c0100008c0100008c0100008c0100008c010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008c010000d4030000d4030000d403000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000480200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000c0000000c000000", + }, + { + name: "deneb", + apiProp: eth2api.VersionedProposal{Version: eth2spec.DataVersionDeneb, Deneb: denebBlock}, + // Header: version=4, blinded=false, offset=13. Inner=eth2deneb.BlockContents (1016 bytes). + expected: "0x0400000000000000000d0000000c000000f8030000f80300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000540000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000880100008801000088010000880100008801000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000880100009803000098030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100200001002000000000000000000000000000000000000", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p, err := core.NewVersionedProposal(&tt.apiProp) + require.NoError(t, err) + + b, err := p.MarshalSSZ() + require.NoError(t, err) + require.Equal(t, tt.expected, fmt.Sprintf("%#x", b)) + + p2 := new(core.VersionedProposal) + require.NoError(t, p2.UnmarshalSSZ(b)) + }) + } +} diff --git a/core/unsigneddata_test.go b/core/unsigneddata_test.go index fcf3ca779..9727e10d7 100644 --- a/core/unsigneddata_test.go +++ b/core/unsigneddata_test.go @@ -3,11 +3,14 @@ package core_test import ( + "encoding/hex" "fmt" "testing" + bitfield "github.com/OffchainLabs/go-bitfield" eth2api "github.com/attestantio/go-eth2-client/api" eth2spec "github.com/attestantio/go-eth2-client/spec" + "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/electra" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/stretchr/testify/require" @@ -492,3 +495,94 @@ func TestVersionedProposal(t *testing.T) { }) } } + +func TestAggregatedAttestationMarshalSSZ(t *testing.T) { + tests := []struct { + name string + value core.AggregatedAttestation + expected string + }{ + { + name: "sentinel_only/slot10", + value: core.NewAggregatedAttestation(ð2p0.Attestation{ + AggregationBits: bitfield.Bitlist{0x01}, + Data: ð2p0.AttestationData{ + Slot: 10, + Index: 0, + Source: new(eth2p0.Checkpoint), + Target: new(eth2p0.Checkpoint), + }, + }), + expected: "0xe40000000a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", + }, + { + name: "one_bit_set/slot42", + value: core.NewAggregatedAttestation(ð2p0.Attestation{ + AggregationBits: bitfield.Bitlist{0x03}, + Data: ð2p0.AttestationData{ + Slot: 42, + Index: 3, + Source: new(eth2p0.Checkpoint), + Target: new(eth2p0.Checkpoint), + }, + }), + expected: "0xe40000002a0000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, err := tt.value.MarshalSSZ() + require.NoError(t, err) + require.Equal(t, tt.expected, "0x"+hex.EncodeToString(b)) + + var got core.AggregatedAttestation + require.NoError(t, got.UnmarshalSSZ(b)) + require.Equal(t, tt.value, got) + }) + } +} + +func TestSyncContributionMarshalSSZ(t *testing.T) { + allBitsSet := bitfield.NewBitvector128() + for i := range 16 { + allBitsSet.SetBitAt(uint64(i), true) + } + + tests := []struct { + name string + value core.SyncContribution + expected string + }{ + { + name: "zeros", + value: core.NewSyncContribution(&altair.SyncCommitteeContribution{ + Slot: 1, + SubcommitteeIndex: 0, + AggregationBits: bitfield.NewBitvector128(), + }), + expected: "0x01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + }, + { + name: "slot100_subcommittee2_all_bits", + value: core.NewSyncContribution(&altair.SyncCommitteeContribution{ + Slot: 100, + SubcommitteeIndex: 2, + AggregationBits: allBitsSet, + }), + expected: "0x640000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000ffff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, err := tt.value.MarshalSSZ() + require.NoError(t, err) + require.Equal(t, tt.expected, "0x"+hex.EncodeToString(b)) + + var got core.SyncContribution + require.NoError(t, got.UnmarshalSSZ(b)) + require.Equal(t, tt.value, got) + }) + } +} diff --git a/core/validatorapi/router_internal_test.go b/core/validatorapi/router_internal_test.go index f799844ed..296c37bf8 100644 --- a/core/validatorapi/router_internal_test.go +++ b/core/validatorapi/router_internal_test.go @@ -40,6 +40,7 @@ import ( "github.com/obolnetwork/charon/app/errors" "github.com/obolnetwork/charon/app/eth2wrap" + "github.com/obolnetwork/charon/core" "github.com/obolnetwork/charon/testutil" ) @@ -2346,6 +2347,90 @@ func (h testHandler) newBeaconHandler(t *testing.T) http.Handler { return mux } +func TestUnmarshalSSZ(t *testing.T) { + phase0Proposal := core.VersionedSignedProposal{ + VersionedSignedProposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionPhase0, + Phase0: ð2p0.SignedBeaconBlock{ + Message: ð2p0.BeaconBlock{ + Slot: 1, + ProposerIndex: 2, + ParentRoot: eth2p0.Root{0x01}, + StateRoot: eth2p0.Root{0x02}, + Body: ð2p0.BeaconBlockBody{ + RANDAOReveal: eth2p0.BLSSignature{0xAA}, + ETH1Data: ð2p0.ETH1Data{ + DepositRoot: eth2p0.Root{0x03}, + DepositCount: 5, + BlockHash: append([]byte{0x04}, make([]byte, 31)...), + }, + Graffiti: [32]byte{0x05}, + ProposerSlashings: []*eth2p0.ProposerSlashing{}, + AttesterSlashings: []*eth2p0.AttesterSlashing{}, + Attestations: []*eth2p0.Attestation{}, + Deposits: []*eth2p0.Deposit{}, + VoluntaryExits: []*eth2p0.SignedVoluntaryExit{}, + }, + }, + Signature: eth2p0.BLSSignature{0xBB}, + }, + }, + } + + tests := []struct { + name string + proposal *core.VersionedSignedProposal + errBytes []byte + errMsg string + }{ + { + name: "phase0", + proposal: &phase0Proposal, + }, + { + name: "empty_body", + errBytes: []byte{}, + errMsg: "empty request body", + }, + { + name: "truncated_body", + errBytes: []byte{0x01, 0x02}, + errMsg: "failed parsing ssz request body", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var b []byte + if tt.errBytes != nil { + b = tt.errBytes + } else { + var err error + + b, err = ssz.MarshalSSZ(tt.proposal) + require.NoError(t, err) + } + + got := new(core.VersionedSignedProposal) + err := unmarshal(contentTypeSSZ, b, got) + + if tt.errMsg != "" { + require.ErrorContains(t, err, tt.errMsg) + return + } + + require.NoError(t, err) + require.Equal(t, *tt.proposal, *got) + }) + } + + // Types without SSZ support must return the appropriate error. + t.Run("no_ssz_support", func(t *testing.T) { + err := unmarshal(contentTypeSSZ, []byte{0x01}, new(struct{})) + require.ErrorContains(t, err, "internal type doesn't support ssz unmarshalling") + }) +} + // nest returns a json nested version the data objected. Note nests must be provided in inverse order. func nest(data any, nests ...string) any { res := data diff --git a/dkg/pedersen/reshare_internal_test.go b/dkg/pedersen/reshare_internal_test.go index 430666634..309631205 100644 --- a/dkg/pedersen/reshare_internal_test.go +++ b/dkg/pedersen/reshare_internal_test.go @@ -240,3 +240,39 @@ func TestGenerateNonce(t *testing.T) { require.NotEqual(t, nonce2, nonce3) } + +func TestGenerateNonceDeterminism(t *testing.T) { + suite := kbls.NewBLS12381Suite().G1().(kdkg.Suite) + _, pub1 := randomKeyPair(suite) + _, pub2 := randomKeyPair(suite) + _, pub3 := randomKeyPair(suite) + + oneNode := []kdkg.Node{{Index: 1, Public: pub1}} + twoNodes := []kdkg.Node{{Index: 1, Public: pub1}, {Index: 2, Public: pub2}} + threeNodes := []kdkg.Node{{Index: 1, Public: pub1}, {Index: 2, Public: pub2}, {Index: 3, Public: pub3}} + + tests := []struct { + name string + nodes []kdkg.Node + iteration int + }{ + {name: "one_node_iter0", nodes: oneNode, iteration: 0}, + {name: "one_node_iter1", nodes: oneNode, iteration: 1}, + {name: "two_nodes_iter0", nodes: twoNodes, iteration: 0}, + {name: "two_nodes_iter5", nodes: twoNodes, iteration: 5}, + {name: "three_nodes_iter0", nodes: threeNodes, iteration: 0}, + {name: "three_nodes_iter99", nodes: threeNodes, iteration: 99}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + nonce1, err := generateNonce(tt.nodes, tt.iteration) + require.NoError(t, err) + + nonce2, err := generateNonce(tt.nodes, tt.iteration) + require.NoError(t, err) + + require.Equal(t, nonce1, nonce2, "same inputs must produce the same nonce") + }) + } +} diff --git a/eth2util/hash_test.go b/eth2util/hash_test.go index 28bab9854..88b61ed6b 100644 --- a/eth2util/hash_test.go +++ b/eth2util/hash_test.go @@ -4,18 +4,48 @@ package eth2util_test import ( "encoding/hex" + "math" "testing" + eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/stretchr/testify/require" "github.com/obolnetwork/charon/eth2util" ) func TestSlotHashRoot(t *testing.T) { - resp, err := eth2util.SlotHashRoot(2) - require.NoError(t, err) - require.Equal(t, - "0200000000000000000000000000000000000000000000000000000000000000", - hex.EncodeToString(resp[:]), - ) + tests := []struct { + name string + slot eth2p0.Slot + expected string + }{ + { + name: "zero", + slot: 0, + expected: "0000000000000000000000000000000000000000000000000000000000000000", + }, + { + name: "one", + slot: 1, + expected: "0100000000000000000000000000000000000000000000000000000000000000", + }, + { + name: "epoch_boundary", + slot: 32000, + expected: "007d000000000000000000000000000000000000000000000000000000000000", + }, + { + name: "max_uint64", + slot: math.MaxUint64, + expected: "ffffffffffffffff000000000000000000000000000000000000000000000000", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := eth2util.SlotHashRoot(tt.slot) + require.NoError(t, err) + require.Equal(t, tt.expected, hex.EncodeToString(got[:])) + }) + } } diff --git a/eth2util/helper_capella_internal_test.go b/eth2util/helper_capella_internal_test.go new file mode 100644 index 000000000..5f4a5a7af --- /dev/null +++ b/eth2util/helper_capella_internal_test.go @@ -0,0 +1,52 @@ +// Copyright © 2022-2026 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package eth2util + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestForkDataTypeHashTreeRoot(t *testing.T) { + tests := []struct { + name string + input forkDataType + expected string + }{ + { + name: "zeros", + input: forkDataType{}, + expected: "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", + }, + { + name: "version_only", + input: forkDataType{ + CurrentVersion: [4]byte{0x01, 0x02, 0x03, 0x04}, + }, + expected: "ffd2fc34e5796a643f749b0b2b908c4ca3ce58ce24a00c49329a2dc0b54e47c6", + }, + { + name: "both_set", + input: forkDataType{ + CurrentVersion: [4]byte{0xAB, 0xCD, 0xEF, 0x01}, + GenesisValidatorsRoot: [32]byte{ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, + }, + }, + expected: "7814fe240599c38bbd9899a02e477beb8c20c9e02ca29ba048f3d9eacb84e658", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.input.HashTreeRoot() + require.NoError(t, err) + require.Equal(t, tt.expected, hex.EncodeToString(got[:])) + }) + } +} diff --git a/eth2util/types_test.go b/eth2util/types_test.go index 2fa73a72c..53a421561 100644 --- a/eth2util/types_test.go +++ b/eth2util/types_test.go @@ -17,15 +17,34 @@ import ( "github.com/obolnetwork/charon/testutil" ) -func TestEpochHashRoot(t *testing.T) { - epoch := eth2util.SignedEpoch{Epoch: 2} - - resp, err := epoch.HashTreeRoot() - require.NoError(t, err) - require.Equal(t, - "0200000000000000000000000000000000000000000000000000000000000000", - hex.EncodeToString(resp[:]), - ) +func TestSignedEpochHashTreeRoot(t *testing.T) { + tests := []struct { + name string + epoch eth2p0.Epoch + expected string + }{ + { + name: "epoch_12345", + epoch: 12345, + expected: "3930000000000000000000000000000000000000000000000000000000000000", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Signature is not included in the hash; verify same epoch with different + // signatures produces the same hash root. + se1 := eth2util.SignedEpoch{Epoch: tt.epoch} + se2 := eth2util.SignedEpoch{Epoch: tt.epoch, Signature: eth2p0.BLSSignature(testutil.RandomBytes96())} + + got1, err := se1.HashTreeRoot() + require.NoError(t, err) + require.Equal(t, tt.expected, hex.EncodeToString(got1[:])) + + got2, err := se2.HashTreeRoot() + require.NoError(t, err) + require.Equal(t, got1, got2) + }) + } } func TestUnmarshallingSignedEpoch(t *testing.T) { diff --git a/go.mod b/go.mod index c2c394157..8ed12e81e 100644 --- a/go.mod +++ b/go.mod @@ -85,6 +85,7 @@ require ( github.com/bufbuild/buf v1.58.0 // indirect github.com/bufbuild/protocompile v0.14.1 // indirect github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 // indirect + github.com/casbin/govaluate v1.8.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chigopher/pathlib v0.19.1 // indirect @@ -153,7 +154,7 @@ require ( github.com/jinzhu/copier v0.4.0 // indirect github.com/kilic/bls12-381 v0.1.0 // indirect github.com/klauspost/compress v1.18.0 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/koron/go-ssdp v0.0.6 // indirect github.com/kr/pretty v0.3.1 // indirect @@ -218,7 +219,8 @@ require ( github.com/pion/transport/v4 v4.0.1 // indirect github.com/pion/turn/v4 v4.0.2 // indirect github.com/pion/webrtc/v4 v4.1.2 // indirect - github.com/pk910/dynamic-ssz v0.0.6 // indirect + github.com/pk910/dynamic-ssz v1.2.2 // indirect + github.com/pk910/hashtree-bindings v0.0.1 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -275,7 +277,6 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect google.golang.org/grpc v1.79.2 // indirect - gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index a82166e81..aefd353e7 100644 --- a/go.sum +++ b/go.sum @@ -66,6 +66,8 @@ github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/ github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 h1:V1xulAoqLqVg44rY97xOR+mQpD2N+GzhMHVwJ030WEU= github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ= +github.com/casbin/govaluate v1.8.0 h1:1dUaV/I0LFP2tcY1uNQEb6wBCbp8GMTcC/zhwQDWvZo= +github.com/casbin/govaluate v1.8.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= @@ -283,8 +285,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/koron/go-ssdp v0.0.6 h1:Jb0h04599eq/CY7rB5YEqPS83HmRfHP2azkxMN2rFtU= @@ -461,8 +463,10 @@ github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps= github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs= github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54= github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U= -github.com/pk910/dynamic-ssz v0.0.6 h1:Tu97LSc2TtCyqRfoSbhG9XuR/FbA7CkKeAnlkgUydFY= -github.com/pk910/dynamic-ssz v0.0.6/go.mod h1:b6CrLaB2X7pYA+OSEEbkgXDEcRnjLOZIxZTsMuO/Y9c= +github.com/pk910/dynamic-ssz v1.2.2 h1:dyvewnBFKGJQUVQjGhS0+LdX95xhFRh7+d7hIq3OnvQ= +github.com/pk910/dynamic-ssz v1.2.2/go.mod h1:HXRWLNcgj3DL65Kznrb+RdL3DEKw2JBZ/6crooqGoII= +github.com/pk910/hashtree-bindings v0.0.1 h1:Sw+UlPlrBle4LUg04kqLFybVQcfmamwKL1QsrR3GU0g= +github.com/pk910/hashtree-bindings v0.0.1/go.mod h1:eayIpxMFkWzMsydESu/5bV8wglZzSE/c9mq6DQdn204= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -768,8 +772,6 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc= -gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/testutil/random.go b/testutil/random.go index c566dc0a8..1769d7291 100644 --- a/testutil/random.go +++ b/testutil/random.go @@ -1151,6 +1151,24 @@ func RandomDenebExecutionPayloadHeader() *deneb.ExecutionPayloadHeader { } } +// ZeroDenebExecutionPayloadHeader returns a deneb ExecutionPayloadHeader with all-zero values. +func ZeroDenebExecutionPayloadHeader() *deneb.ExecutionPayloadHeader { + return &deneb.ExecutionPayloadHeader{ + BaseFeePerGas: new(uint256.Int), + ExtraData: []byte{}, + } +} + +// ZeroDenebExecutionPayload returns a deneb ExecutionPayload with all-zero values. +func ZeroDenebExecutionPayload() *deneb.ExecutionPayload { + return &deneb.ExecutionPayload{ + BaseFeePerGas: new(uint256.Int), + ExtraData: []byte{}, + Transactions: []bellatrix.Transaction{}, + Withdrawals: []*capella.Withdrawal{}, + } +} + func RandomAttestationDuty(t *testing.T) *eth2v1.AttesterDuty { t.Helper() return RandomAttestationDutySeed(t, NewSeedRand())