From 8aec170f9b65252c274aa76775d9a252705c0936 Mon Sep 17 00:00:00 2001 From: Sammy Rosso <15244892+saolyn@users.noreply.github.com> Date: Tue, 14 Mar 2023 16:29:48 +0100 Subject: [PATCH] Eip4881: Tests (#11754) --- WORKSPACE | 15 + .../cache/depositsnapshot/BUILD.bazel | 24 +- .../cache/depositsnapshot/deposit_tree.go | 7 +- .../depositsnapshot/deposit_tree_snapshot.go | 2 +- .../deposit_tree_snapshot_test.go | 54 +++ .../cache/depositsnapshot/merkle_tree_test.go | 141 +++++++ .../cache/depositsnapshot/spec_test.go | 355 ++++++++++++++++++ go.mod | 2 +- 8 files changed, 591 insertions(+), 9 deletions(-) create mode 100644 beacon-chain/cache/depositsnapshot/deposit_tree_snapshot_test.go create mode 100644 beacon-chain/cache/depositsnapshot/merkle_tree_test.go create mode 100644 beacon-chain/cache/depositsnapshot/spec_test.go diff --git a/WORKSPACE b/WORKSPACE index d224e8739..e498355a7 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -190,6 +190,21 @@ filegroup( url = "https://github.com/eth-clients/slashing-protection-interchange-tests/archive/b8413ca42dc92308019d0d4db52c87e9e125c4e9.tar.gz", ) +http_archive( + name = "eip4881_spec_tests", + build_file_content = """ +filegroup( + name = "test_data", + srcs = glob([ + "**/*.yaml", + ]), + visibility = ["//visibility:public"], +) + """, + sha256 = "89cb659498c0d196fc9f957f8b849b2e1a5c041c3b2b3ae5432ac5c26944297e", + url = "https://github.com/ethereum/EIPs/archive/5480440fe51742ed23342b68cf106cefd427e39d.tar.gz", +) + consensus_spec_version = "v1.3.0-rc.3" bls_test_version = "v0.1.1" diff --git a/beacon-chain/cache/depositsnapshot/BUILD.bazel b/beacon-chain/cache/depositsnapshot/BUILD.bazel index 81c531e40..412a32e26 100644 --- a/beacon-chain/cache/depositsnapshot/BUILD.bazel +++ b/beacon-chain/cache/depositsnapshot/BUILD.bazel @@ -1,4 +1,4 @@ -load("@prysm//tools/go:def.bzl", "go_library") +load("@prysm//tools/go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", @@ -19,3 +19,25 @@ go_library( "@com_github_pkg_errors//:go_default_library", ], ) + +go_test( + name = "go_default_test", + srcs = [ + "deposit_tree_snapshot_test.go", + "merkle_tree_test.go", + "spec_test.go", + ], + data = [ + "@eip4881_spec_tests//:test_data", + ], + embed = [":go_default_library"], + deps = [ + "//io/file:go_default_library", + "//proto/eth/v1:go_default_library", + "//testing/assert:go_default_library", + "//testing/require:go_default_library", + "@com_github_pkg_errors//:go_default_library", + "@in_gopkg_yaml_v3//:go_default_library", + "@io_bazel_rules_go//go/tools/bazel:go_default_library", + ], +) diff --git a/beacon-chain/cache/depositsnapshot/deposit_tree.go b/beacon-chain/cache/depositsnapshot/deposit_tree.go index 7eca7b2fb..e006f0f7d 100644 --- a/beacon-chain/cache/depositsnapshot/deposit_tree.go +++ b/beacon-chain/cache/depositsnapshot/deposit_tree.go @@ -23,8 +23,6 @@ var ( ErrInvalidIndex = errors.New("index should be greater than finalizedDeposits - 1") // ErrNoDeposits occurs when the number of deposits is 0. ErrNoDeposits = errors.New("number of deposits should be greater than 0") - // ErrNoFinalizedDeposits occurs when the number of finalized deposits is 0. - ErrNoFinalizedDeposits = errors.New("number of finalized deposits should be greater than 0") // ErrTooManyDeposits occurs when the number of deposits exceeds the capacity of the tree. ErrTooManyDeposits = errors.New("number of deposits should not be greater than the capacity of the tree") ) @@ -62,7 +60,7 @@ func (d *DepositTree) getSnapshot() (DepositTreeSnapshot, error) { return DepositTreeSnapshot{}, ErrEmptyExecutionBlock } var finalized [][32]byte - depositCount, _ := d.tree.GetFinalized(finalized) + depositCount, finalized := d.tree.GetFinalized(finalized) return fromTreeParts(finalized, depositCount, d.finalizedExecutionBlock) } @@ -119,9 +117,6 @@ func (d *DepositTree) getProof(index uint64) ([32]byte, [][32]byte, error) { return [32]byte{}, nil, ErrInvalidMixInLength } finalizedDeposits, _ := d.tree.GetFinalized([][32]byte{}) - if finalizedDeposits == 0 { - return [32]byte{}, nil, ErrNoFinalizedDeposits - } if finalizedDeposits != 0 { finalizedDeposits = finalizedDeposits - 1 } diff --git a/beacon-chain/cache/depositsnapshot/deposit_tree_snapshot.go b/beacon-chain/cache/depositsnapshot/deposit_tree_snapshot.go index 3b35ac344..6aad60e00 100644 --- a/beacon-chain/cache/depositsnapshot/deposit_tree_snapshot.go +++ b/beacon-chain/cache/depositsnapshot/deposit_tree_snapshot.go @@ -40,7 +40,7 @@ func (ds *DepositTreeSnapshot) CalculateRoot() ([32]byte, error) { } size >>= 1 } - return sha256.Sum256(append(root[:], bytesutil.Uint64ToBytesLittleEndian(ds.depositCount)...)), nil + return sha256.Sum256(append(root[:], bytesutil.Uint64ToBytesLittleEndian32(ds.depositCount)...)), nil } // fromTreeParts constructs the deposit tree from pre-existing data. diff --git a/beacon-chain/cache/depositsnapshot/deposit_tree_snapshot_test.go b/beacon-chain/cache/depositsnapshot/deposit_tree_snapshot_test.go new file mode 100644 index 000000000..78236a560 --- /dev/null +++ b/beacon-chain/cache/depositsnapshot/deposit_tree_snapshot_test.go @@ -0,0 +1,54 @@ +package depositsnapshot + +import ( + "fmt" + "reflect" + "testing" + + "github.com/prysmaticlabs/prysm/v3/testing/require" +) + +func TestDepositTreeSnapshot_CalculateRoot(t *testing.T) { + tests := []struct { + name string + finalized int + depositCount uint64 + want [32]byte + }{ + { + name: "empty", + finalized: 0, + depositCount: 0, + want: [32]byte{215, 10, 35, 71, 49, 40, 92, 104, 4, 194, 164, 245, 103, 17, 221, 184, 200, 44, 153, 116, 15, 32, 120, 84, 137, 16, 40, 175, 52, 226, 126, 94}, + }, + { + name: "1 Finalized", + finalized: 1, + depositCount: 2, + want: [32]byte{36, 118, 154, 57, 217, 109, 145, 116, 238, 1, 207, 59, 187, 28, 69, 187, 70, 55, 153, 180, 15, 150, 37, 72, 140, 36, 109, 154, 212, 202, 47, 59}, + }, + { + name: "many finalised", + finalized: 6, + depositCount: 20, + want: [32]byte{210, 63, 57, 119, 12, 5, 3, 25, 139, 20, 244, 59, 114, 119, 35, 88, 222, 88, 122, 106, 239, 20, 45, 140, 99, 92, 222, 166, 133, 159, 128, 72}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var finalized [][32]byte + for i := 0; i < tt.finalized; i++ { + finalized = append(finalized, hexString(t, fmt.Sprintf("%064d", i))) + } + ds := &DepositTreeSnapshot{ + finalized: finalized, + depositCount: tt.depositCount, + } + root, err := ds.CalculateRoot() + require.NoError(t, err) + if got := root; !reflect.DeepEqual(got, tt.want) { + require.DeepEqual(t, tt.want, got) + } + }) + } +} diff --git a/beacon-chain/cache/depositsnapshot/merkle_tree_test.go b/beacon-chain/cache/depositsnapshot/merkle_tree_test.go new file mode 100644 index 000000000..d28bb0491 --- /dev/null +++ b/beacon-chain/cache/depositsnapshot/merkle_tree_test.go @@ -0,0 +1,141 @@ +package depositsnapshot + +import ( + "encoding/hex" + "fmt" + "reflect" + "testing" + + "github.com/prysmaticlabs/prysm/v3/testing/assert" + "github.com/prysmaticlabs/prysm/v3/testing/require" +) + +func hexString(t *testing.T, hexStr string) [32]byte { + t.Helper() + b, err := hex.DecodeString(hexStr) + require.NoError(t, err) + if len(b) != 32 { + assert.Equal(t, 32, len(b), "bad hash length, expected 32") + } + x := (*[32]byte)(b) + return *x +} + +func Test_create(t *testing.T) { + tests := []struct { + name string + leaves [][32]byte + depth uint64 + want MerkleTreeNode + }{ + { + name: "empty tree", + leaves: nil, + depth: 0, + want: &ZeroNode{}, + }, + { + name: "zero depth", + leaves: [][32]byte{hexString(t, fmt.Sprintf("%064d", 0))}, + depth: 0, + want: &LeafNode{}, + }, + { + name: "depth of 1", + leaves: [][32]byte{hexString(t, fmt.Sprintf("%064d", 0))}, + depth: 1, + want: &InnerNode{&LeafNode{}, &ZeroNode{}}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := create(tt.leaves, tt.depth); !reflect.DeepEqual(got, tt.want) { + require.DeepEqual(t, tt.want, got) + } + }) + } +} + +func Test_fromSnapshotParts(t *testing.T) { + tests := []struct { + name string + finalized [][32]byte + deposits uint64 + level uint64 + want MerkleTreeNode + }{ + { + name: "empty", + finalized: nil, + deposits: 0, + level: 0, + want: &ZeroNode{}, + }, + { + name: "single finalized node", + finalized: [][32]byte{hexString(t, fmt.Sprintf("%064d", 0))}, + deposits: 1, + level: 0, + want: &FinalizedNode{ + depositCount: 1, + hash: [32]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + }, + }, + { + name: "multiple deposits and 1 Finalized", + finalized: [][32]byte{hexString(t, fmt.Sprintf("%064d", 0))}, + deposits: 2, + level: 4, + want: &InnerNode{ + left: &InnerNode{&InnerNode{&FinalizedNode{depositCount: 2, hash: hexString(t, fmt.Sprintf("%064d", 0))}, &ZeroNode{1}}, &ZeroNode{2}}, + right: &ZeroNode{3}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tree, err := fromSnapshotParts(tt.finalized, tt.deposits, tt.level) + require.NoError(t, err) + if got := tree; !reflect.DeepEqual(got, tt.want) { + require.DeepEqual(t, tt.want, got) + } + }) + } +} + +func Test_generateProof(t *testing.T) { + tests := []struct { + name string + leaves uint64 + }{ + { + name: "1 leaf", + leaves: 1, + }, + { + name: "4 leaves", + leaves: 4, + }, + { + name: "10 leaves", + leaves: 10, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testCases, err := readTestCases() + require.NoError(t, err) + tree := New() + for _, c := range testCases[:tt.leaves] { + err = tree.pushLeaf(c.DepositDataRoot) + require.NoError(t, err) + } + for i := uint64(0); i < tt.leaves; i++ { + leaf, proof := generateProof(tree.tree, i, DepositContractDepth) + require.Equal(t, leaf, testCases[i].DepositDataRoot) + calcRoot := merkleRootFromBranch(leaf, proof, i) + require.Equal(t, tree.tree.GetRoot(), calcRoot) + } + }) + } +} diff --git a/beacon-chain/cache/depositsnapshot/spec_test.go b/beacon-chain/cache/depositsnapshot/spec_test.go new file mode 100644 index 000000000..901adb906 --- /dev/null +++ b/beacon-chain/cache/depositsnapshot/spec_test.go @@ -0,0 +1,355 @@ +package depositsnapshot + +import ( + "crypto/sha256" + "encoding/hex" + "strconv" + "strings" + "testing" + + "github.com/bazelbuild/rules_go/go/tools/bazel" + "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/v3/io/file" + eth "github.com/prysmaticlabs/prysm/v3/proto/eth/v1" + "github.com/prysmaticlabs/prysm/v3/testing/require" + "gopkg.in/yaml.v3" +) + +type testCase struct { + DepositData depositData `yaml:"deposit_data"` + DepositDataRoot [32]byte `yaml:"deposit_data_root"` + Eth1Data *eth1Data `yaml:"eth1_data"` + BlockHeight uint64 `yaml:"block_height"` + Snapshot snapshot `yaml:"snapshot"` +} + +func (tc *testCase) UnmarshalYAML(value *yaml.Node) error { + raw := struct { + DepositData depositData `yaml:"deposit_data"` + DepositDataRoot string `yaml:"deposit_data_root"` + Eth1Data *eth1Data `yaml:"eth1_data"` + BlockHeight string `yaml:"block_height"` + Snapshot snapshot `yaml:"snapshot"` + }{} + err := value.Decode(&raw) + if err != nil { + return err + } + tc.DepositDataRoot, err = hexStringToByteArray(raw.DepositDataRoot) + if err != nil { + return err + } + tc.DepositData = raw.DepositData + tc.Eth1Data = raw.Eth1Data + tc.BlockHeight, err = stringToUint64(raw.BlockHeight) + if err != nil { + return err + } + tc.Snapshot = raw.Snapshot + return nil +} + +type depositData struct { + Pubkey []byte `yaml:"pubkey"` + WithdrawalCredentials []byte `yaml:"withdrawal_credentials"` + Amount uint64 `yaml:"amount"` + Signature []byte `yaml:"signature"` +} + +func (dd *depositData) UnmarshalYAML(value *yaml.Node) error { + raw := struct { + Pubkey string `yaml:"pubkey"` + WithdrawalCredentials string `yaml:"withdrawal_credentials"` + Amount string `yaml:"amount"` + Signature string `yaml:"signature"` + }{} + err := value.Decode(&raw) + if err != nil { + return err + } + dd.Pubkey, err = hexStringToBytes(raw.Pubkey) + if err != nil { + return err + } + dd.WithdrawalCredentials, err = hexStringToBytes(raw.WithdrawalCredentials) + if err != nil { + return err + } + dd.Amount, err = strconv.ParseUint(raw.Amount, 10, 64) + if err != nil { + return err + } + dd.Signature, err = hexStringToBytes(raw.Signature) + if err != nil { + return err + } + return nil +} + +type eth1Data struct { + DepositRoot [32]byte `yaml:"deposit_root"` + DepositCount uint64 `yaml:"deposit_count"` + BlockHash [32]byte `yaml:"block_hash"` +} + +func (ed *eth1Data) UnmarshalYAML(value *yaml.Node) error { + raw := struct { + DepositRoot string `yaml:"deposit_root"` + DepositCount string `yaml:"deposit_count"` + BlockHash string `yaml:"block_hash"` + }{} + err := value.Decode(&raw) + if err != nil { + return err + } + ed.DepositRoot, err = hexStringToByteArray(raw.DepositRoot) + if err != nil { + return err + } + ed.DepositCount, err = stringToUint64(raw.DepositCount) + if err != nil { + return err + } + ed.BlockHash, err = hexStringToByteArray(raw.BlockHash) + if err != nil { + return err + } + return nil +} + +type snapshot struct { + DepositTreeSnapshot +} + +func (sd *snapshot) UnmarshalYAML(value *yaml.Node) error { + raw := struct { + Finalized []string `yaml:"finalized"` + DepositRoot string `yaml:"deposit_root"` + DepositCount string `yaml:"deposit_count"` + ExecutionBlockHash string `yaml:"execution_block_hash"` + ExecutionBlockHeight string `yaml:"execution_block_height"` + }{} + err := value.Decode(&raw) + if err != nil { + return err + } + sd.finalized = make([][32]byte, len(raw.Finalized)) + for i, finalized := range raw.Finalized { + sd.finalized[i], err = hexStringToByteArray(finalized) + if err != nil { + return err + } + } + sd.depositRoot, err = hexStringToByteArray(raw.DepositRoot) + if err != nil { + return err + } + sd.depositCount, err = stringToUint64(raw.DepositCount) + if err != nil { + return err + } + sd.executionBlock.Hash, err = hexStringToByteArray(raw.ExecutionBlockHash) + if err != nil { + return err + } + sd.executionBlock.Depth, err = stringToUint64(raw.ExecutionBlockHeight) + if err != nil { + return err + } + return nil +} + +func readTestCases() ([]testCase, error) { + testFolders, err := bazel.ListRunfiles() + if err != nil { + return nil, err + } + for _, ff := range testFolders { + if strings.Contains(ff.ShortPath, "eip4881_spec_tests") && + strings.Contains(ff.ShortPath, "eip-4881/test_cases.yaml") { + enc, err := file.ReadFileAsBytes(ff.Path) + if err != nil { + return nil, err + } + var testCases []testCase + err = yaml.Unmarshal(enc, &testCases) + if err != nil { + return []testCase{}, err + } + return testCases, nil + } + } + return nil, errors.New("spec test file not found") +} + +func TestRead(t *testing.T) { + tcs, err := readTestCases() + require.NoError(t, err) + for _, tc := range tcs { + t.Log(tc) + } +} + +func hexStringToByteArray(s string) (b [32]byte, err error) { + var raw []byte + raw, err = hexStringToBytes(s) + if err != nil { + return + } + if len(raw) != 32 { + err = errors.New("invalid hex string length") + return + } + copy(b[:], raw[:32]) + return +} + +func hexStringToBytes(s string) (b []byte, err error) { + b, err = hex.DecodeString(strings.TrimPrefix(s, "0x")) + return +} + +func stringToUint64(s string) (uint64, error) { + value, err := strconv.ParseUint(s, 10, 32) + if err != nil { + return 0, err + } + return value, nil +} + +func merkleRootFromBranch(leaf [32]byte, branch [][32]byte, index uint64) [32]byte { + root := leaf + for i, l := range branch { + ithBit := (index >> i) & 0x1 + if ithBit == 1 { + root = sha256.Sum256(append(l[:], root[:]...)) + } else { + root = sha256.Sum256(append(root[:], l[:]...)) + } + } + return root +} + +func checkProof(t *testing.T, tree *DepositTree, index uint64) { + leaf, proof, err := tree.getProof(index) + require.NoError(t, err) + calcRoot := merkleRootFromBranch(leaf, proof, index) + require.Equal(t, tree.getRoot(), calcRoot) +} + +func compareProof(t *testing.T, tree1, tree2 *DepositTree, index uint64) { + require.Equal(t, tree1.getRoot(), tree2.getRoot()) + checkProof(t, tree1, index) + checkProof(t, tree2, index) +} + +func cloneFromSnapshot(t *testing.T, snapshot DepositTreeSnapshot, testCases []testCase) *DepositTree { + cp, err := fromSnapshot(snapshot) + require.NoError(t, err) + for _, c := range testCases { + err = cp.pushLeaf(c.DepositDataRoot) + require.NoError(t, err) + } + return &cp +} + +func TestDepositCases(t *testing.T) { + tree := New() + testCases, err := readTestCases() + require.NoError(t, err) + for _, c := range testCases { + err = tree.pushLeaf(c.DepositDataRoot) + require.NoError(t, err) + } +} + +func TestFinalization(t *testing.T) { + tree := New() + testCases, err := readTestCases() + require.NoError(t, err) + for _, c := range testCases[:128] { + err = tree.pushLeaf(c.DepositDataRoot) + require.NoError(t, err) + } + originalRoot := tree.getRoot() + require.DeepEqual(t, testCases[127].Eth1Data.DepositRoot, originalRoot) + err = tree.finalize(ð.Eth1Data{ + DepositRoot: testCases[100].Eth1Data.DepositRoot[:], + DepositCount: testCases[100].Eth1Data.DepositCount, + BlockHash: testCases[100].Eth1Data.BlockHash[:], + }, testCases[100].BlockHeight) + require.NoError(t, err) + // ensure finalization doesn't change root + require.Equal(t, tree.getRoot(), originalRoot) + snapshotData, err := tree.getSnapshot() + require.NoError(t, err) + require.DeepEqual(t, testCases[100].Snapshot.DepositTreeSnapshot, snapshotData) + // create a copy of the tree from a snapshot by replaying + // the deposits after the finalized deposit + cp := cloneFromSnapshot(t, snapshotData, testCases[101:128]) + // ensure original and copy have the same root + require.Equal(t, tree.getRoot(), cp.getRoot()) + // finalize original again to check double finalization + err = tree.finalize(ð.Eth1Data{ + DepositRoot: testCases[105].Eth1Data.DepositRoot[:], + DepositCount: testCases[105].Eth1Data.DepositCount, + BlockHash: testCases[105].Eth1Data.BlockHash[:], + }, testCases[105].BlockHeight) + require.NoError(t, err) + // root should still be the same + require.Equal(t, originalRoot, tree.getRoot()) + // create a copy of the tree by taking a snapshot again + snapshotData, err = tree.getSnapshot() + require.NoError(t, err) + cp = cloneFromSnapshot(t, snapshotData, testCases[106:128]) + // create a copy of the tree by replaying ALL deposits from nothing + fullTreeCopy := New() + for _, c := range testCases[:128] { + err = fullTreeCopy.pushLeaf(c.DepositDataRoot) + require.NoError(t, err) + } + for i := 106; i < 128; i++ { + compareProof(t, tree, cp, uint64(i)) + compareProof(t, tree, fullTreeCopy, uint64(i)) + } +} + +func TestSnapshotCases(t *testing.T) { + tree := New() + testCases, err := readTestCases() + require.NoError(t, err) + for _, c := range testCases { + err = tree.pushLeaf(c.DepositDataRoot) + require.NoError(t, err) + } + for _, c := range testCases { + err = tree.finalize(ð.Eth1Data{ + DepositRoot: c.Eth1Data.DepositRoot[:], + DepositCount: c.Eth1Data.DepositCount, + BlockHash: c.Eth1Data.BlockHash[:], + }, c.BlockHeight) + require.NoError(t, err) + s, err := tree.getSnapshot() + require.NoError(t, err) + require.DeepEqual(t, c.Snapshot.DepositTreeSnapshot, s) + } +} + +func TestEmptyTreeSnapshot(t *testing.T) { + _, err := New().getSnapshot() + require.ErrorContains(t, "empty execution block", err) +} + +func TestInvalidSnapshot(t *testing.T) { + invalidSnapshot := DepositTreeSnapshot{ + finalized: nil, + depositRoot: Zerohashes[0], + depositCount: 0, + executionBlock: executionBlock{ + Hash: Zerohashes[0], + Depth: 0, + }, + } + _, err := fromSnapshot(invalidSnapshot) + require.ErrorContains(t, "snapshot root is invalid", err) +} diff --git a/go.mod b/go.mod index 5cf9fa292..b9be31121 100644 --- a/go.mod +++ b/go.mod @@ -91,6 +91,7 @@ require ( google.golang.org/protobuf v1.28.1 gopkg.in/d4l3k/messagediff.v1 v1.2.1 gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.1 k8s.io/client-go v0.18.3 ) @@ -230,7 +231,6 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apimachinery v0.18.3 // indirect k8s.io/klog v1.0.0 // indirect lukechampine.com/blake3 v1.1.7 // indirect