From f4f48d63729113686639a40fe51f1ff124a4726c Mon Sep 17 00:00:00 2001 From: terence Date: Wed, 9 Jul 2025 09:12:14 -0700 Subject: [PATCH] Optimize `BuildBlobSidecars` Merkle proof computation by pre-computing subtrees (#15473) * Optimize BuildBlobSidecars Merkle proof computation by pre-computing subtrees Co-Authored-By: Claude * Add change log * Fix change log --------- Co-authored-by: Claude --- .../v1alpha1/validator/proposer_deneb.go | 8 +- .../validator/proposer_deneb_bench_test.go | 224 ++++++++++++++++++ changelog/tt_sushi.md | 3 + consensus-types/blocks/kzg.go | 67 ++++++ 4 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 beacon-chain/rpc/prysm/v1alpha1/validator/proposer_deneb_bench_test.go create mode 100644 changelog/tt_sushi.md diff --git a/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_deneb.go b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_deneb.go index 52ac08faeb..9fc843babd 100644 --- a/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_deneb.go +++ b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_deneb.go @@ -28,8 +28,14 @@ func BuildBlobSidecars(blk interfaces.SignedBeaconBlock, blobs [][]byte, kzgProo return nil, err } body := blk.Block().Body() + // Pre-compute subtrees once before the loop to avoid redundant calculations + proofComponents, err := blocks.PrecomputeMerkleProofComponents(body) + if err != nil { + return nil, err + } + for i := range blobSidecars { - proof, err := blocks.MerkleProofKZGCommitment(body, i) + proof, err := blocks.MerkleProofKZGCommitmentFromComponents(proofComponents, i) if err != nil { return nil, err } diff --git a/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_deneb_bench_test.go b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_deneb_bench_test.go new file mode 100644 index 0000000000..abbfb4e1c0 --- /dev/null +++ b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_deneb_bench_test.go @@ -0,0 +1,224 @@ +package validator + +import ( + "errors" + "testing" + + fieldparams "github.com/OffchainLabs/prysm/v6/config/fieldparams" + "github.com/OffchainLabs/prysm/v6/consensus-types/blocks" + "github.com/OffchainLabs/prysm/v6/consensus-types/interfaces" + "github.com/OffchainLabs/prysm/v6/encoding/bytesutil" + ethpb "github.com/OffchainLabs/prysm/v6/proto/prysm/v1alpha1" + "github.com/OffchainLabs/prysm/v6/runtime/version" + "github.com/OffchainLabs/prysm/v6/testing/util" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +// BuildBlobSidecarsOriginal is the original implementation for comparison +func BuildBlobSidecarsOriginal(blk interfaces.SignedBeaconBlock, blobs [][]byte, kzgProofs [][]byte) ([]*ethpb.BlobSidecar, error) { + if blk.Version() < version.Deneb { + return nil, nil // No blobs before deneb. + } + commits, err := blk.Block().Body().BlobKzgCommitments() + if err != nil { + return nil, err + } + cLen := len(commits) + if cLen != len(blobs) || cLen != len(kzgProofs) { + return nil, errors.New("blob KZG commitments don't match number of blobs or KZG proofs") + } + blobSidecars := make([]*ethpb.BlobSidecar, cLen) + header, err := blk.Header() + if err != nil { + return nil, err + } + body := blk.Block().Body() + for i := range blobSidecars { + proof, err := blocks.MerkleProofKZGCommitment(body, i) + if err != nil { + return nil, err + } + blobSidecars[i] = ðpb.BlobSidecar{ + Index: uint64(i), + Blob: blobs[i], + KzgCommitment: commits[i], + KzgProof: kzgProofs[i], + SignedBlockHeader: header, + CommitmentInclusionProof: proof, + } + } + return blobSidecars, nil +} + +func setupBenchmarkData(b *testing.B, numBlobs int) (interfaces.SignedBeaconBlock, [][]byte, [][]byte) { + b.Helper() + + // Create KZG commitments + kzgCommitments := make([][]byte, numBlobs) + for i := 0; i < numBlobs; i++ { + kzgCommitments[i] = bytesutil.PadTo([]byte{byte(i)}, 48) + } + + // Create block with KZG commitments + blk, err := blocks.NewSignedBeaconBlock(util.NewBeaconBlockDeneb()) + if err != nil { + b.Fatal(err) + } + if err := blk.SetBlobKzgCommitments(kzgCommitments); err != nil { + b.Fatal(err) + } + + // Create blobs + blobs := make([][]byte, numBlobs) + for i := 0; i < numBlobs; i++ { + blobs[i] = make([]byte, fieldparams.BlobLength) + // Add some variation to the blob data + blobs[i][0] = byte(i) + } + + // Create KZG proofs + proof, err := hexutil.Decode("0xb4021b0de10f743893d4f71e1bf830c019e832958efd6795baf2f83b8699a9eccc5dc99015d8d4d8ec370d0cc333c06a") + if err != nil { + b.Fatal(err) + } + kzgProofs := make([][]byte, numBlobs) + for i := 0; i < numBlobs; i++ { + kzgProofs[i] = proof + } + + return blk, blobs, kzgProofs +} + +func BenchmarkBuildBlobSidecars_Original_1Blob(b *testing.B) { + blk, blobs, kzgProofs := setupBenchmarkData(b, 1) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := BuildBlobSidecarsOriginal(blk, blobs, kzgProofs) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkBuildBlobSidecars_Optimized_1Blob(b *testing.B) { + blk, blobs, kzgProofs := setupBenchmarkData(b, 1) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := BuildBlobSidecars(blk, blobs, kzgProofs) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkBuildBlobSidecars_Original_2Blobs(b *testing.B) { + blk, blobs, kzgProofs := setupBenchmarkData(b, 2) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := BuildBlobSidecarsOriginal(blk, blobs, kzgProofs) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkBuildBlobSidecars_Optimized_3Blobs(b *testing.B) { + blk, blobs, kzgProofs := setupBenchmarkData(b, 3) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := BuildBlobSidecars(blk, blobs, kzgProofs) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkBuildBlobSidecars_Original_3Blobs(b *testing.B) { + blk, blobs, kzgProofs := setupBenchmarkData(b, 3) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := BuildBlobSidecarsOriginal(blk, blobs, kzgProofs) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkBuildBlobSidecars_Optimized_4Blobs(b *testing.B) { + blk, blobs, kzgProofs := setupBenchmarkData(b, 4) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := BuildBlobSidecars(blk, blobs, kzgProofs) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkBuildBlobSidecars_Original_9Blobs(b *testing.B) { + blk, blobs, kzgProofs := setupBenchmarkData(b, 9) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := BuildBlobSidecarsOriginal(blk, blobs, kzgProofs) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkBuildBlobSidecars_Optimized_9Blobs(b *testing.B) { + blk, blobs, kzgProofs := setupBenchmarkData(b, 9) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := BuildBlobSidecars(blk, blobs, kzgProofs) + if err != nil { + b.Fatal(err) + } + } +} + +// Benchmark the individual components to understand where the improvements come from +func BenchmarkMerkleProofKZGCommitment_Original(b *testing.B) { + blk, _, _ := setupBenchmarkData(b, 4) + body := blk.Block().Body() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := 0; j < 4; j++ { + _, err := blocks.MerkleProofKZGCommitment(body, j) + if err != nil { + b.Fatal(err) + } + } + } +} + +func BenchmarkMerkleProofKZGCommitment_Optimized(b *testing.B) { + blk, _, _ := setupBenchmarkData(b, 4) + body := blk.Block().Body() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + // Pre-compute components once + components, err := blocks.PrecomputeMerkleProofComponents(body) + if err != nil { + b.Fatal(err) + } + + // Generate proofs for each index + for j := 0; j < 4; j++ { + _, err := blocks.MerkleProofKZGCommitmentFromComponents(components, j) + if err != nil { + b.Fatal(err) + } + } + } +} diff --git a/changelog/tt_sushi.md b/changelog/tt_sushi.md new file mode 100644 index 0000000000..8c437153de --- /dev/null +++ b/changelog/tt_sushi.md @@ -0,0 +1,3 @@ +### Changed + +- Optimize proposer inclusion proof calcuation by pre caching subtries diff --git a/consensus-types/blocks/kzg.go b/consensus-types/blocks/kzg.go index 8d04a29e9a..6cb3bc8b4f 100644 --- a/consensus-types/blocks/kzg.go +++ b/consensus-types/blocks/kzg.go @@ -25,6 +25,12 @@ var ( errInvalidInclusionProof = errors.New("invalid KZG commitment inclusion proof") ) +// MerkleProofComponents contains pre-computed components for efficient proof generation +type MerkleProofComponents struct { + kzgSubtree *trie.SparseMerkleTrie + topLevelProof [][]byte +} + // VerifyKZGInclusionProof verifies the Merkle proof in a Blob sidecar against // the beacon block body root. func VerifyKZGInclusionProof(blob ROBlob) error { @@ -80,6 +86,67 @@ func MerkleProofKZGCommitment(body interfaces.ReadOnlyBeaconBlockBody, index int return proof, nil } +// PrecomputeMerkleProofComponents pre-computes the expensive parts of Merkle proof generation +// that are shared across all blob indices for a given block body. +func PrecomputeMerkleProofComponents(body interfaces.ReadOnlyBeaconBlockBody) (*MerkleProofComponents, error) { + bodyVersion := body.Version() + if bodyVersion < version.Deneb { + return nil, errUnsupportedBeaconBlockBody + } + + // Pre-compute KZG subtree + commitments, err := body.BlobKzgCommitments() + if err != nil { + return nil, err + } + + // No work needed if there are no commitments + if len(commitments) == 0 { + return nil, nil + } + + leaves := LeavesFromCommitments(commitments) + kzgSubtree, err := trie.GenerateTrieFromItems(leaves, field_params.LogMaxBlobCommitments) + if err != nil { + return nil, err + } + + // Pre-compute top-level components + membersRoots, err := topLevelRoots(body) + if err != nil { + return nil, err + } + topLevelTrie, err := trie.GenerateTrieFromItems(membersRoots, logBodyLength) + if err != nil { + return nil, err + } + topLevelProof, err := topLevelTrie.MerkleProof(kzgPosition) + if err != nil { + return nil, err + } + // Remove the last element that is not needed in topProof + topLevelProof = topLevelProof[:len(topLevelProof)-1] + + return &MerkleProofComponents{ + kzgSubtree: kzgSubtree, + topLevelProof: topLevelProof, + }, nil +} + +// MerkleProofKZGCommitmentFromComponents constructs a Merkle proof for a specific index +// using pre-computed components, avoiding redundant calculations. +func MerkleProofKZGCommitmentFromComponents(components *MerkleProofComponents, index int) ([][]byte, error) { + // Generate index-specific proof from pre-computed KZG subtree + subtreeProof, err := components.kzgSubtree.MerkleProof(index) + if err != nil { + return nil, err + } + + // Combine with pre-computed top-level proof + proof := append(subtreeProof, components.topLevelProof...) + return proof, nil +} + // MerkleProofKZGCommitments constructs a Merkle proof of inclusion of the KZG // commitments into the Beacon Block with the given `body` func MerkleProofKZGCommitments(body interfaces.ReadOnlyBeaconBlockBody) ([][]byte, error) {