diff --git a/config/fieldparams/mainnet.go b/config/fieldparams/mainnet.go index 5f17b92cbab6..4faf4526209b 100644 --- a/config/fieldparams/mainnet.go +++ b/config/fieldparams/mainnet.go @@ -28,6 +28,7 @@ const ( MaxWithdrawalsPerPayload = 16 // MaxWithdrawalsPerPayloadLength defines the maximum number of withdrawals that can be included in a payload. MaxBlobsPerBlock = 6 // MaxBlobsPerBlock defines the maximum number of blobs with respect to consensus rule can be included in a block. MaxBlobCommitmentsPerBlock = 4096 // MaxBlobCommitmentsPerBlock defines the theoretical limit of blobs can be included in a block. + LogMaxBlobCommitments = 12 // Log_2 of MaxBlobCommitmentsPerBlock BlobLength = 131072 // BlobLength defines the byte length of a blob. BlobSize = 131072 // defined to match blob.size in bazel ssz codegen ) diff --git a/config/fieldparams/minimal.go b/config/fieldparams/minimal.go index 9efa6b458d81..7c0e98bb114f 100644 --- a/config/fieldparams/minimal.go +++ b/config/fieldparams/minimal.go @@ -28,6 +28,7 @@ const ( MaxWithdrawalsPerPayload = 4 // MaxWithdrawalsPerPayloadLength defines the maximum number of withdrawals that can be included in a payload. MaxBlobsPerBlock = 6 // MaxBlobsPerBlock defines the maximum number of blobs with respect to consensus rule can be included in a block. MaxBlobCommitmentsPerBlock = 16 // MaxBlobCommitmentsPerBlock defines the theoretical limit of blobs can be included in a block. + LogMaxBlobCommitments = 4 // Log_2 of MaxBlobCommitmentsPerBlock BlobLength = 4 // BlobLength defines the byte length of a blob. BlobSize = 128 // defined to match blob.size in bazel ssz codegen ) diff --git a/consensus-types/blocks/BUILD.bazel b/consensus-types/blocks/BUILD.bazel index eba5f11b133e..5f369a6a7aef 100644 --- a/consensus-types/blocks/BUILD.bazel +++ b/consensus-types/blocks/BUILD.bazel @@ -6,6 +6,7 @@ go_library( "execution.go", "factory.go", "getters.go", + "kzg.go", "proto.go", "roblob.go", "roblock.go", @@ -16,9 +17,11 @@ go_library( visibility = ["//visibility:public"], deps = [ "//config/fieldparams:go_default_library", + "//config/params:go_default_library", "//consensus-types:go_default_library", "//consensus-types/interfaces:go_default_library", "//consensus-types/primitives:go_default_library", + "//container/trie:go_default_library", "//encoding/bytesutil:go_default_library", "//encoding/ssz:go_default_library", "//math:go_default_library", @@ -28,6 +31,7 @@ go_library( "//runtime/version:go_default_library", "@com_github_pkg_errors//:go_default_library", "@com_github_prysmaticlabs_fastssz//:go_default_library", + "@com_github_prysmaticlabs_gohashtree//:go_default_library", "@com_github_sirupsen_logrus//:go_default_library", "@org_golang_google_protobuf//proto:go_default_library", ], @@ -39,6 +43,7 @@ go_test( "execution_test.go", "factory_test.go", "getters_test.go", + "kzg_test.go", "proto_test.go", "roblob_test.go", "roblock_test.go", @@ -49,6 +54,7 @@ go_test( "//consensus-types:go_default_library", "//consensus-types/interfaces:go_default_library", "//consensus-types/primitives:go_default_library", + "//container/trie:go_default_library", "//encoding/bytesutil:go_default_library", "//proto/engine/v1:go_default_library", "//proto/prysm/v1alpha1:go_default_library", @@ -58,5 +64,6 @@ go_test( "//testing/require:go_default_library", "@com_github_prysmaticlabs_fastssz//:go_default_library", "@com_github_prysmaticlabs_go_bitfield//:go_default_library", + "@com_github_prysmaticlabs_gohashtree//:go_default_library", ], ) diff --git a/consensus-types/blocks/getters.go b/consensus-types/blocks/getters.go index 4ae081ba091f..9626be0180c7 100644 --- a/consensus-types/blocks/getters.go +++ b/consensus-types/blocks/getters.go @@ -1097,6 +1097,11 @@ func (b *BeaconBlockBody) BlobKzgCommitments() ([][]byte, error) { } } +// Version returns the version of the beacon block body +func (b *BeaconBlockBody) Version() int { + return b.version +} + // HashTreeRoot returns the ssz root of the block body. func (b *BeaconBlockBody) HashTreeRoot() ([field_params.RootLength]byte, error) { pb, err := b.Proto() diff --git a/consensus-types/blocks/kzg.go b/consensus-types/blocks/kzg.go new file mode 100644 index 000000000000..720c0ada3a96 --- /dev/null +++ b/consensus-types/blocks/kzg.go @@ -0,0 +1,192 @@ +package blocks + +import ( + "github.com/pkg/errors" + "github.com/prysmaticlabs/gohashtree" + field_params "github.com/prysmaticlabs/prysm/v4/config/fieldparams" + "github.com/prysmaticlabs/prysm/v4/config/params" + "github.com/prysmaticlabs/prysm/v4/consensus-types/interfaces" + "github.com/prysmaticlabs/prysm/v4/container/trie" + "github.com/prysmaticlabs/prysm/v4/encoding/ssz" + "github.com/prysmaticlabs/prysm/v4/runtime/version" +) + +const ( + bodyLength = 12 // The number of elements in the BeaconBlockBody Container + logBodyLength = 4 // The log 2 of bodyLength + kzgPosition = 11 // The index of the KZG commitment list in the Body +) + +var ( + errInvalidIndex = errors.New("index out of bounds") +) + +// MerkleProofKZGCommitment constructs a Merkle proof of inclusion of the KZG +// commitment of index `index` into the Beacon Block with the given `body` +func MerkleProofKZGCommitment(body interfaces.ReadOnlyBeaconBlockBody, index int) ([][]byte, error) { + bodyVersion := body.Version() + if bodyVersion < version.Deneb { + return nil, errUnsupportedBeaconBlockBody + } + commitments, err := body.BlobKzgCommitments() + if err != nil { + return nil, err + } + proof, err := bodyProof(commitments, index) + if err != nil { + return nil, err + } + membersRoots, err := topLevelRoots(body) + if err != nil { + return nil, err + } + sparse, err := trie.GenerateTrieFromItems(membersRoots, logBodyLength) + if err != nil { + return nil, err + } + topProof, err := sparse.MerkleProof(kzgPosition) + if err != nil { + return nil, err + } + // sparse.MerkleProof always includes the length of the slice this is + // why we remove the last element that is not needed in topProof + proof = append(proof, topProof[:len(topProof)-1]...) + return proof, nil +} + +// leavesFromCommitments hashes each commitment to construct a slice of roots +func leavesFromCommitments(commitments [][]byte) [][]byte { + leaves := make([][]byte, len(commitments)) + for i, kzg := range commitments { + chunk := make([][32]byte, 2) + copy(chunk[0][:], kzg) + copy(chunk[1][:], kzg[field_params.RootLength:]) + gohashtree.HashChunks(chunk, chunk) + leaves[i] = chunk[0][:] + } + return leaves +} + +// bodyProof returns the Merkle proof of the subtree up to the root of the KZG +// commitment list. +func bodyProof(commitments [][]byte, index int) ([][]byte, error) { + if index < 0 || index >= len(commitments) { + return nil, errInvalidIndex + } + leaves := leavesFromCommitments(commitments) + sparse, err := trie.GenerateTrieFromItems(leaves, field_params.LogMaxBlobCommitments) + if err != nil { + return nil, err + } + proof, err := sparse.MerkleProof(index) + if err != nil { + return nil, err + } + return proof, err +} + +// topLevelRoots computes the slice with the roots of each element in the +// BeaconBlockBody. Notice that the KZG commitments root is not needed for the +// proof computation thus it's omitted +func topLevelRoots(body interfaces.ReadOnlyBeaconBlockBody) ([][]byte, error) { + layer := make([][]byte, bodyLength) + for i := range layer { + layer[i] = make([]byte, 32) + } + + // Randao Reveal + randao := body.RandaoReveal() + root, err := ssz.MerkleizeByteSliceSSZ(randao[:]) + if err != nil { + return nil, err + } + copy(layer[0], root[:]) + + // eth1_data + eth1 := body.Eth1Data() + root, err = eth1.HashTreeRoot() + if err != nil { + return nil, err + } + copy(layer[1], root[:]) + + // graffiti + root = body.Graffiti() + copy(layer[2], root[:]) + + // Proposer slashings + ps := body.ProposerSlashings() + root, err = ssz.MerkleizeListSSZ(ps, params.BeaconConfig().MaxProposerSlashings) + if err != nil { + return nil, err + } + copy(layer[3], root[:]) + + // Attester slashings + as := body.AttesterSlashings() + root, err = ssz.MerkleizeListSSZ(as, params.BeaconConfig().MaxAttesterSlashings) + if err != nil { + return nil, err + } + copy(layer[4], root[:]) + + // Attestations + att := body.Attestations() + root, err = ssz.MerkleizeListSSZ(att, params.BeaconConfig().MaxAttestations) + if err != nil { + return nil, err + } + copy(layer[5], root[:]) + + // Deposits + dep := body.Deposits() + root, err = ssz.MerkleizeListSSZ(dep, params.BeaconConfig().MaxDeposits) + if err != nil { + return nil, err + } + copy(layer[6], root[:]) + + // Voluntary Exits + ve := body.VoluntaryExits() + root, err = ssz.MerkleizeListSSZ(ve, params.BeaconConfig().MaxVoluntaryExits) + if err != nil { + return nil, err + } + copy(layer[7], root[:]) + + // Sync Aggregate + sa, err := body.SyncAggregate() + if err != nil { + return nil, err + } + root, err = sa.HashTreeRoot() + if err != nil { + return nil, err + } + copy(layer[8], root[:]) + + // Execution Payload + ep, err := body.Execution() + if err != nil { + return nil, err + } + root, err = ep.HashTreeRoot() + if err != nil { + return nil, err + } + copy(layer[9], root[:]) + + // BLS Changes + bls, err := body.BLSToExecutionChanges() + if err != nil { + return nil, err + } + root, err = ssz.MerkleizeListSSZ(bls, params.BeaconConfig().MaxBlsToExecutionChanges) + if err != nil { + return nil, err + } + copy(layer[10], root[:]) + + // KZG commitments is not needed + return layer, nil +} diff --git a/consensus-types/blocks/kzg_test.go b/consensus-types/blocks/kzg_test.go new file mode 100644 index 000000000000..28b09ce5e89d --- /dev/null +++ b/consensus-types/blocks/kzg_test.go @@ -0,0 +1,130 @@ +package blocks + +import ( + "math/rand" + "testing" + + "github.com/prysmaticlabs/gohashtree" + fieldparams "github.com/prysmaticlabs/prysm/v4/config/fieldparams" + "github.com/prysmaticlabs/prysm/v4/container/trie" + enginev1 "github.com/prysmaticlabs/prysm/v4/proto/engine/v1" + ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/v4/testing/require" +) + +func Test_MerkleProofKZGCommitment_Altair(t *testing.T) { + kzgs := make([][]byte, 3) + kzgs[0] = make([]byte, 48) + _, err := rand.Read(kzgs[0]) + require.NoError(t, err) + kzgs[1] = make([]byte, 48) + _, err = rand.Read(kzgs[1]) + require.NoError(t, err) + kzgs[2] = make([]byte, 48) + _, err = rand.Read(kzgs[2]) + require.NoError(t, err) + pbBody := ðpb.BeaconBlockBodyAltair{} + + body, err := NewBeaconBlockBody(pbBody) + require.NoError(t, err) + _, err = MerkleProofKZGCommitment(body, 0) + require.ErrorIs(t, errUnsupportedBeaconBlockBody, err) +} + +func Test_MerkleProofKZGCommitment(t *testing.T) { + kzgs := make([][]byte, 3) + kzgs[0] = make([]byte, 48) + _, err := rand.Read(kzgs[0]) + require.NoError(t, err) + kzgs[1] = make([]byte, 48) + _, err = rand.Read(kzgs[1]) + require.NoError(t, err) + kzgs[2] = make([]byte, 48) + _, err = rand.Read(kzgs[2]) + require.NoError(t, err) + pbBody := ðpb.BeaconBlockBodyDeneb{ + SyncAggregate: ðpb.SyncAggregate{ + SyncCommitteeBits: make([]byte, fieldparams.SyncAggregateSyncCommitteeBytesLength), + SyncCommitteeSignature: make([]byte, fieldparams.BLSSignatureLength), + }, + ExecutionPayload: &enginev1.ExecutionPayloadDeneb{ + ParentHash: make([]byte, fieldparams.RootLength), + FeeRecipient: make([]byte, 20), + StateRoot: make([]byte, fieldparams.RootLength), + ReceiptsRoot: make([]byte, fieldparams.RootLength), + LogsBloom: make([]byte, 256), + PrevRandao: make([]byte, fieldparams.RootLength), + BaseFeePerGas: make([]byte, fieldparams.RootLength), + BlockHash: make([]byte, fieldparams.RootLength), + Transactions: make([][]byte, 0), + ExtraData: make([]byte, 0), + }, + Eth1Data: ðpb.Eth1Data{ + DepositRoot: make([]byte, fieldparams.RootLength), + BlockHash: make([]byte, fieldparams.RootLength), + }, + BlobKzgCommitments: kzgs, + } + + body, err := NewBeaconBlockBody(pbBody) + require.NoError(t, err) + index := 1 + _, err = MerkleProofKZGCommitment(body, 10) + require.ErrorIs(t, errInvalidIndex, err) + proof, err := MerkleProofKZGCommitment(body, index) + require.NoError(t, err) + + chunk := make([][32]byte, 2) + copy(chunk[0][:], kzgs[index]) + copy(chunk[1][:], kzgs[index][32:]) + gohashtree.HashChunks(chunk, chunk) + root, err := body.HashTreeRoot() + require.NoError(t, err) + kzgOffset := 54 * fieldparams.MaxBlobCommitmentsPerBlock + require.Equal(t, true, trie.VerifyMerkleProof(root[:], chunk[0][:], uint64(index+kzgOffset), proof)) +} + +func Benchmark_MerkleProofKZGCommitment(b *testing.B) { + kzgs := make([][]byte, 3) + kzgs[0] = make([]byte, 48) + _, err := rand.Read(kzgs[0]) + require.NoError(b, err) + kzgs[1] = make([]byte, 48) + _, err = rand.Read(kzgs[1]) + require.NoError(b, err) + kzgs[2] = make([]byte, 48) + _, err = rand.Read(kzgs[2]) + require.NoError(b, err) + pbBody := ðpb.BeaconBlockBodyDeneb{ + SyncAggregate: ðpb.SyncAggregate{ + SyncCommitteeBits: make([]byte, fieldparams.SyncAggregateSyncCommitteeBytesLength), + SyncCommitteeSignature: make([]byte, fieldparams.BLSSignatureLength), + }, + ExecutionPayload: &enginev1.ExecutionPayloadDeneb{ + ParentHash: make([]byte, fieldparams.RootLength), + FeeRecipient: make([]byte, 20), + StateRoot: make([]byte, fieldparams.RootLength), + ReceiptsRoot: make([]byte, fieldparams.RootLength), + LogsBloom: make([]byte, 256), + PrevRandao: make([]byte, fieldparams.RootLength), + BaseFeePerGas: make([]byte, fieldparams.RootLength), + BlockHash: make([]byte, fieldparams.RootLength), + Transactions: make([][]byte, 0), + ExtraData: make([]byte, 0), + }, + Eth1Data: ðpb.Eth1Data{ + DepositRoot: make([]byte, fieldparams.RootLength), + BlockHash: make([]byte, fieldparams.RootLength), + }, + BlobKzgCommitments: kzgs, + } + + body, err := NewBeaconBlockBody(pbBody) + require.NoError(b, err) + index := 1 + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := MerkleProofKZGCommitment(body, index) + require.NoError(b, err) + } +} diff --git a/consensus-types/interfaces/beacon_block.go b/consensus-types/interfaces/beacon_block.go index ce196279a75a..ca6bef3d4b8c 100644 --- a/consensus-types/interfaces/beacon_block.go +++ b/consensus-types/interfaces/beacon_block.go @@ -59,6 +59,7 @@ type ReadOnlyBeaconBlock interface { // ReadOnlyBeaconBlockBody describes the method set employed by an object // that is a beacon block body. type ReadOnlyBeaconBlockBody interface { + Version() int RandaoReveal() [field_params.BLSSignatureLength]byte Eth1Data() *ethpb.Eth1Data Graffiti() [field_params.RootLength]byte diff --git a/consensus-types/mock/block.go b/consensus-types/mock/block.go index 78f468548443..a31d69835580 100644 --- a/consensus-types/mock/block.go +++ b/consensus-types/mock/block.go @@ -316,6 +316,10 @@ func (b *BeaconBlockBody) Attestations() []*eth.Attestation { panic("implement me") } +func (b *BeaconBlockBody) Version() int { + panic("implement me") +} + var _ interfaces.ReadOnlySignedBeaconBlock = &SignedBeaconBlock{} var _ interfaces.ReadOnlyBeaconBlock = &BeaconBlock{} var _ interfaces.ReadOnlyBeaconBlockBody = &BeaconBlockBody{} diff --git a/encoding/ssz/BUILD.bazel b/encoding/ssz/BUILD.bazel index f2757ff43325..39c5a9c7ede6 100644 --- a/encoding/ssz/BUILD.bazel +++ b/encoding/ssz/BUILD.bazel @@ -20,6 +20,7 @@ go_library( "@com_github_minio_sha256_simd//:go_default_library", "@com_github_pkg_errors//:go_default_library", "@com_github_prysmaticlabs_go_bitfield//:go_default_library", + "@com_github_prysmaticlabs_gohashtree//:go_default_library", ], ) diff --git a/encoding/ssz/merkleize.go b/encoding/ssz/merkleize.go index af27f3106b3d..8359e5a7ac0f 100644 --- a/encoding/ssz/merkleize.go +++ b/encoding/ssz/merkleize.go @@ -1,15 +1,15 @@ package ssz import ( + "encoding/binary" + + "github.com/pkg/errors" + "github.com/prysmaticlabs/gohashtree" "github.com/prysmaticlabs/prysm/v4/container/trie" "github.com/prysmaticlabs/prysm/v4/crypto/hash/htr" ) -// Merkleize.go is mostly a directly copy of the same filename from -// https://github.com/protolambda/zssz/blob/master/merkle/merkleize.go. -// The reason the method is copied instead of imported is due to us using a -// custom hasher interface for a reduced memory footprint when using -// 'Merkleize'. +var errInvalidNilSlice = errors.New("invalid empty slice") const ( mask0 = ^uint64((1 << (1 << iota)) - 1) @@ -132,72 +132,6 @@ func Merkleize(hasher Hasher, count, limit uint64, leaf func(i uint64) []byte) ( return tmp[limitDepth] } -// ConstructProof builds a merkle-branch of the given depth, at the given index (at that depth), -// for a list of leafs of a balanced binary tree. -func ConstructProof(hasher Hasher, count, limit uint64, leaf func(i uint64) []byte, index uint64) (branch [][32]byte) { - if count > limit { - panic("merkleizing list that is too large, over limit") - } - if index >= limit { - panic("index out of range, over limit") - } - if limit <= 1 { - return - } - depth := Depth(count) - limitDepth := Depth(limit) - branch = append(branch, trie.ZeroHashes[:limitDepth]...) - - tmp := make([][32]byte, limitDepth+1) - - j := uint8(0) - var hArr [32]byte - h := hArr[:] - - merge := func(i uint64) { - // merge back up from bottom to top, as far as we can - for j = 0; ; j++ { - // if i is a sibling of index at the given depth, - // and i is the last index of the subtree to that depth, - // then put h into the branch - if (i>>j)^1 == (index>>j) && (((1< kzgOffset+field_params.MaxBlobsPerBlock { + return + } + localProof, err := consensus_blocks.MerkleProofKZGCommitment(body, int(index-kzgOffset)) + require.NoError(t, err) + require.Equal(t, len(branch), len(localProof)) + for i, root := range localProof { + require.DeepEqual(t, branch[i], root) + } }) } }