Eip4881: Tests (#11754)

This commit is contained in:
Sammy Rosso 2023-03-14 16:29:48 +01:00 committed by GitHub
parent cc764c346b
commit 8aec170f9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 591 additions and 9 deletions

View File

@ -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"

View File

@ -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",
],
)

View File

@ -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
}

View File

@ -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.

View File

@ -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)
}
})
}
}

View File

@ -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)
}
})
}
}

View File

@ -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(&eth.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(&eth.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(&eth.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)
}

2
go.mod
View File

@ -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