diff --git a/encoding/ssz/BUILD.bazel b/encoding/ssz/BUILD.bazel index 3f3c6641d86b..330a2658e760 100644 --- a/encoding/ssz/BUILD.bazel +++ b/encoding/ssz/BUILD.bazel @@ -40,6 +40,7 @@ go_test( "//config/fieldparams:go_default_library", "//config/params:go_default_library", "//crypto/hash:go_default_library", + "//encoding/bytesutil:go_default_library", "//proto/engine/v1:go_default_library", "//proto/prysm/v1alpha1:go_default_library", "//testing/assert:go_default_library", diff --git a/encoding/ssz/htrutils.go b/encoding/ssz/htrutils.go index c7e42cdf4a35..f610d2c3a1ae 100644 --- a/encoding/ssz/htrutils.go +++ b/encoding/ssz/htrutils.go @@ -141,6 +141,56 @@ func WithdrawalSliceRoot(withdrawals []*enginev1.Withdrawal, limit uint64) ([32] return MixInLength(bytesRoot, bytesRootBufRoot), nil } +// DepositReceiptSliceRoot computes the HTR of a slice of deposit receipts. +// The limit parameter is used as input to the bitwise merkleization algorithm. +func DepositReceiptSliceRoot(depositReceipts []*enginev1.DepositReceipt, limit uint64) ([32]byte, error) { + roots := make([][32]byte, len(depositReceipts)) + for i := 0; i < len(depositReceipts); i++ { + r, err := depositReceipts[i].HashTreeRoot() + if err != nil { + return [32]byte{}, err + } + roots[i] = r + } + + bytesRoot, err := BitwiseMerkleize(roots, uint64(len(roots)), limit) + if err != nil { + return [32]byte{}, errors.Wrap(err, "could not compute merkleization") + } + bytesRootBuf := new(bytes.Buffer) + if err := binary.Write(bytesRootBuf, binary.LittleEndian, uint64(len(depositReceipts))); err != nil { + return [32]byte{}, errors.Wrap(err, "could not marshal length") + } + bytesRootBufRoot := make([]byte, 32) + copy(bytesRootBufRoot, bytesRootBuf.Bytes()) + return MixInLength(bytesRoot, bytesRootBufRoot), nil +} + +// WithdrawalRequestSliceRoot computes the HTR of a slice of withdrawal requests from the EL. +// The limit parameter is used as input to the bitwise merkleization algorithm. +func WithdrawalRequestSliceRoot(withdrawalRequests []*enginev1.ExecutionLayerWithdrawalRequest, limit uint64) ([32]byte, error) { + roots := make([][32]byte, len(withdrawalRequests)) + for i := 0; i < len(withdrawalRequests); i++ { + r, err := withdrawalRequests[i].HashTreeRoot() + if err != nil { + return [32]byte{}, err + } + roots[i] = r + } + + bytesRoot, err := BitwiseMerkleize(roots, uint64(len(roots)), limit) + if err != nil { + return [32]byte{}, errors.Wrap(err, "could not compute merkleization") + } + bytesRootBuf := new(bytes.Buffer) + if err := binary.Write(bytesRootBuf, binary.LittleEndian, uint64(len(withdrawalRequests))); err != nil { + return [32]byte{}, errors.Wrap(err, "could not marshal length") + } + bytesRootBufRoot := make([]byte, 32) + copy(bytesRootBufRoot, bytesRootBuf.Bytes()) + return MixInLength(bytesRoot, bytesRootBufRoot), nil +} + // ByteSliceRoot is a helper func to merkleize an arbitrary List[Byte, N] // this func runs Chunkify + MerkleizeVector // max length is dividable by 32 ( root length ) diff --git a/encoding/ssz/htrutils_test.go b/encoding/ssz/htrutils_test.go index 94a52730db9d..a49f63932f75 100644 --- a/encoding/ssz/htrutils_test.go +++ b/encoding/ssz/htrutils_test.go @@ -5,6 +5,7 @@ import ( "testing" fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams" + "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" "github.com/prysmaticlabs/prysm/v5/encoding/ssz" enginev1 "github.com/prysmaticlabs/prysm/v5/proto/engine/v1" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" @@ -279,3 +280,75 @@ func TestWithrawalSliceRoot(t *testing.T) { }) } } + +func TestDepositReceiptSliceRoot(t *testing.T) { + tests := []struct { + name string + input []*enginev1.DepositReceipt + limit uint64 + want [32]byte + }{ + { + name: "empty", + input: make([]*enginev1.DepositReceipt, 0), + want: [32]byte{0xf5, 0xa5, 0xfd, 0x42, 0xd1, 0x6a, 0x20, 0x30, 0x27, 0x98, 0xef, 0x6e, 0xd3, 0x9, 0x97, 0x9b, 0x43, 0x0, 0x3d, 0x23, 0x20, 0xd9, 0xf0, 0xe8, 0xea, 0x98, 0x31, 0xa9, 0x27, 0x59, 0xfb, 0x4b}, + }, + { + name: "non-empty", + input: []*enginev1.DepositReceipt{ + { + Pubkey: bytesutil.PadTo([]byte{0x01, 0x02}, 48), + WithdrawalCredentials: bytesutil.PadTo([]byte{0x03, 0x04}, 32), + Amount: 5, + Signature: bytesutil.PadTo([]byte{0x06, 0x07}, 96), + Index: 8, + }, + }, + limit: 16, + want: [32]byte{0x34, 0xe3, 0x76, 0x5, 0xe5, 0x12, 0xe4, 0x75, 0x14, 0xf6, 0x72, 0x1c, 0x56, 0x5a, 0xa7, 0xf8, 0x8d, 0xaf, 0x84, 0xb7, 0xd7, 0x3e, 0xe6, 0x5f, 0x3f, 0xb1, 0x9f, 0x41, 0xf0, 0x10, 0x2b, 0xe6}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ssz.DepositReceiptSliceRoot(tt.input, tt.limit) + require.NoError(t, err) + require.DeepSSZEqual(t, tt.want, got) + }) + } +} + +func TestWithdrawalRequestSliceRoot(t *testing.T) { + tests := []struct { + name string + input []*enginev1.ExecutionLayerWithdrawalRequest + limit uint64 + want [32]byte + }{ + { + name: "empty", + input: make([]*enginev1.ExecutionLayerWithdrawalRequest, 0), + want: [32]byte{0xf5, 0xa5, 0xfd, 0x42, 0xd1, 0x6a, 0x20, 0x30, 0x27, 0x98, 0xef, 0x6e, 0xd3, 0x9, 0x97, 0x9b, 0x43, 0x0, 0x3d, 0x23, 0x20, 0xd9, 0xf0, 0xe8, 0xea, 0x98, 0x31, 0xa9, 0x27, 0x59, 0xfb, 0x4b}, + }, + { + name: "non-empty", + input: []*enginev1.ExecutionLayerWithdrawalRequest{ + { + SourceAddress: bytesutil.PadTo([]byte{0x01, 0x02}, 20), + ValidatorPubkey: bytesutil.PadTo([]byte{0x03, 0x04}, 48), + Amount: 5, + }, + }, + limit: 16, + want: [32]byte{0xa8, 0xab, 0xb2, 0x20, 0xe6, 0xd6, 0x5a, 0x7e, 0x56, 0x60, 0xe4, 0x9d, 0xae, 0x36, 0x17, 0x3d, 0x8b, 0xd, 0xde, 0x28, 0x96, 0x5, 0x82, 0x72, 0x18, 0xda, 0xc7, 0x5a, 0x53, 0xe0, 0x35, 0xf7}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ssz.WithdrawalRequestSliceRoot(tt.input, tt.limit) + require.NoError(t, err) + require.DeepSSZEqual(t, tt.want, got) + }) + } +}