mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-09 15:37:56 -05:00
Merkle Proofs of KZG commitments (#13159)
* Merkle Proofs of KZG commitments * fix mock * Implement Merkle proof spectests * Check Proof construction in spectests * fix Merkle proof generator * Add unit test * add ssz package unit tests * add benchmark * fix typo in comment * ProposerSlashing was repeated * Terence's review * move to consensus_blocks * use existing error
This commit is contained in:
@@ -28,6 +28,7 @@ const (
|
|||||||
MaxWithdrawalsPerPayload = 16 // MaxWithdrawalsPerPayloadLength defines the maximum number of withdrawals that can be included in a payload.
|
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.
|
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.
|
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.
|
BlobLength = 131072 // BlobLength defines the byte length of a blob.
|
||||||
BlobSize = 131072 // defined to match blob.size in bazel ssz codegen
|
BlobSize = 131072 // defined to match blob.size in bazel ssz codegen
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const (
|
|||||||
MaxWithdrawalsPerPayload = 4 // MaxWithdrawalsPerPayloadLength defines the maximum number of withdrawals that can be included in a payload.
|
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.
|
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.
|
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.
|
BlobLength = 4 // BlobLength defines the byte length of a blob.
|
||||||
BlobSize = 128 // defined to match blob.size in bazel ssz codegen
|
BlobSize = 128 // defined to match blob.size in bazel ssz codegen
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ go_library(
|
|||||||
"execution.go",
|
"execution.go",
|
||||||
"factory.go",
|
"factory.go",
|
||||||
"getters.go",
|
"getters.go",
|
||||||
|
"kzg.go",
|
||||||
"proto.go",
|
"proto.go",
|
||||||
"roblob.go",
|
"roblob.go",
|
||||||
"roblock.go",
|
"roblock.go",
|
||||||
@@ -16,9 +17,11 @@ go_library(
|
|||||||
visibility = ["//visibility:public"],
|
visibility = ["//visibility:public"],
|
||||||
deps = [
|
deps = [
|
||||||
"//config/fieldparams:go_default_library",
|
"//config/fieldparams:go_default_library",
|
||||||
|
"//config/params:go_default_library",
|
||||||
"//consensus-types:go_default_library",
|
"//consensus-types:go_default_library",
|
||||||
"//consensus-types/interfaces:go_default_library",
|
"//consensus-types/interfaces:go_default_library",
|
||||||
"//consensus-types/primitives:go_default_library",
|
"//consensus-types/primitives:go_default_library",
|
||||||
|
"//container/trie:go_default_library",
|
||||||
"//encoding/bytesutil:go_default_library",
|
"//encoding/bytesutil:go_default_library",
|
||||||
"//encoding/ssz:go_default_library",
|
"//encoding/ssz:go_default_library",
|
||||||
"//math:go_default_library",
|
"//math:go_default_library",
|
||||||
@@ -28,6 +31,7 @@ go_library(
|
|||||||
"//runtime/version:go_default_library",
|
"//runtime/version:go_default_library",
|
||||||
"@com_github_pkg_errors//:go_default_library",
|
"@com_github_pkg_errors//:go_default_library",
|
||||||
"@com_github_prysmaticlabs_fastssz//:go_default_library",
|
"@com_github_prysmaticlabs_fastssz//:go_default_library",
|
||||||
|
"@com_github_prysmaticlabs_gohashtree//:go_default_library",
|
||||||
"@com_github_sirupsen_logrus//:go_default_library",
|
"@com_github_sirupsen_logrus//:go_default_library",
|
||||||
"@org_golang_google_protobuf//proto:go_default_library",
|
"@org_golang_google_protobuf//proto:go_default_library",
|
||||||
],
|
],
|
||||||
@@ -39,6 +43,7 @@ go_test(
|
|||||||
"execution_test.go",
|
"execution_test.go",
|
||||||
"factory_test.go",
|
"factory_test.go",
|
||||||
"getters_test.go",
|
"getters_test.go",
|
||||||
|
"kzg_test.go",
|
||||||
"proto_test.go",
|
"proto_test.go",
|
||||||
"roblob_test.go",
|
"roblob_test.go",
|
||||||
"roblock_test.go",
|
"roblock_test.go",
|
||||||
@@ -49,6 +54,7 @@ go_test(
|
|||||||
"//consensus-types:go_default_library",
|
"//consensus-types:go_default_library",
|
||||||
"//consensus-types/interfaces:go_default_library",
|
"//consensus-types/interfaces:go_default_library",
|
||||||
"//consensus-types/primitives:go_default_library",
|
"//consensus-types/primitives:go_default_library",
|
||||||
|
"//container/trie:go_default_library",
|
||||||
"//encoding/bytesutil:go_default_library",
|
"//encoding/bytesutil:go_default_library",
|
||||||
"//proto/engine/v1:go_default_library",
|
"//proto/engine/v1:go_default_library",
|
||||||
"//proto/prysm/v1alpha1:go_default_library",
|
"//proto/prysm/v1alpha1:go_default_library",
|
||||||
@@ -58,5 +64,6 @@ go_test(
|
|||||||
"//testing/require:go_default_library",
|
"//testing/require:go_default_library",
|
||||||
"@com_github_prysmaticlabs_fastssz//:go_default_library",
|
"@com_github_prysmaticlabs_fastssz//:go_default_library",
|
||||||
"@com_github_prysmaticlabs_go_bitfield//:go_default_library",
|
"@com_github_prysmaticlabs_go_bitfield//:go_default_library",
|
||||||
|
"@com_github_prysmaticlabs_gohashtree//:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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.
|
// HashTreeRoot returns the ssz root of the block body.
|
||||||
func (b *BeaconBlockBody) HashTreeRoot() ([field_params.RootLength]byte, error) {
|
func (b *BeaconBlockBody) HashTreeRoot() ([field_params.RootLength]byte, error) {
|
||||||
pb, err := b.Proto()
|
pb, err := b.Proto()
|
||||||
|
|||||||
192
consensus-types/blocks/kzg.go
Normal file
192
consensus-types/blocks/kzg.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
130
consensus-types/blocks/kzg_test.go
Normal file
130
consensus-types/blocks/kzg_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,6 +59,7 @@ type ReadOnlyBeaconBlock interface {
|
|||||||
// ReadOnlyBeaconBlockBody describes the method set employed by an object
|
// ReadOnlyBeaconBlockBody describes the method set employed by an object
|
||||||
// that is a beacon block body.
|
// that is a beacon block body.
|
||||||
type ReadOnlyBeaconBlockBody interface {
|
type ReadOnlyBeaconBlockBody interface {
|
||||||
|
Version() int
|
||||||
RandaoReveal() [field_params.BLSSignatureLength]byte
|
RandaoReveal() [field_params.BLSSignatureLength]byte
|
||||||
Eth1Data() *ethpb.Eth1Data
|
Eth1Data() *ethpb.Eth1Data
|
||||||
Graffiti() [field_params.RootLength]byte
|
Graffiti() [field_params.RootLength]byte
|
||||||
|
|||||||
@@ -316,6 +316,10 @@ func (b *BeaconBlockBody) Attestations() []*eth.Attestation {
|
|||||||
panic("implement me")
|
panic("implement me")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BeaconBlockBody) Version() int {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
var _ interfaces.ReadOnlySignedBeaconBlock = &SignedBeaconBlock{}
|
var _ interfaces.ReadOnlySignedBeaconBlock = &SignedBeaconBlock{}
|
||||||
var _ interfaces.ReadOnlyBeaconBlock = &BeaconBlock{}
|
var _ interfaces.ReadOnlyBeaconBlock = &BeaconBlock{}
|
||||||
var _ interfaces.ReadOnlyBeaconBlockBody = &BeaconBlockBody{}
|
var _ interfaces.ReadOnlyBeaconBlockBody = &BeaconBlockBody{}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ go_library(
|
|||||||
"@com_github_minio_sha256_simd//:go_default_library",
|
"@com_github_minio_sha256_simd//:go_default_library",
|
||||||
"@com_github_pkg_errors//:go_default_library",
|
"@com_github_pkg_errors//:go_default_library",
|
||||||
"@com_github_prysmaticlabs_go_bitfield//:go_default_library",
|
"@com_github_prysmaticlabs_go_bitfield//:go_default_library",
|
||||||
|
"@com_github_prysmaticlabs_gohashtree//:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
package ssz
|
package ssz
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/prysmaticlabs/gohashtree"
|
||||||
"github.com/prysmaticlabs/prysm/v4/container/trie"
|
"github.com/prysmaticlabs/prysm/v4/container/trie"
|
||||||
"github.com/prysmaticlabs/prysm/v4/crypto/hash/htr"
|
"github.com/prysmaticlabs/prysm/v4/crypto/hash/htr"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Merkleize.go is mostly a directly copy of the same filename from
|
var errInvalidNilSlice = errors.New("invalid empty slice")
|
||||||
// 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'.
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
mask0 = ^uint64((1 << (1 << iota)) - 1)
|
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]
|
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<<j)-1)&i) == ((1<<j)-1) {
|
|
||||||
// insert sibling into the proof
|
|
||||||
branch[j] = hArr
|
|
||||||
}
|
|
||||||
// stop merging when we are in the left side of the next combi
|
|
||||||
if i&(uint64(1)<<j) == 0 {
|
|
||||||
// if we are at the count, we want to merge in zero-hashes for padding
|
|
||||||
if i == count && j < depth {
|
|
||||||
v := hasher.Combi(hArr, trie.ZeroHashes[j])
|
|
||||||
copy(h, v[:])
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// keep merging up if we are the right side
|
|
||||||
v := hasher.Combi(tmp[j], hArr)
|
|
||||||
copy(h, v[:])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// store the merge result (may be no merge, i.e. bottom leaf node)
|
|
||||||
copy(tmp[j][:], h)
|
|
||||||
}
|
|
||||||
|
|
||||||
// merge in leaf by leaf.
|
|
||||||
for i := uint64(0); i < count; i++ {
|
|
||||||
copy(h, leaf(i))
|
|
||||||
merge(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// complement with 0 if empty, or if not the right power of 2
|
|
||||||
if (uint64(1) << depth) != count {
|
|
||||||
copy(h, trie.ZeroHashes[0][:])
|
|
||||||
merge(count)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// MerkleizeVector uses our optimized routine to hash a list of 32-byte
|
// MerkleizeVector uses our optimized routine to hash a list of 32-byte
|
||||||
// elements.
|
// elements.
|
||||||
func MerkleizeVector(elements [][32]byte, length uint64) [32]byte {
|
func MerkleizeVector(elements [][32]byte, length uint64) [32]byte {
|
||||||
@@ -217,3 +151,52 @@ func MerkleizeVector(elements [][32]byte, length uint64) [32]byte {
|
|||||||
}
|
}
|
||||||
return elements[0]
|
return elements[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hashable is an interface representing objects that implement HashTreeRoot()
|
||||||
|
type Hashable interface {
|
||||||
|
HashTreeRoot() ([32]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MerkleizeVectorSSZ hashes each element in the list and then returns the HTR
|
||||||
|
// of the corresponding list of roots
|
||||||
|
func MerkleizeVectorSSZ[T Hashable](elements []T, length uint64) ([32]byte, error) {
|
||||||
|
roots := make([][32]byte, len(elements))
|
||||||
|
var err error
|
||||||
|
for i, el := range elements {
|
||||||
|
roots[i], err = el.HashTreeRoot()
|
||||||
|
if err != nil {
|
||||||
|
return [32]byte{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MerkleizeVector(roots, length), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MerkleizeListSSZ hashes each element in the list and then returns the HTR of
|
||||||
|
// the list of corresponding roots, with the length mixed in.
|
||||||
|
func MerkleizeListSSZ[T Hashable](elements []T, limit uint64) ([32]byte, error) {
|
||||||
|
body, err := MerkleizeVectorSSZ(elements, limit)
|
||||||
|
if err != nil {
|
||||||
|
return [32]byte{}, err
|
||||||
|
}
|
||||||
|
chunks := make([][32]byte, 2)
|
||||||
|
chunks[0] = body
|
||||||
|
binary.LittleEndian.PutUint64(chunks[1][:], uint64(len(elements)))
|
||||||
|
if err := gohashtree.Hash(chunks, chunks); err != nil {
|
||||||
|
return [32]byte{}, err
|
||||||
|
}
|
||||||
|
return chunks[0], err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MerkleizeByteSliceSSZ hashes a byteslice by chunkifying it and returning the
|
||||||
|
// corresponding HTR as if it were a fixed vector of bytes of the given length.
|
||||||
|
func MerkleizeByteSliceSSZ(input []byte) ([32]byte, error) {
|
||||||
|
numChunks := (len(input) + 31) / 32
|
||||||
|
if numChunks == 0 {
|
||||||
|
return [32]byte{}, errInvalidNilSlice
|
||||||
|
}
|
||||||
|
chunks := make([][32]byte, numChunks)
|
||||||
|
for i := range chunks {
|
||||||
|
copy(chunks[i][:], input[32*i:])
|
||||||
|
}
|
||||||
|
return MerkleizeVector(chunks, uint64(numChunks)), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ package ssz_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/prysmaticlabs/go-bitfield"
|
||||||
"github.com/prysmaticlabs/prysm/v4/crypto/hash"
|
"github.com/prysmaticlabs/prysm/v4/crypto/hash"
|
||||||
"github.com/prysmaticlabs/prysm/v4/encoding/ssz"
|
"github.com/prysmaticlabs/prysm/v4/encoding/ssz"
|
||||||
|
ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
|
||||||
"github.com/prysmaticlabs/prysm/v4/testing/assert"
|
"github.com/prysmaticlabs/prysm/v4/testing/assert"
|
||||||
|
"github.com/prysmaticlabs/prysm/v4/testing/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetDepth(t *testing.T) {
|
func TestGetDepth(t *testing.T) {
|
||||||
@@ -59,60 +62,74 @@ func TestMerkleizeNormalPath(t *testing.T) {
|
|||||||
assert.Equal(t, expected, result)
|
assert.Equal(t, expected, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConstructProofCountGreaterThanLimit(t *testing.T) {
|
|
||||||
hashFn := ssz.NewHasherFunc(hash.CustomSHA256Hasher())
|
|
||||||
count := uint64(2)
|
|
||||||
limit := uint64(1)
|
|
||||||
chunks := [][]byte{{}}
|
|
||||||
leafIndexer := func(i uint64) []byte {
|
|
||||||
return chunks[i]
|
|
||||||
}
|
|
||||||
index := uint64(0)
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r == nil {
|
|
||||||
t.Errorf("The code did not panic.")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
ssz.ConstructProof(hashFn, count, limit, leafIndexer, index)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConstructProofIndexGreaterThanEqualToLimit(t *testing.T) {
|
|
||||||
hashFn := ssz.NewHasherFunc(hash.CustomSHA256Hasher())
|
|
||||||
count := uint64(1)
|
|
||||||
limit := uint64(1)
|
|
||||||
chunks := [][]byte{{}}
|
|
||||||
leafIndexer := func(i uint64) []byte {
|
|
||||||
return chunks[i]
|
|
||||||
}
|
|
||||||
index := uint64(1)
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r == nil {
|
|
||||||
t.Errorf("The code did not panic.")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
ssz.ConstructProof(hashFn, count, limit, leafIndexer, index)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConstructProofNormalPath(t *testing.T) {
|
|
||||||
hashFn := ssz.NewHasherFunc(hash.CustomSHA256Hasher())
|
|
||||||
count := uint64(2)
|
|
||||||
limit := uint64(3)
|
|
||||||
chunks := [][]byte{{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}}
|
|
||||||
leafIndexer := func(i uint64) []byte {
|
|
||||||
return chunks[i]
|
|
||||||
}
|
|
||||||
index := uint64(1)
|
|
||||||
expected := [][32]byte{
|
|
||||||
{1, 2, 3, 4, 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},
|
|
||||||
{245, 165, 253, 66, 209, 106, 32, 48, 39, 152, 239, 110, 211, 9, 151, 155, 67, 0, 61, 35, 32, 217, 240, 232, 234, 152, 49, 169, 39, 89, 251, 75},
|
|
||||||
}
|
|
||||||
result := ssz.ConstructProof(hashFn, count, limit, leafIndexer, index)
|
|
||||||
assert.Equal(t, len(expected), len(result))
|
|
||||||
for i, v := range expected {
|
|
||||||
assert.DeepEqual(t, result[i], v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDepthOfOne(t *testing.T) {
|
func TestDepthOfOne(t *testing.T) {
|
||||||
assert.Equal(t, uint8(0), ssz.Depth(1))
|
assert.Equal(t, uint8(0), ssz.Depth(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_MerkleizeVectorSSZ(t *testing.T) {
|
||||||
|
t.Run("empty vector", func(t *testing.T) {
|
||||||
|
attList := make([]*ethpb.Attestation, 0)
|
||||||
|
expected := [32]byte{83, 109, 152, 131, 127, 45, 209, 101, 165, 93, 94, 234, 233, 20, 133, 149, 68, 114, 213, 111, 36, 109, 242, 86, 191, 60, 174, 25, 53, 42, 18, 60}
|
||||||
|
length := uint64(16)
|
||||||
|
root, err := ssz.MerkleizeVectorSSZ(attList, length)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, expected, root)
|
||||||
|
})
|
||||||
|
t.Run("non empty vector", func(t *testing.T) {
|
||||||
|
sig := make([]byte, 96)
|
||||||
|
br := make([]byte, 32)
|
||||||
|
attList := make([]*ethpb.Attestation, 1)
|
||||||
|
attList[0] = ðpb.Attestation{
|
||||||
|
AggregationBits: bitfield.Bitlist{0x01},
|
||||||
|
Data: ðpb.AttestationData{
|
||||||
|
BeaconBlockRoot: br,
|
||||||
|
Source: ðpb.Checkpoint{
|
||||||
|
Root: br,
|
||||||
|
},
|
||||||
|
Target: ðpb.Checkpoint{
|
||||||
|
Root: br,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Signature: sig,
|
||||||
|
}
|
||||||
|
expected := [32]byte{199, 186, 55, 142, 200, 75, 219, 191, 66, 153, 100, 181, 200, 15, 143, 160, 25, 133, 105, 26, 183, 107, 10, 198, 232, 231, 107, 162, 243, 243, 56, 20}
|
||||||
|
length := uint64(16)
|
||||||
|
root, err := ssz.MerkleizeVectorSSZ(attList, length)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, expected, root)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_MerkleizeListSSZ(t *testing.T) {
|
||||||
|
t.Run("empty vector", func(t *testing.T) {
|
||||||
|
attList := make([]*ethpb.Attestation, 0)
|
||||||
|
expected := [32]byte{121, 41, 48, 187, 213, 186, 172, 67, 188, 199, 152, 238, 73, 170, 129, 133, 239, 118, 187, 59, 68, 186, 98, 185, 29, 134, 174, 86, 158, 75, 181, 53}
|
||||||
|
length := uint64(16)
|
||||||
|
root, err := ssz.MerkleizeListSSZ(attList, length)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, expected, root)
|
||||||
|
})
|
||||||
|
t.Run("non empty vector", func(t *testing.T) {
|
||||||
|
sig := make([]byte, 96)
|
||||||
|
br := make([]byte, 32)
|
||||||
|
attList := make([]*ethpb.Attestation, 1)
|
||||||
|
attList[0] = ðpb.Attestation{
|
||||||
|
AggregationBits: bitfield.Bitlist{0x01},
|
||||||
|
Data: ðpb.AttestationData{
|
||||||
|
BeaconBlockRoot: br,
|
||||||
|
Source: ðpb.Checkpoint{
|
||||||
|
Root: br,
|
||||||
|
},
|
||||||
|
Target: ðpb.Checkpoint{
|
||||||
|
Root: br,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Signature: sig,
|
||||||
|
}
|
||||||
|
expected := [32]byte{161, 247, 30, 234, 219, 222, 154, 88, 7, 207, 6, 23, 46, 125, 135, 67, 225, 178, 217, 131, 113, 124, 242, 106, 194, 43, 205, 194, 49, 172, 232, 229}
|
||||||
|
length := uint64(16)
|
||||||
|
root, err := ssz.MerkleizeListSSZ(attList, length)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, expected, root)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ go_library(
|
|||||||
importpath = "github.com/prysmaticlabs/prysm/v4/testing/spectest/shared/common/merkle_proof",
|
importpath = "github.com/prysmaticlabs/prysm/v4/testing/spectest/shared/common/merkle_proof",
|
||||||
visibility = ["//visibility:public"],
|
visibility = ["//visibility:public"],
|
||||||
deps = [
|
deps = [
|
||||||
|
"//config/fieldparams:go_default_library",
|
||||||
|
"//consensus-types/blocks:go_default_library",
|
||||||
"//container/trie:go_default_library",
|
"//container/trie:go_default_library",
|
||||||
"//testing/require:go_default_library",
|
"//testing/require:go_default_library",
|
||||||
"//testing/spectest/shared/common/ssz_static:go_default_library",
|
"//testing/spectest/shared/common/ssz_static:go_default_library",
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
"github.com/bazelbuild/rules_go/go/tools/bazel"
|
"github.com/bazelbuild/rules_go/go/tools/bazel"
|
||||||
"github.com/golang/snappy"
|
"github.com/golang/snappy"
|
||||||
fssz "github.com/prysmaticlabs/fastssz"
|
fssz "github.com/prysmaticlabs/fastssz"
|
||||||
|
field_params "github.com/prysmaticlabs/prysm/v4/config/fieldparams"
|
||||||
|
consensus_blocks "github.com/prysmaticlabs/prysm/v4/consensus-types/blocks"
|
||||||
"github.com/prysmaticlabs/prysm/v4/container/trie"
|
"github.com/prysmaticlabs/prysm/v4/container/trie"
|
||||||
"github.com/prysmaticlabs/prysm/v4/testing/require"
|
"github.com/prysmaticlabs/prysm/v4/testing/require"
|
||||||
"github.com/prysmaticlabs/prysm/v4/testing/spectest/shared/common/ssz_static"
|
"github.com/prysmaticlabs/prysm/v4/testing/spectest/shared/common/ssz_static"
|
||||||
@@ -16,6 +18,8 @@ import (
|
|||||||
"github.com/prysmaticlabs/prysm/v4/testing/util"
|
"github.com/prysmaticlabs/prysm/v4/testing/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const kzgOffset = 54 * field_params.MaxBlobCommitmentsPerBlock
|
||||||
|
|
||||||
// SingleMerkleProof is the format used to read spectest Merkle Proof test data.
|
// SingleMerkleProof is the format used to read spectest Merkle Proof test data.
|
||||||
type SingleMerkleProof struct {
|
type SingleMerkleProof struct {
|
||||||
Leaf string `json:"leaf"`
|
Leaf string `json:"leaf"`
|
||||||
@@ -73,7 +77,21 @@ func runSingleMerkleProofTests(t *testing.T, config, forkOrPhase string, unmarsh
|
|||||||
leaf, err := hex.DecodeString(proof.Leaf[2:])
|
leaf, err := hex.DecodeString(proof.Leaf[2:])
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, true, trie.VerifyMerkleProof(root[:], leaf, proof.LeafIndex, branch))
|
index := proof.LeafIndex
|
||||||
|
require.Equal(t, true, trie.VerifyMerkleProof(root[:], leaf, index, branch))
|
||||||
|
body, err := consensus_blocks.NewBeaconBlockBody(object)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if index < kzgOffset || index > 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)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user