Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1/n] feat(deposits): merkle tree machinery to support deposit inclusion verification #2265

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion beacon/blockchain/deposit.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (s *Service[
ctx context.Context,
blockNum math.U64,
) {
if blockNum < s.eth1FollowDistance {
if blockNum <= s.eth1FollowDistance {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: never necessary to fetch logs for block height 0

s.logger.Info(
"depositFetcher, nothing to fetch",
"block num", blockNum,
Expand Down
6 changes: 2 additions & 4 deletions beacon/blockchain/finalize_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,12 @@ func (s *Service[
// STEP 4: Post Finalizations cleanups

// fetch and store the deposit for the block
blockNum := blk.GetBody().GetExecutionPayload().GetNumber()
s.depositFetcher(ctx, blockNum)
s.depositFetcher(ctx, blk.GetBody().GetExecutionPayload().GetNumber())

// store the finalized block in the KVStore.
slot := blk.GetSlot()
if err = s.blockStore.Set(blk); err != nil {
s.logger.Error(
"failed to store block", "slot", slot, "error", err,
"failed to store block", "slot", blk.GetSlot(), "error", err,
)
}

Expand Down
6 changes: 3 additions & 3 deletions consensus-types/types/deposits.go
calbera marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ func (ds Deposits) SizeSSZ(siz *ssz.Sizer, _ bool) uint32 {
func (ds Deposits) DefineSSZ(c *ssz.Codec) {
c.DefineDecoder(func(*ssz.Decoder) {
ssz.DefineSliceOfStaticObjectsContent(
c, (*[]*Deposit)(&ds), constants.MaxDepositsPerBlock)
c, (*[]*Deposit)(&ds), constants.MaxDeposits)
})
c.DefineEncoder(func(*ssz.Encoder) {
ssz.DefineSliceOfStaticObjectsContent(
c, (*[]*Deposit)(&ds), constants.MaxDepositsPerBlock)
c, (*[]*Deposit)(&ds), constants.MaxDeposits)
})
c.DefineHasher(func(*ssz.Hasher) {
ssz.DefineSliceOfStaticObjectsOffset(
c, (*[]*Deposit)(&ds), constants.MaxDepositsPerBlock)
c, (*[]*Deposit)(&ds), constants.MaxDeposits)
})
}

Expand Down
5 changes: 4 additions & 1 deletion node-api/backend/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ type BlockStore[BeaconBlockT any] interface {
// DepositStore defines the interface for deposit storage.
type DepositStore interface {
// GetDepositsByIndex returns `numView` expected deposits.
GetDepositsByIndex(startIndex uint64, numView uint64) ([]*ctypes.Deposit, error)
GetDepositsByIndex(
startIndex uint64,
numView uint64,
) ([]*ctypes.Deposit, error)
calbera marked this conversation as resolved.
Show resolved Hide resolved
// Prune prunes the deposit store of [start, end)
Prune(start, end uint64) error
// EnqueueDeposits adds a list of deposits to the deposit store.
Expand Down
7 changes: 7 additions & 0 deletions primitives/common/consensus.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
package common

import (
stdbytes "bytes"

"github.com/berachain/beacon-kit/primitives/bytes"
"github.com/berachain/beacon-kit/primitives/encoding/hex"
"github.com/berachain/beacon-kit/primitives/encoding/json"
Expand Down Expand Up @@ -85,3 +87,8 @@ func (r Root) MarshalJSON() ([]byte, error) {
func (r *Root) UnmarshalJSON(input []byte) error {
return r.UnmarshalText(input[1 : len(input)-1])
}

// Equals returns true if the root is equal to the other root.
func (r Root) Equals(other Root) bool {
return stdbytes.Equal(r[:], other[:])
}
8 changes: 6 additions & 2 deletions primitives/constants/payload.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ const (
// payload.
MaxTxsPerPayload uint64 = 1048576

// MaxDepositsPerBlock is the maximum number of deposits per block.
MaxDepositsPerBlock uint64 = 16
// DepositContractDepth is the depth of the deposit contract merkle tree.
DepositContractDepth uint64 = 32

// MaxDeposits is the maximum number of deposits supported by the
// deposit tree (2**32).
MaxDeposits uint64 = 1 << DepositContractDepth

// MaxWithdrawalsPerPayload is the maximum number of withdrawals in a
// execution payload.
Expand Down
3 changes: 3 additions & 0 deletions primitives/math/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ var (
// ErrUnexpectedInputLengthBase is the base error for unexpected input
// length errors.
ErrUnexpectedInputLengthBase = errors.New("unexpected input length")

// ErrOverflow is the base error for overflow errors.
ErrOverflow = errors.New("integer overflow")
)

// ErrUnexpectedInputLength returns an error indicating that the input length.
Expand Down
11 changes: 11 additions & 0 deletions primitives/math/pow/pow.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@

package pow

// TwoToThePowerOf returns 2^n. panics if n >= 64.
//
//nolint:mnd // todo fix.
func TwoToThePowerOf[U64T ~uint64](n U64T) U64T {
if n >= 64 {
panic("integer overflow")
}

return 1 << n
}

// PrevPowerOfTwo returns the previous power of 2 for the given input.
//
//nolint:mnd // todo fix.
Expand Down
14 changes: 14 additions & 0 deletions primitives/math/u64.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
package math

import (
"math"
"math/big"
"strconv"

Expand Down Expand Up @@ -155,3 +156,16 @@ func GweiFromWei(i *big.Int) (Gwei, error) {
func (u Gwei) ToWei() *U256 {
return (&U256{}).Mul(NewU256(uint64(u)), NewU256(GweiPerWei))
}

// ---------------------------- Helper Methods ----------------------------

// Int returns the integer value of the uint64 argument. If there is an
// overflow, then an error is
// returned.
func Int(u uint64) (int, error) {
if u > math.MaxInt {
return 0, ErrOverflow
}

return int(u), nil //#nosec:G701 // already checked for overflow.
}
2 changes: 1 addition & 1 deletion scripts/build/golines.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ fi
# Find all .go files in the project directory and its subdirectories, ignoring .pb.go and .pb_encoding.go files
find "${ROOT_DIR}" -type f -name "*.go" ! -name "*.pb.go" ! -name "*.pb_encoding.go" | while read -r file; do
echo "Processing $file..."
golines --reformat-tags --shorten-comments --write-output --max-len=80 "$file"
golines --reformat-tags --shorten-comments --write-output --max-len=140 "$file"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fridrik01 I found the culprit. This was shortening lines whenever I ran make format locally which runs this golines script

done

echo "✅ All files processed."
2 changes: 1 addition & 1 deletion scripts/build/linting.mk
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ license-fix:

nilaway:
@echo "--> Running nilaway"
(go run go.uber.org/nilaway/cmd/nilaway -exclude-errors-in-files "geth-primitives/deposit/" -v ./...) || exit 1;
(go run go.uber.org/nilaway/cmd/nilaway -exclude-pkgs "github.com/berachain/beacon-kit/storage/deposit/merkle" -exclude-errors-in-files "geth-primitives/deposit/" -v ./...) || exit 1;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not useful for now

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will revive in subsequent PR when deposit/merkle package is being used

@printf "Nilaway check complete\n"

#################
Expand Down
7 changes: 7 additions & 0 deletions storage/deposit/merkle/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Deposit Merkle Tree

This package implements the [EIP-4881 spec](https://eips.ethereum.org/EIPS/eip-4881) for a minimal sparse Merkle tree.

The format proposed by the EIP allows for the pruning of deposits that are no longer needed to participate fully in consensus.

Thanks to [Prysm](https://github.com/prysmaticlabs/prysm/blob/develop/beacon-chain/cache/depositsnapshot/deposit_tree.go) and Mark Mackey ([@ethDreamer](https://github.com/ethDreamer)) for the reference implementation.
65 changes: 65 additions & 0 deletions storage/deposit/merkle/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: BUSL-1.1
//
// Copyright (C) 2024, Berachain Foundation. All rights reserved.
// Use of this software is governed by the Business Source License included
// in the LICENSE file of this repository and at www.mariadb.com/bsl11.
//
// ANY USE OF THE LICENSED WORK IN VIOLATION OF THIS LICENSE WILL AUTOMATICALLY
// TERMINATE YOUR RIGHTS UNDER THIS LICENSE FOR THE CURRENT AND ALL OTHER
// VERSIONS OF THE LICENSED WORK.
//
// THIS LICENSE DOES NOT GRANT YOU ANY RIGHT IN ANY TRADEMARK OR LOGO OF
// LICENSOR OR ITS AFFILIATES (PROVIDED THAT YOU MAY USE A TRADEMARK OR LOGO OF
// LICENSOR AS EXPRESSLY REQUIRED BY THIS LICENSE).
//
// TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
// AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
// EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
// TITLE.

package merkle

import "github.com/berachain/beacon-kit/errors"

var (
// ErrFinalizedNodeCannotPushLeaf may occur when attempting to push a leaf
// to a finalized node.
// When a node is finalized, it cannot be modified or changed.
ErrFinalizedNodeCannotPushLeaf = errors.New(
"can't push a leaf to a finalized node",
)

// ErrLeafNodeCannotPushLeaf may occur when attempting to push a leaf to a
// leaf node.
ErrLeafNodeCannotPushLeaf = errors.New("can't push a leaf to a leaf node")

// ErrZeroLevel occurs when the value of level is 0.
ErrZeroLevel = errors.New("level should be greater than 0")

// ErrZeroDepth occurs when the value of depth is 0.
ErrZeroDepth = errors.New("depth should be greater than 0")
)

var (
// ErrInvalidSnapshotRoot occurs when the snapshot root does not match the
// calculated root.
ErrInvalidSnapshotRoot = errors.New("snapshot root is invalid")

// ErrInvalidDepositCount occurs when the value for mix in length is 0.
ErrInvalidDepositCount = errors.New(
"deposit count should be greater than 0",
)

// ErrInvalidIndex occurs when the index is less than the number of
// finalized deposits.
ErrInvalidIndex = errors.New(
"index should be greater than finalizedDeposits - 1",
)

// 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",
)
)
130 changes: 130 additions & 0 deletions storage/deposit/merkle/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// SPDX-License-Identifier: BUSL-1.1
//
// Copyright (C) 2024, Berachain Foundation. All rights reserved.
// Use of this software is governed by the Business Source License included
// in the LICENSE file of this repository and at www.mariadb.com/bsl11.
//
// ANY USE OF THE LICENSED WORK IN VIOLATION OF THIS LICENSE WILL AUTOMATICALLY
// TERMINATE YOUR RIGHTS UNDER THIS LICENSE FOR THE CURRENT AND ALL OTHER
// VERSIONS OF THE LICENSED WORK.
//
// THIS LICENSE DOES NOT GRANT YOU ANY RIGHT IN ANY TRADEMARK OR LOGO OF
// LICENSOR OR ITS AFFILIATES (PROVIDED THAT YOU MAY USE A TRADEMARK OR LOGO OF
// LICENSOR AS EXPRESSLY REQUIRED BY THIS LICENSE).
//
// TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
// AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
// EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
// TITLE.

package merkle

import (
"slices"

"github.com/berachain/beacon-kit/primitives/math/pow"
"github.com/berachain/beacon-kit/primitives/merkle"
)

// create builds a new merkle tree.
func create(
hasher merkle.Hasher[[32]byte],
leaves [][32]byte,
depth uint64,
) TreeNode {
length := uint64(len(leaves))
if length == 0 {
return &ZeroNode{
depth: depth,
hasher: hasher,
}
}

if depth == 0 {
return &LeafNode{hash: leaves[0]}
}

split := min(pow.TwoToThePowerOf(depth-1), length)
left := create(hasher, leaves[0:split], depth-1)
right := create(hasher, leaves[split:], depth-1)
return &InnerNode{
left: left,
right: right,
hasher: hasher,
}
}

// fromSnapshotParts creates a new Merkle tree from a list of finalized leaves,
// number of deposits and specified depth.
func fromSnapshotParts(
hasher merkle.Hasher[[32]byte],
finalized [][32]byte,
deposits uint64,
level uint64,
) (TreeNode, error) {
var err error

if len(finalized) < 1 || deposits == 0 {
return &ZeroNode{
depth: level,
hasher: hasher,
}, nil
}
if deposits == pow.TwoToThePowerOf(level) {
return &FinalizedNode{
depositCount: deposits,
hash: finalized[0],
}, nil
}
if level == 0 {
return nil, ErrZeroLevel
}
node := InnerNode{
hasher: hasher,
}
if leftSubtree := pow.TwoToThePowerOf(level - 1); deposits <= leftSubtree {
node.left, err = fromSnapshotParts(hasher, finalized, deposits, level-1)
if err != nil {
return nil, err
}
node.right = &ZeroNode{
depth: level - 1,
hasher: hasher,
}
} else {
node.left = &FinalizedNode{
depositCount: leftSubtree,
hash: finalized[0],
}
node.right, err = fromSnapshotParts(hasher, finalized[1:], deposits-leftSubtree, level-1)
if err != nil {
return nil, err
}
}
return &node, nil
}

// generateProof returns a merkle proof and root.
func generateProof(
tree TreeNode,
index uint64,
depth uint64,
) ([32]byte, [][32]byte) {
var proof [][32]byte
node := tree
for depth > 0 {
ithBit := (index >> (depth - 1)) & 0x1 //nolint:mnd // spec.
if ithBit == 1 {
proof = append(proof, node.Left().GetRoot())
node = node.Right()
} else {
proof = append(proof, node.Right().GetRoot())
node = node.Left()
}
depth--
}

slices.Reverse(proof)
return node.GetRoot(), proof
}
Loading
Loading