From a85df38d0f47d6340a8ba78f7e56fa2249986fad Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Wed, 16 Oct 2024 07:48:33 -0500 Subject: [PATCH] [3/3] - Replace Legacy History Commitment Implementation (#686) Co-authored-by: Pepper Lebeck-Jobe --- chain-abstraction/interfaces.go | 6 +- .../edge_challenge_manager.go | 6 +- .../edge_challenge_manager_test.go | 8 +- challenge-manager/edge-tracker/tracker.go | 26 +- .../history_commitment_provider.go | 105 +-- layer2-state-provider/provider.go | 18 +- math/intlog2.go | 17 +- math/intlog2_test.go | 86 ++- state-commitments/history/BUILD.bazel | 12 +- .../history/history_commitment.go | 614 ++++++++++++++++++ .../history/history_commitment_test.go | 369 +++++++++++ .../inclusion-proofs/BUILD.bazel | 7 +- state-commitments/legacy/BUILD.bazel | 25 + .../commitments.go => legacy/legacy.go} | 15 +- .../legacy_test.go} | 4 +- .../optimized/history_commitment.go | 511 --------------- .../optimized/inclusion_proof.go | 113 ---- state-commitments/prefix-proofs/BUILD.bazel | 13 +- .../integration}/BUILD.bazel | 22 +- .../integration/prefixproofs_test.go | 286 +------- testing/mocks/mocks.go | 10 +- .../state-provider/layer2_state_provider.go | 12 +- 22 files changed, 1234 insertions(+), 1051 deletions(-) create mode 100644 state-commitments/history/history_commitment.go create mode 100644 state-commitments/history/history_commitment_test.go create mode 100644 state-commitments/legacy/BUILD.bazel rename state-commitments/{history/commitments.go => legacy/legacy.go} (90%) rename state-commitments/{history/commitments_test.go => legacy/legacy_test.go} (95%) delete mode 100644 state-commitments/optimized/history_commitment.go delete mode 100644 state-commitments/optimized/inclusion_proof.go rename {state-commitments/optimized => testing/integration}/BUILD.bazel (53%) rename state-commitments/optimized/history_commitment_test.go => testing/integration/prefixproofs_test.go (50%) diff --git a/chain-abstraction/interfaces.go b/chain-abstraction/interfaces.go index 27a7b485b..43ed88a3b 100644 --- a/chain-abstraction/interfaces.go +++ b/chain-abstraction/interfaces.go @@ -12,7 +12,7 @@ import ( "github.com/OffchainLabs/bold/containers/option" "github.com/OffchainLabs/bold/solgen/go/rollupgen" - commitments "github.com/OffchainLabs/bold/state-commitments/history" + "github.com/OffchainLabs/bold/state-commitments/history" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" @@ -289,7 +289,7 @@ type SpecChallengeManager interface { ctx context.Context, assertion Assertion, startCommit, - endCommit commitments.History, + endCommit history.History, startEndPrefixProof []byte, ) (VerifiedRoyalEdge, error) // Adds a level-zero edge to subchallenge given a source edge and history commitments. @@ -297,7 +297,7 @@ type SpecChallengeManager interface { ctx context.Context, challengedEdge SpecEdge, startCommit, - endCommit commitments.History, + endCommit history.History, startParentInclusionProof []common.Hash, endParentInclusionProof []common.Hash, startEndPrefixProof []byte, diff --git a/chain-abstraction/sol-implementation/edge_challenge_manager.go b/chain-abstraction/sol-implementation/edge_challenge_manager.go index 01e62940a..2e2398672 100644 --- a/chain-abstraction/sol-implementation/edge_challenge_manager.go +++ b/chain-abstraction/sol-implementation/edge_challenge_manager.go @@ -17,7 +17,7 @@ import ( "github.com/OffchainLabs/bold/solgen/go/challengeV2gen" "github.com/OffchainLabs/bold/solgen/go/ospgen" "github.com/OffchainLabs/bold/solgen/go/rollupgen" - commitments "github.com/OffchainLabs/bold/state-commitments/history" + "github.com/OffchainLabs/bold/state-commitments/history" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" @@ -916,7 +916,7 @@ func (cm *specChallengeManager) AddBlockChallengeLevelZeroEdge( ctx context.Context, assertion protocol.Assertion, startCommit, - endCommit commitments.History, + endCommit history.History, startEndPrefixProof []byte, ) (protocol.VerifiedRoyalEdge, error) { assertionCreation, err := cm.assertionChain.ReadAssertionCreationInfo(ctx, assertion.Id()) @@ -1051,7 +1051,7 @@ func (cm *specChallengeManager) AddSubChallengeLevelZeroEdge( ctx context.Context, challengedEdge protocol.SpecEdge, startCommit, - endCommit commitments.History, + endCommit history.History, startParentInclusionProof, endParentInclusionProof []common.Hash, startEndPrefixProof []byte, diff --git a/chain-abstraction/sol-implementation/edge_challenge_manager_test.go b/chain-abstraction/sol-implementation/edge_challenge_manager_test.go index 671b02861..3372ea280 100644 --- a/chain-abstraction/sol-implementation/edge_challenge_manager_test.go +++ b/chain-abstraction/sol-implementation/edge_challenge_manager_test.go @@ -13,7 +13,7 @@ import ( l2stateprovider "github.com/OffchainLabs/bold/layer2-state-provider" "github.com/OffchainLabs/bold/solgen/go/mocksgen" "github.com/OffchainLabs/bold/solgen/go/rollupgen" - commitments "github.com/OffchainLabs/bold/state-commitments/history" + "github.com/OffchainLabs/bold/state-commitments/history" challenge_testing "github.com/OffchainLabs/bold/testing" stateprovider "github.com/OffchainLabs/bold/testing/mocks/state-provider" "github.com/OffchainLabs/bold/testing/setup" @@ -681,8 +681,8 @@ type bisectionScenario struct { evilStateManager l2stateprovider.Provider honestLevelZeroEdge protocol.SpecEdge evilLevelZeroEdge protocol.SpecEdge - honestStartCommit commitments.History - evilStartCommit commitments.History + honestStartCommit history.History + evilStartCommit history.History } func setupBisectionScenario( @@ -699,7 +699,7 @@ func setupBisectionScenario( require.NoError(t, err) // Honest assertion being added. - leafAdder := func(stateManager l2stateprovider.Provider, leaf protocol.Assertion) (commitments.History, protocol.SpecEdge) { + leafAdder := func(stateManager l2stateprovider.Provider, leaf protocol.Assertion) (history.History, protocol.SpecEdge) { req := &l2stateprovider.HistoryCommitmentRequest{ WasmModuleRoot: common.Hash{}, FromBatch: 0, diff --git a/challenge-manager/edge-tracker/tracker.go b/challenge-manager/edge-tracker/tracker.go index e6992429c..7f423dbfc 100644 --- a/challenge-manager/edge-tracker/tracker.go +++ b/challenge-manager/edge-tracker/tracker.go @@ -18,7 +18,7 @@ import ( l2stateprovider "github.com/OffchainLabs/bold/layer2-state-provider" "github.com/OffchainLabs/bold/math" retry "github.com/OffchainLabs/bold/runtime" - commitments "github.com/OffchainLabs/bold/state-commitments/history" + "github.com/OffchainLabs/bold/state-commitments/history" utilTime "github.com/OffchainLabs/bold/time" "github.com/ethereum/go-ethereum/common" gethtypes "github.com/ethereum/go-ethereum/core/types" @@ -471,12 +471,12 @@ func (et *Tracker) tryToConfirmEdge(ctx context.Context) (bool, error) { // commitment with a prefix proof for the action based on the challenge type. func (et *Tracker) DetermineBisectionHistoryWithProof( ctx context.Context, -) (commitments.History, []byte, error) { +) (history.History, []byte, error) { startHeight, _ := et.edge.StartCommitment() endHeight, _ := et.edge.EndCommitment() bisectTo, err := math.Bisect(uint64(startHeight), uint64(endHeight)) if err != nil { - return commitments.History{}, nil, errors.Wrapf(err, "determining bisection point errored for %d and %d", startHeight, endHeight) + return history.History{}, nil, errors.Wrapf(err, "determining bisection point errored for %d and %d", startHeight, endHeight) } challengeLevel := et.edge.GetChallengeLevel() if challengeLevel == protocol.NewBlockChallengeLevel() { @@ -492,7 +492,7 @@ func (et *Tracker) DetermineBisectionHistoryWithProof( }, ) if commitErr != nil { - return commitments.History{}, nil, commitErr + return history.History{}, nil, commitErr } proof, proofErr := et.stateProvider.PrefixProof( ctx, @@ -507,18 +507,18 @@ func (et *Tracker) DetermineBisectionHistoryWithProof( l2stateprovider.Height(bisectTo), ) if proofErr != nil { - return commitments.History{}, nil, proofErr + return history.History{}, nil, proofErr } return historyCommit, proof, nil } - var historyCommit commitments.History + var historyCommit history.History var commitErr error var proof []byte var proofErr error originHeights, err := et.edge.TopLevelClaimHeight(ctx) if err != nil { - return commitments.History{}, nil, err + return history.History{}, nil, err } challengeOriginHeights := make([]l2stateprovider.Height, len(originHeights.ChallengeOriginHeights)) for index, height := range originHeights.ChallengeOriginHeights { @@ -537,7 +537,7 @@ func (et *Tracker) DetermineBisectionHistoryWithProof( }, ) if commitErr != nil { - return commitments.History{}, nil, errors.Wrap(commitErr, "could not produce history commitment") + return history.History{}, nil, errors.Wrap(commitErr, "could not produce history commitment") } proof, proofErr = et.stateProvider.PrefixProof( ctx, @@ -552,7 +552,7 @@ func (et *Tracker) DetermineBisectionHistoryWithProof( l2stateprovider.Height(bisectTo), ) if proofErr != nil { - return commitments.History{}, nil, errors.Wrap(proofErr, "could not produce prefix proof") + return history.History{}, nil, errors.Wrap(proofErr, "could not produce prefix proof") } return historyCommit, proof, nil } @@ -601,10 +601,10 @@ func (et *Tracker) openSubchallengeLeaf(ctx context.Context) error { fields := et.uniqueTrackerLogFields() - var startHistory commitments.History - var endHistory commitments.History - var startParentCommitment commitments.History - var endParentCommitment commitments.History + var startHistory history.History + var endHistory history.History + var startParentCommitment history.History + var endParentCommitment history.History var startEndPrefixProof []byte challengeLevel := et.edge.GetChallengeLevel() switch challengeLevel { diff --git a/layer2-state-provider/history_commitment_provider.go b/layer2-state-provider/history_commitment_provider.go index 27b53ab5c..d2d5151ab 100644 --- a/layer2-state-provider/history_commitment_provider.go +++ b/layer2-state-provider/history_commitment_provider.go @@ -4,10 +4,12 @@ import ( "context" "fmt" "math/big" + "slices" "strconv" "time" protocol "github.com/OffchainLabs/bold/chain-abstraction" + "github.com/OffchainLabs/bold/state-commitments/history" prefixproofs "github.com/OffchainLabs/bold/state-commitments/prefix-proofs" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/metrics" @@ -15,7 +17,6 @@ import ( "github.com/OffchainLabs/bold/api" "github.com/OffchainLabs/bold/api/db" "github.com/OffchainLabs/bold/containers/option" - commitments "github.com/OffchainLabs/bold/state-commitments/history" "github.com/ethereum/go-ethereum/common" ) @@ -109,7 +110,7 @@ type HistoryCommitmentProvider struct { } // NewHistoryCommitmentProvider creates an instance of a struct which can compute history -// commitments over any number of challenge levels for BOLD. +// commitments over any number of challenge levels for BoLD. func NewHistoryCommitmentProvider( l2MessageStateCollector L2MessageStateCollector, machineHashCollector MachineHashCollector, @@ -136,18 +137,46 @@ func (p *HistoryCommitmentProvider) UpdateAPIDatabase(apiDB db.Database) { p.apiDB = apiDB } +// virtualFrom computes the virtual value for a history commitment +// +// I the optional h value is None, then based on the challenge level, and given +// slice of challenge origin heights (coh) determine the maximum number of +// leaves for that level and return it as virtual. +func (p *HistoryCommitmentProvider) virtualFrom(h option.Option[Height], coh []Height) (uint64, error) { + var virtual uint64 + if h.IsNone() { + validatedHeights, err := p.validateOriginHeights(coh) + if err != nil { + return 0, err + } + if len(validatedHeights) == 0 { + virtual = uint64(p.challengeLeafHeights[0]) + 1 + } else { + lvl := deepestRequestedChallengeLevel(validatedHeights) + virtual = uint64(p.challengeLeafHeights[lvl]) + 1 + } + } else { + virtual = uint64(h.Unwrap()) + 1 + } + return virtual, nil +} + // HistoryCommitment computes a Merklelized commitment over a set of hashes // at specified challenge levels. For block challenges, for example, this is a set // of machine hashes corresponding each message in a range N to M. func (p *HistoryCommitmentProvider) HistoryCommitment( ctx context.Context, req *HistoryCommitmentRequest, -) (commitments.History, error) { +) (history.History, error) { hashes, err := p.historyCommitmentImpl(ctx, req) if err != nil { - return commitments.History{}, err + return history.History{}, err + } + virtual, err := p.virtualFrom(req.UpToHeight, req.UpperChallengeOriginHeights) + if err != nil { + return history.History{}, err } - return commitments.New(hashes) + return history.NewCommitment(hashes, virtual) } func (p *HistoryCommitmentProvider) historyCommitmentImpl( @@ -266,7 +295,7 @@ func (p *HistoryCommitmentProvider) AgreesWithHistoryCommitment( historyCommitMetadata *HistoryCommitmentRequest, commit History, ) (bool, error) { - var localCommit commitments.History + var localCommit history.History var err error switch challengeLevel { case protocol.NewBlockChallengeLevel(): @@ -343,68 +372,44 @@ func (p *HistoryCommitmentProvider) PrefixProof( if err != nil { return nil, err } - // If no upToHeight is provided, we want to use the max number of leaves in our computation. - lowCommitmentNumLeaves := uint64(prefixHeight + 1) - var highCommitmentNumLeaves uint64 - if req.UpToHeight.IsNone() { - highCommitmentNumLeaves = uint64(len(leaves)) - } else { - // Else if it is provided, we expect the number of leaves to be the difference - // between the to and from height + 1. - upTo := req.UpToHeight.Unwrap() - if upTo < req.FromHeight { - return nil, fmt.Errorf("invalid range: end %d was < start %d", upTo, req.FromHeight) - } - highCommitmentNumLeaves = uint64(upTo) - uint64(req.FromHeight) + 1 - } - - // Validate we are within bounds of the leaves slice. - if highCommitmentNumLeaves > uint64(len(leaves)) { - return nil, fmt.Errorf("high prefix size out of bounds, got %d, leaves length %d", highCommitmentNumLeaves, len(leaves)) - } - - // Validate low vs high commitment. - if lowCommitmentNumLeaves > highCommitmentNumLeaves { - return nil, fmt.Errorf("low prefix size %d was greater than high prefix size %d", lowCommitmentNumLeaves, highCommitmentNumLeaves) - } - - prefixExpansion, err := prefixproofs.ExpansionFromLeaves(leaves[:lowCommitmentNumLeaves]) + virtual, err := p.virtualFrom(req.UpToHeight, req.UpperChallengeOriginHeights) if err != nil { return nil, err } - prefixProof, err := prefixproofs.GeneratePrefixProof( - lowCommitmentNumLeaves, - prefixExpansion, - leaves[lowCommitmentNumLeaves:highCommitmentNumLeaves], - prefixproofs.RootFetcherFromExpansion, - ) + // If no upToHeight is provided, we want to use the max number of leaves in our computation. + lowCommitmentNumLeaves := uint64(prefixHeight + 1) + // The prefix proof may be over a range of leaves that include virtual ones. + prefixLen := min(lowCommitmentNumLeaves, uint64(len(leaves))) + prefixHashes := slices.Clone(leaves[:prefixLen]) + prefixRoot, err := history.ComputeRoot(prefixHashes, lowCommitmentNumLeaves) if err != nil { return nil, err } - bigCommit, err := commitments.New(leaves[:highCommitmentNumLeaves]) + fullTreeHashes := slices.Clone(leaves) + fullTreeRoot, err := history.ComputeRoot(fullTreeHashes, virtual) if err != nil { return nil, err } - - prefixCommit, err := commitments.New(leaves[:lowCommitmentNumLeaves]) + hashesForProof := make([]common.Hash, len(leaves)) + for i := uint64(0); i < uint64(len(leaves)); i++ { + hashesForProof[i] = leaves[i] + } + prefixExp, proof, err := history.GeneratePrefixProof(uint64(prefixHeight), hashesForProof, virtual) if err != nil { return nil, err } - _, numRead := prefixproofs.MerkleExpansionFromCompact(prefixProof, lowCommitmentNumLeaves) - onlyProof := prefixProof[numRead:] - // We verify our prefix proof before an onchain submission as an extra safety-check. if err = prefixproofs.VerifyPrefixProof(&prefixproofs.VerifyPrefixProofConfig{ - PreRoot: prefixCommit.Merkle, + PreRoot: prefixRoot, PreSize: lowCommitmentNumLeaves, - PostRoot: bigCommit.Merkle, - PostSize: highCommitmentNumLeaves, - PreExpansion: prefixExpansion, - PrefixProof: onlyProof, + PostRoot: fullTreeRoot, + PostSize: virtual, + PreExpansion: prefixExp, + PrefixProof: proof, }); err != nil { return nil, fmt.Errorf("could not verify prefix proof locally: %w", err) } - return ProofArgs.Pack(&prefixExpansion, &onlyProof) + return ProofArgs.Pack(&prefixExp, &proof) } func (p *HistoryCommitmentProvider) OneStepProofData( diff --git a/layer2-state-provider/provider.go b/layer2-state-provider/provider.go index e95636114..2bd390b54 100644 --- a/layer2-state-provider/provider.go +++ b/layer2-state-provider/provider.go @@ -13,7 +13,7 @@ import ( protocol "github.com/OffchainLabs/bold/chain-abstraction" "github.com/OffchainLabs/bold/containers/option" - commitments "github.com/OffchainLabs/bold/state-commitments/history" + "github.com/OffchainLabs/bold/state-commitments/history" "github.com/ethereum/go-ethereum/common" ) @@ -22,14 +22,14 @@ var ErrChainCatchingUp = errors.New("chain is catching up to the execution state // Batch index for an Arbitrum L2 state. type Batch uint64 -// Height for a BOLD history commitment. +// Height for a BoLD history commitment. type Height uint64 // OpcodeIndex within an Arbitrator machine for an L2 message. type OpcodeIndex uint64 // StepSize is the number of opcode increments used for stepping through -// machines for BOLD challenges. +// machines for BoLD challenges. type StepSize uint64 // ConfigSnapshot for an assertion on Arbitrum. @@ -41,6 +41,11 @@ type ConfigSnapshot struct { InboxMaxCount *big.Int } +type History struct { + Height uint64 + MerkleRoot common.Hash +} + // Provider defines an L2 state backend that can provide history commitments, execution // states, prefix proofs, and more for the BOLD protocol. type Provider interface { @@ -86,7 +91,7 @@ type GeneralHistoryCommitter interface { HistoryCommitment( ctx context.Context, req *HistoryCommitmentRequest, - ) (commitments.History, error) + ) (history.History, error) } type GeneralPrefixProver interface { @@ -109,11 +114,6 @@ type OneStepProofProvider interface { ) (data *protocol.OneStepData, startLeafInclusionProof, endLeafInclusionProof []common.Hash, err error) } -type History struct { - Height uint64 - MerkleRoot common.Hash -} - type HistoryChecker interface { AgreesWithHistoryCommitment( ctx context.Context, diff --git a/math/intlog2.go b/math/intlog2.go index daa98f5aa..f4216d48a 100644 --- a/math/intlog2.go +++ b/math/intlog2.go @@ -2,10 +2,23 @@ package math import "math/bits" -// Log2 returns the integer logarithm base 2 of u (rounded down). -func Log2(u uint64) int { +// Log2Floor returns the integer logarithm base 2 of u (rounded down). +func Log2Floor(u uint64) int { if u == 0 { panic("log2 undefined for non-positive values") } return bits.Len64(u) - 1 } + +// Log2Ceil returns the integer logarithm base 2 of u (rounded up). +func Log2Ceil(u uint64) int { + r := Log2Floor(u) + if isPowerOfTwo(u) { + return r + } + return r + 1 +} + +func isPowerOfTwo(u uint64) bool { + return u&(u-1) == 0 +} diff --git a/math/intlog2_test.go b/math/intlog2_test.go index 9b4bc5a0a..5eb598964 100644 --- a/math/intlog2_test.go +++ b/math/intlog2_test.go @@ -11,7 +11,9 @@ import ( "github.com/stretchr/testify/require" ) -func TestUnsingedIntegerLog2(t *testing.T) { +var benchResult int + +func TestUnsingedIntegerLog2Floor(t *testing.T) { type log2TestCase struct { input uint64 expected int @@ -27,19 +29,19 @@ func TestUnsingedIntegerLog2(t *testing.T) { } for _, tc := range testCases { t.Run(fmt.Sprintf("%d", tc.input), func(t *testing.T) { - res := Log2(tc.input) + res := Log2Floor(tc.input) require.Equal(t, tc.expected, res) }) } } -func TestUnsingedIntegerLog2PanicsOnZero(t *testing.T) { +func TestUnsingedIntegerLog2FloorPanicsOnZero(t *testing.T) { require.Panics(t, func() { - Log2(0) + Log2Floor(0) }) } -func FuzzUnsingedIntegerLog2(f *testing.F) { +func FuzzUnsingedIntegerLog2Floor(f *testing.F) { testcases := []uint64{0, 2, 4, 6, 8} for _, tc := range testcases { f.Add(tc) @@ -47,30 +49,90 @@ func FuzzUnsingedIntegerLog2(f *testing.F) { f.Fuzz(func(t *testing.T, input uint64) { if input == 0 { require.Panics(t, func() { - Log2(input) + Log2Floor(input) }) t.Skip() } - r := Log2(input) + r := Log2Floor(input) fr := math.Log2(float64(input)) require.Equal(t, int(math.Floor(fr)), r) }) } -var benchResult int - -func BenchmarkUnsingedIntegerLog2(b *testing.B) { +func BenchmarkUnsingedIntegerLog2Floor(b *testing.B) { var r int for i := 1; i < b.N; i++ { - r = Log2(uint64(i)) + r = Log2Floor(uint64(i)) } benchResult = r } -func BenchmarkMathLog2(b *testing.B) { +func BenchmarkMathLog2Floor(b *testing.B) { var r int for i := 1; i < b.N; i++ { r = int(math.Log2(float64(i))) } benchResult = r } + +func TestUnsingedIntegerLog2Ceil(t *testing.T) { + type log2TestCase struct { + input uint64 + expected int + } + + testCases := []log2TestCase{ + {input: 1, expected: 0}, + {input: 2, expected: 1}, + {input: 4, expected: 2}, + {input: 6, expected: 3}, + {input: 8, expected: 3}, + {input: 24601, expected: 15}, + } + for _, tc := range testCases { + t.Run(fmt.Sprintf("%d", tc.input), func(t *testing.T) { + res := Log2Ceil(tc.input) + require.Equal(t, tc.expected, res) + }) + } +} + +func TestUnsingedIntegerLog2CeilPanicsOnZero(t *testing.T) { + require.Panics(t, func() { + Log2Ceil(0) + }) +} + +func FuzzUnsingedIntegerLog2Ceil(f *testing.F) { + testcases := []uint64{0, 2, 4, 6, 8} + for _, tc := range testcases { + f.Add(tc) + } + f.Fuzz(func(t *testing.T, input uint64) { + if input == 0 { + require.Panics(t, func() { + Log2Ceil(input) + }) + t.Skip() + } + r := Log2Ceil(input) + fr := math.Log2(float64(input)) + require.Equal(t, int(math.Ceil(fr)), r) + }) +} + +func BenchmarkUnsingedIntegerLog2Ceil(b *testing.B) { + var r int + for i := 1; i < b.N; i++ { + r = Log2Ceil(uint64(i)) + } + benchResult = r +} + +func BenchmarkMathLog2Ceil(b *testing.B) { + var r int + for i := 1; i < b.N; i++ { + r = int(math.Ceil(math.Log2(float64(i)))) + } + benchResult = r +} diff --git a/state-commitments/history/BUILD.bazel b/state-commitments/history/BUILD.bazel index 9f3ac2ed6..300b1b080 100644 --- a/state-commitments/history/BUILD.bazel +++ b/state-commitments/history/BUILD.bazel @@ -2,24 +2,26 @@ load("@rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "history", - srcs = ["commitments.go"], + srcs = ["history_commitment.go"], importpath = "github.com/OffchainLabs/bold/state-commitments/history", visibility = ["//visibility:public"], deps = [ - "//state-commitments/inclusion-proofs", - "//state-commitments/prefix-proofs", + "//math", "@com_github_ethereum_go_ethereum//common", + "@com_github_ethereum_go_ethereum//crypto", ], ) go_test( name = "history_test", size = "small", - srcs = ["commitments_test.go"], + srcs = ["history_commitment_test.go"], embed = [":history"], deps = [ - "//state-commitments/inclusion-proofs", + "//state-commitments/legacy", + "//state-commitments/prefix-proofs", "@com_github_ethereum_go_ethereum//common", + "@com_github_ethereum_go_ethereum//crypto", "@com_github_stretchr_testify//require", ], ) diff --git a/state-commitments/history/history_commitment.go b/state-commitments/history/history_commitment.go new file mode 100644 index 000000000..408ab87cc --- /dev/null +++ b/state-commitments/history/history_commitment.go @@ -0,0 +1,614 @@ +// Package history provides functions for computing merkle tree roots +// and proofs needed for the BoLD protocol's history commitments. +// +// Throughout this package, the following terms are used: +// +// - leaf: a leaf node in a merkle tree, which is a hash of some data. +// - virtual: the length of the desired number of leaf nodes. In the BoLD +// protocol, it is important that all history commitments which for a given +// challenge edge have the same length, even if the participants disagree +// about the number of blocks or steps to which they are committing. To +// solve this, history commitments must have fixed lengths at different +// challenge levels. Callers only need to provide the leaves they to which +// they commit, and the virtual length. The last leaf in the list is used +// to pad the tree to the virtual length. +// - limit: the length of the leaves that would be in a complete subtree +// of the depth required to hold the virtual leaves in a tree (or subtree) +// - pure tree: a tree where len(leaves) == virtual +// - complete tree: a tree where the number of leaves is a power of 2 +// - complete virtual tree: a tree where the number of leaves including the +// virtual padding is a power of 2 +// - partial tree: a tree where the number of leaves is not a power of 2 +// - partial virtual tree: a tree where the number of leaves including the +// virtual padding is not a power of 2 +// - empty hash: common.Hash{} +// Any time the root of a partial tree (either virtual or pure) is computed, +// the sibling node of the last node in a layer may be missing. In this case +// an empty hash (common.Hash{}) is used as the sibling node. +// Note: This is not the same as padding the leaves of the tree with +// common.Hash{} values. If that approach were taken, then the higher-level +// layers would contain the hash of the empty hash, or the hash of multiple +// empty hashes. This would be less efficient to calculate, and would not +// change expressiveness or security of the data structure, but it would +// produce a different root hash. +// - virtual node: a node in a virtual tree which is not one of the real +// leaves and not computed from the data in the real leaves. +package history + +import ( + "errors" + "fmt" + + "github.com/OffchainLabs/bold/math" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +var ( + emptyHash = common.Hash{} + emptyHistory = History{} +) + +// History represents a history commitment in the protocol. +type History struct { + Height uint64 + Merkle common.Hash + FirstLeaf common.Hash + LastLeaf common.Hash + LastLeafProof []common.Hash +} + +// treePosition tracks the current position in the merkle tree. +type treePosition struct { + // layer is the layer of the tree. + layer uint64 + // index is the index of the leaf in this layer of the tree. + index uint64 +} + +type historyCommitter struct { + fillers []common.Hash + keccak crypto.KeccakState + cursor treePosition + lastLeafProver *lastLeafProver +} + +func newCommitter() *historyCommitter { + return &historyCommitter{ + fillers: make([]common.Hash, 0), + keccak: crypto.NewKeccakState(), + } +} + +// soughtHash holds a pointer to the hash and whether it has been found. +// +// Without this type, it would be impossible to distinguish between a hash which +// has not been found and a hash which is the value of common.Hash{}. +// That's because the lastLeafProver's postions map is initialized with pointers +// to common.Hash{} values in a pre-allocated slice. +type soughtHash struct { + found bool + hash *common.Hash +} + +// lastLeafProver finds the siblings needed to produce a merkle inclusion +// proof for the last leaf in a virtual merkle tree. +// +// The prover maintains a map of treePositions where sibling nodes live +// and fills them in as the historyCommitter calculates them. +type lastLeafProver struct { + positions map[treePosition]*soughtHash + proof []common.Hash +} + +func newLastLeafProver(virtual uint64) (*lastLeafProver, error) { + positions, err := lastLeafProofPositions(virtual) + if err != nil { + return nil, err + } + posMap := make(map[treePosition]*soughtHash, len(positions)) + proof := make([]common.Hash, len(positions)) + for i, pos := range positions { + posMap[pos] = &soughtHash{false, &proof[i]} + } + return &lastLeafProver{ + positions: posMap, + proof: proof, + }, nil +} + +// handle filters the hashes found while computing the merkle root looking for +// the sibling nodes needed to produce the merkle inclusion proof, and fills +// them in the proof slice. +func (p *lastLeafProver) handle(hash common.Hash, pos treePosition) { + if sibling, ok := p.positions[pos]; ok { + sibling.found = true + *sibling.hash = hash + } +} + +// handle is called each time a hash is computed in the merkle tree. +// +// The cursor is kept in sync with tree traversal. The implementation of +// handle can therefore assume that the cursor is pointing to the node which +// has the value of the hash. +func (h *historyCommitter) handle(hash common.Hash) { + if h.lastLeafProver != nil { + h.lastLeafProver.handle(hash, h.cursor) + } +} + +// hash hashes the passed item into a common.Hash. +func (h *historyCommitter) hash(item ...*common.Hash) common.Hash { + var result common.Hash + h.hashInto(&result, item...) + return result +} + +// proof returns the merkle inclusion proof for the last leaf in a virtual tree. +// +// If the proof is not complete (i.e. some sibling nodes are missing), the +// sibling nodes are filled in with the fillers. +// +// The reason this works, is that the only nodes which are not visited when +// computing the merkle root are those which are in some complete virtual +// subtree. +func (h *historyCommitter) lastLeafProof() []common.Hash { + for pos, sibling := range h.lastLeafProver.positions { + if !sibling.found { + *h.lastLeafProver.positions[pos].hash = h.fillers[pos.layer] + } + } + if len(h.lastLeafProver.proof) == 0 { + return nil + } + return h.lastLeafProver.proof +} + +// hashInto hashes the concatenation of the passed items into the result. +// nolint:errcheck +func (h *historyCommitter) hashInto(result *common.Hash, items ...*common.Hash) { + defer h.keccak.Reset() + for _, item := range items { + h.keccak.Write(item[:]) // #nosec G104 - KeccakState.Write never errors + } + h.keccak.Read(result[:]) // #nosec G104 - KeccakState.Read never errors +} + +// NewCommitment produces a history commitment from a list of real leaves that +// are virtually padded using the last leaf in the list to some virtual length. +// +// Virtual must be >= len(leaves). +func NewCommitment(leaves []common.Hash, virtual uint64) (History, error) { + if len(leaves) == 0 { + return emptyHistory, errors.New("must commit to at least one leaf") + } + if virtual < uint64(len(leaves)) { + return emptyHistory, errors.New("virtual size must be >= len(leaves)") + } + comm := newCommitter() + firstLeaf := leaves[0] + lastLeaf := leaves[len(leaves)-1] + prover, err := newLastLeafProver(virtual) + if err != nil { + return emptyHistory, err + } + comm.lastLeafProver = prover + root, err := comm.computeRoot(leaves, virtual) + if err != nil { + return emptyHistory, err + } + lastLeafProof := comm.lastLeafProof() + return History{ + // Height is the relative height of the history commitment. + // It's the index of the last leaf in the tree. + Height: virtual - 1, + Merkle: root, + FirstLeaf: firstLeaf, + LastLeaf: lastLeaf, + LastLeafProof: lastLeafProof, + }, nil +} + +// ComputeRoot computes the merkle root of a virtual merkle tree. +func ComputeRoot(leaves []common.Hash, virtual uint64) (common.Hash, error) { + comm := newCommitter() + return comm.computeRoot(leaves, virtual) +} + +// GeneratePrefixProof generates a prefix proof for a given prefix index. +func GeneratePrefixProof(prefixIndex uint64, leaves []common.Hash, virtual uint64) ([]common.Hash, []common.Hash, error) { + comm := newCommitter() + return comm.generatePrefixProof(prefixIndex, leaves, virtual) +} + +// computeRoot computes the merkle root of a virtual merkle tree. +func (h *historyCommitter) computeRoot(leaves []common.Hash, virtual uint64) (common.Hash, error) { + lvLen := uint64(len(leaves)) + if lvLen == 0 { + return emptyHash, nil + } + hashed := h.hashLeaves(leaves) + limit := nextPowerOf2(virtual) + depth := uint(math.Log2Floor(limit)) + n := max(uint(math.Log2Ceil(virtual)), 1) + if err := h.populateFillers(&hashed[lvLen-1], n); err != nil { + return emptyHash, err + } + h.cursor = treePosition{layer: uint64(depth), index: 0} + return h.partialRoot(hashed, virtual, limit) +} + +// generatePrefixProof generates a prefix proof for a given prefix index. +// +// A prefix proof consists of the data needed to prove that a merkle root +// created from the leaves upto the prefix index represents a merkle tree which +// spans a specific prefix of the virtual merkle tree. +func (h *historyCommitter) generatePrefixProof(prefixIndex uint64, leaves []common.Hash, virtual uint64) ([]common.Hash, []common.Hash, error) { + hashed := h.hashLeaves(leaves) + prefixExpansion, proof, err := h.prefixAndProof(prefixIndex, hashed, virtual) + if err != nil { + return nil, nil, err + } + prefixExpansion = trimTrailingEmptyHashes(prefixExpansion) + proof = filterEmptyHashes(proof) + return prefixExpansion, proof, nil +} + +// hashLeaves returns a slice of hashes of the leaves +func (h *historyCommitter) hashLeaves(leaves []common.Hash) []common.Hash { + hashedLeaves := make([]common.Hash, len(leaves)) + for i := range leaves { + hashedLeaves[i] = h.hash(&leaves[i]) + } + return hashedLeaves +} + +// partialRoot returns the merkle root of a possibly partial hashtree where the +// first layer is passed as leaves, then padded by repeating the last leaf +// until it reaches virtual and terminated with a single common.Hash{}. +// +// limit is a power of 2 which is greater or equal to virtual, and defines how +// deep the complete tree analogous to this partial one would be. +// +// Implementation note: The historyCommitter's fillers member must be populated +// correctly before calling this method. There must be at least +// Log2FCeil(virtual) filler nodes to properly pad each layer of the tree if it +// is a partial virtual tree. +// +// The algorithm is split in three different logical cases: +// +// 1. If the virtual length is less than or equal to half the limit (this can +// never happen in the first iteration of the algorithm), the left half of +// the tree is computed by recursion and the right half is an empty hash. +// 2. If the leaves all fit in the left half, then both halves of the tree are +// computed by recursion. This is the most common starting scenario. +// There is a special case when the virtual length is equal to the limit, +// and the right half is a complete virtual tree. In this case, the right +// subtree is just a lookup in the precomputed fillers. +// 3. If the leaves do not fit in the left half, then both halves are computed +// by recursion. +func (h *historyCommitter) partialRoot(leaves []common.Hash, virtual, limit uint64) (common.Hash, error) { + lvLen := uint64(len(leaves)) + if lvLen == 0 { + return emptyHash, errors.New("nil leaves") + } + if uint64(virtual) < lvLen { + return emptyHash, fmt.Errorf("virtual %d should be >= num leaves %d", virtual, lvLen) + } + if limit < virtual { + return emptyHash, fmt.Errorf("limit %d should be >= virtual %d", limit, virtual) + } + minFillers := math.Log2Ceil(uint64(virtual)) + if len(h.fillers) < minFillers { + return emptyHash, fmt.Errorf("insufficient fillers, want %d, got %d", minFillers, len(h.fillers)) + } + if limit == 1 { + h.handle(leaves[0]) + return leaves[0], nil + } + + h.cursor.layer-- + var left, right common.Hash + var err error + mid := limit / 2 + + // Deal with the left child first + h.cursor.index *= 2 + var lLeaves []common.Hash + var lVirtual uint64 + if virtual > mid { + // Case 2 or 3: A complete subtree can be computed + lVirtual = mid + if lvLen > uint64(mid) { + // Case 3: A complete pure subtree can be computed + lLeaves = leaves[:mid] + } else { + // Case 2: A complete virtual subtree can be computed + lLeaves = leaves + } + } else { + // Case 1: A partial virtual tree can be computed + lLeaves = leaves + lVirtual = virtual + } + left, err = h.partialRoot(lLeaves, lVirtual, mid) + if err != nil { + return emptyHash, err + } + + // Deal with the right child + h.cursor.index++ + if virtual > mid { + // Case 2 or 3: The virtual size is greater than half the limit + if lvLen <= uint64(mid) && virtual == limit { + // This is a special case of 2 where the entire right subtree is + // made purely of virtual nodes, and it is a complete tree. + // So, the root of the subtree will be the precomputed filler + // at the current layer. + right = h.fillers[math.Log2Floor(uint64(mid))] + h.handle(right) + } else { + var rLeaves []common.Hash + if lvLen > uint64(mid) { + // Case 3: The leaves do not fit in the first half + rLeaves = leaves[mid:] + } else { + // Case 2: The leaves fit in the first half + rLeaves = []common.Hash{h.fillers[0]} + } + right, err = h.partialRoot(rLeaves, virtual-mid, mid) + if err != nil { + return emptyHash, err + } + } + } else { + // Case 1: The virtual size is less than half the limit + right = emptyHash + h.handle(right) + } + + h.hashInto(&leaves[0], &left, &right) + + // Restore the cursor layer to the state for this level of recursion + h.cursor.index /= 2 + h.cursor.layer++ + h.handle(leaves[0]) + + return leaves[0], nil +} + +func (h *historyCommitter) subtreeExpansion(leaves []common.Hash, virtual, limit uint64, stripped bool) (proof []common.Hash, err error) { + lvLen := uint64(len(leaves)) + if lvLen == 0 { + return make([]common.Hash, 0), nil + } + if virtual == 0 { + for i := limit; i > 1; i /= 2 { + proof = append(proof, emptyHash) + } + return + } + if limit == 0 { + limit = nextPowerOf2(virtual) + } + if limit == virtual { + left, err2 := h.partialRoot(leaves, limit, limit) + if err2 != nil { + return nil, err2 + } + if !stripped { + for i := limit; i > 1; i /= 2 { + proof = append(proof, emptyHash) + } + } + return append(proof, left), nil + } + mid := limit / 2 + if lvLen > mid { + left, err2 := h.partialRoot(leaves[:mid], mid, mid) + if err2 != nil { + return nil, err2 + } + proof, err = h.subtreeExpansion(leaves[mid:], virtual-mid, mid, stripped) + if err != nil { + return nil, err + } + return append(proof, left), nil + } + if virtual >= mid { + left, err2 := h.partialRoot(leaves, mid, mid) + if err2 != nil { + return nil, err2 + } + if len(h.fillers) == 0 { + return nil, errors.New("fillers is empty") + } + proof, err = h.subtreeExpansion([]common.Hash{h.fillers[0]}, virtual-mid, mid, stripped) + if err != nil { + return nil, err + } + return append(proof, left), nil + } + if stripped { + return h.subtreeExpansion(leaves, virtual, mid, stripped) + } + expac, err := h.subtreeExpansion(leaves, virtual, mid, stripped) + if err != nil { + return nil, err + } + return append(expac, emptyHash), nil +} + +func (h *historyCommitter) proof(index uint64, leaves []common.Hash, virtual, limit uint64) (tail []common.Hash, err error) { + lvLen := uint64(len(leaves)) + if lvLen == 0 { + return nil, errors.New("empty leaves slice") + } + if limit == 0 { + limit = nextPowerOf2(virtual) + } + if limit == 1 { + // Can only reach this with index == 0 + return + } + mid := limit / 2 + if index >= mid { + if lvLen > mid { + return h.proof(index-mid, leaves[mid:], virtual-mid, mid) + } + if len(h.fillers) == 0 { + return nil, errors.New("fillers is empty") + } + return h.proof(index-mid, []common.Hash{h.fillers[0]}, virtual-mid, mid) + } + if lvLen > mid { + tail, err = h.proof(index, leaves[:mid], mid, mid) + if err != nil { + return nil, err + } + right, err2 := h.subtreeExpansion(leaves[mid:], virtual-mid, mid, true) + if err2 != nil { + return nil, err2 + } + for i := len(right) - 1; i >= 0; i-- { + tail = append(tail, right[i]) + } + return tail, nil + } + if virtual > mid { + tail, err = h.proof(index, leaves, mid, mid) + if err != nil { + return nil, err + } + if len(h.fillers) == 0 { + return nil, errors.New("fillers is empty") + } + right, err := h.subtreeExpansion([]common.Hash{h.fillers[0]}, virtual-mid, mid, true) + if err != nil { + return nil, err + } + for i := len(right) - 1; i >= 0; i-- { + tail = append(tail, right[i]) + } + return tail, nil + } + return h.proof(index, leaves, virtual, mid) +} + +func (h *historyCommitter) prefixAndProof(index uint64, leaves []common.Hash, virtual uint64) (prefix []common.Hash, tail []common.Hash, err error) { + lvLen := uint64(len(leaves)) + if lvLen == 0 { + return nil, nil, errors.New("nil leaves") + } + if virtual == 0 { + return nil, nil, errors.New("virtual size cannot be zero") + } + if lvLen > virtual { + return nil, nil, fmt.Errorf("num leaves %d should be <= virtual %d", lvLen, virtual) + } + if index+1 > virtual { + return nil, nil, fmt.Errorf("index %d + 1 should be <= virtual %d", index, virtual) + } + logVirtual := uint(math.Log2Floor(virtual) + 1) + if err = h.populateFillers(&leaves[lvLen-1], logVirtual); err != nil { + return nil, nil, err + } + + if index+1 > lvLen { + prefix, err = h.subtreeExpansion(leaves, index+1, 0, false) + } else { + prefix, err = h.subtreeExpansion(leaves[:index+1], index+1, 0, false) + } + if err != nil { + return nil, nil, err + } + tail, err = h.proof(index, leaves, virtual, 0) + return +} + +// populateFillers returns a slice built recursively as +// ret[0] = the passed in leaf +// ret[i+1] = Hash(ret[i] + ret[i]) +// +// Allocates n hashes +// Computes n-1 hashes +// Copies 1 hash +func (h *historyCommitter) populateFillers(leaf *common.Hash, n uint) error { + if leaf == nil { + return errors.New("nil leaf pointer") + } + h.fillers = make([]common.Hash, n) + copy(h.fillers[0][:], (*leaf)[:]) + for i := uint(1); i < n; i++ { + h.hashInto(&h.fillers[i], &h.fillers[i-1], &h.fillers[i-1]) + } + return nil +} + +// lastLeafProofPositions returns the positions in a virtual merkle tree +// of the sibling nodes that need to be hashed with the last leaf at each +// layer to compute the root of the tree. +func lastLeafProofPositions(virtual uint64) ([]treePosition, error) { + if virtual == 0 { + return nil, errors.New("virtual size cannot be zero") + } + if virtual == 1 { + return []treePosition{}, nil + } + limit := nextPowerOf2(uint64(virtual)) + depth := math.Log2Floor(limit) + positions := make([]treePosition, depth) + idx := uint64(virtual) - 1 + for l := range positions { + positions[l] = sibling(idx, uint64(l)) + idx = parent(idx) + } + return positions, nil +} + +// sibling returns the position of the sibling of the node at the given layer +func sibling(index, layer uint64) treePosition { + return treePosition{layer: layer, index: index ^ 1} +} + +// parent returns the index of the parent of the node in the next higher layer +func parent(index uint64) uint64 { + return index >> 1 +} + +func nextPowerOf2(n uint64) uint64 { + if n == 0 { + return 1 + } + n-- // Decrement n to handle the case where n is a power of 2 + n |= n >> 1 // Propagate the highest bit set + n |= n >> 2 + n |= n >> 4 + n |= n >> 8 + n |= n >> 16 + n |= n >> 32 + return n + 1 // Increment n to get the next power of 2 +} + +func trimTrailingEmptyHashes(hashes []common.Hash) []common.Hash { + // Start from the end of the slice + for i := len(hashes) - 1; i >= 0; i-- { + if hashes[i] != emptyHash { + return hashes[:i+1] + } + } + // If all elements are zero, return an empty slice + return []common.Hash{} +} + +func filterEmptyHashes(hashes []common.Hash) []common.Hash { + newHashes := make([]common.Hash, 0, len(hashes)) + for _, h := range hashes { + if h == emptyHash { + continue + } + newHashes = append(newHashes, h) + } + return newHashes +} diff --git a/state-commitments/history/history_commitment_test.go b/state-commitments/history/history_commitment_test.go new file mode 100644 index 000000000..6d19cdc2a --- /dev/null +++ b/state-commitments/history/history_commitment_test.go @@ -0,0 +1,369 @@ +package history + +import ( + "fmt" + "testing" + + "github.com/OffchainLabs/bold/state-commitments/legacy" + prefixproofs "github.com/OffchainLabs/bold/state-commitments/prefix-proofs" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" +) + +func FuzzHistoryCommitter(f *testing.F) { + simpleHash := crypto.Keccak256Hash([]byte("foo")) + f.Fuzz(func(t *testing.T, numReal uint64, virtual uint64, limit uint64) { + // Set some bounds. + numReal = numReal % (1 << 10) + virtual = virtual % (1 << 20) + hashedLeaves := make([]common.Hash, numReal) + for i := range hashedLeaves { + hashedLeaves[i] = simpleHash + } + _, err := NewCommitment(hashedLeaves, virtual) + if err != nil { + if len(hashedLeaves) == 0 || virtual < uint64(len(hashedLeaves)) { + t.Skip() + } + t.Errorf("NewCommitment(%v, %d): err %v\n", hashedLeaves, virtual, err) + } + _ = err + }) +} + +func BenchmarkPrefixProofGeneration_Legacy(b *testing.B) { + for i := 0; i < b.N; i++ { + prefixIndex := 13384 + simpleHash := crypto.Keccak256Hash([]byte("foo")) + hashes := make([]common.Hash, 1<<14) + for i := 0; i < len(hashes); i++ { + hashes[i] = simpleHash + } + + lowCommitmentNumLeaves := prefixIndex + 1 + hiCommitmentNumLeaves := (1 << 14) + prefixExpansion, err := prefixproofs.ExpansionFromLeaves(hashes[:lowCommitmentNumLeaves]) + require.NoError(b, err) + _, err = prefixproofs.GeneratePrefixProof( + uint64(lowCommitmentNumLeaves), + prefixExpansion, + hashes[lowCommitmentNumLeaves:hiCommitmentNumLeaves], + prefixproofs.RootFetcherFromExpansion, + ) + require.NoError(b, err) + } +} + +func BenchmarkPrefixProofGeneration_Optimized(b *testing.B) { + b.StopTimer() + simpleHash := crypto.Keccak256Hash([]byte("foo")) + hashes := []common.Hash{crypto.Keccak256Hash(simpleHash[:])} + prefixIndex := uint64(13384) + virtual := uint64(1 << 14) + committer := newCommitter() + b.StartTimer() + for i := 0; i < b.N; i++ { + _, _, err := committer.generatePrefixProof(prefixIndex, hashes, virtual) + require.NoError(b, err) + } +} + +func TestSimpleHistoryCommitment(t *testing.T) { + aLeaf := common.HexToHash("0xA") + bLeaf := common.HexToHash("0xB") + // Level 0 + aHash := crypto.Keccak256Hash(aLeaf[:]) + bHash := crypto.Keccak256Hash(bLeaf[:]) + // Level 1 + abHash := crypto.Keccak256Hash(append(aHash[:], bHash[:]...)) + bzHash := crypto.Keccak256Hash(append(bHash[:], emptyHash[:]...)) + bbHash := crypto.Keccak256Hash(append(bHash[:], bHash[:]...)) + // Level 2 + abbzHash := crypto.Keccak256Hash(append(abHash[:], bzHash[:]...)) + abbbHash := crypto.Keccak256Hash(append(abHash[:], bbHash[:]...)) + ababHash := crypto.Keccak256Hash(append(abHash[:], abHash[:]...)) + bbbbHash := crypto.Keccak256Hash(append(bbHash[:], bbHash[:]...)) + // Level 3 + ababbbbbHash := crypto.Keccak256Hash(append(ababHash[:], bbbbHash[:]...)) + abababbbHash := crypto.Keccak256Hash(append(ababHash[:], abbbHash[:]...)) + + tests := []struct { + name string + lvs []common.Hash + virt uint64 + want common.Hash + }{ + { + name: "empty leaves", + lvs: []common.Hash{}, + virt: 0, + want: emptyHash, + }, + { + name: "single leaf", + lvs: []common.Hash{aLeaf}, + virt: 1, + want: aHash, + }, + { + name: "two leaves", + lvs: []common.Hash{aLeaf, bLeaf}, + virt: 2, + want: abHash, + }, + { + name: "two leaves - virtual 3", + lvs: []common.Hash{aLeaf, bLeaf}, + virt: 3, + want: abbzHash, + }, + { + name: "two leaves - virtual 4", + lvs: []common.Hash{aLeaf, bLeaf}, + virt: 4, + want: abbbHash, + }, + { + name: "four leaves", + lvs: []common.Hash{aLeaf, bLeaf, aLeaf, bLeaf}, + virt: 4, + want: ababHash, + }, + { + name: "four leaves - virtual 8", + lvs: []common.Hash{aLeaf, bLeaf, aLeaf, bLeaf}, + virt: 8, + want: ababbbbbHash, + }, + { + name: "six leaves - virtual 8", + lvs: []common.Hash{aLeaf, bLeaf, aLeaf, bLeaf, aLeaf, bLeaf}, + virt: 8, + want: abababbbHash, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + hc := newCommitter() + got, err := hc.computeRoot(tc.lvs, tc.virt) + if err != nil { + t.Errorf("ComputeRoot(%v, %d): err %v\n", tc.lvs, tc.virt, err) + } + if got != tc.want { + t.Errorf("ComputeRoot(%v, %d): got %s, want %s\n", tc.lvs, tc.virt, got.Hex(), tc.want.Hex()) + } + }) + } +} + +func TestLegacyVsOptimized(t *testing.T) { + t.Parallel() + end := uint64(1 << 6) + simpleHash := crypto.Keccak256Hash([]byte("foo")) + for i := uint64(1); i < end; i++ { + limit := nextPowerOf2(i) + for j := i; j < limit; j++ { + inputLeaves := make([]common.Hash, i) + for i := range inputLeaves { + inputLeaves[i] = simpleHash + } + committer := newCommitter() + computedRoot, err := committer.computeRoot(inputLeaves, uint64(j)) + require.NoError(t, err) + + legacyInputLeaves := make([]common.Hash, j) + for i := range legacyInputLeaves { + legacyInputLeaves[i] = simpleHash + } + histCommit, err := legacy.NewLegacy(legacyInputLeaves) + require.NoError(t, err) + require.Equal(t, computedRoot, histCommit.Merkle) + } + } +} + +func TestLegacyVsOptimizedEdgeCases(t *testing.T) { + t.Parallel() + simpleHash := crypto.Keccak256Hash([]byte("foo")) + + tests := []struct { + realLength int + virtualLength int + }{ + {12, 14}, + {8, 10}, + {6, 6}, + {10, 16}, + {4, 8}, + {1, 5}, + {3, 5}, + {5, 5}, + {1023, 1024}, + {(1 << 14) - 7, (1 << 14) - 7}, + } + + for _, tt := range tests { + t.Run(fmt.Sprintf("real length %d, virtual %d", tt.realLength, tt.virtualLength), func(t *testing.T) { + inputLeaves := make([]common.Hash, tt.realLength) + for i := range inputLeaves { + inputLeaves[i] = simpleHash + } + committer := newCommitter() + computedRoot, err := committer.computeRoot(inputLeaves, uint64(tt.virtualLength)) + require.NoError(t, err) + + leaves := make([]common.Hash, tt.virtualLength) + for i := range leaves { + leaves[i] = simpleHash + } + histCommit, err := legacy.NewLegacy(leaves) + require.NoError(t, err) + require.Equal(t, computedRoot, histCommit.Merkle) + }) + } +} + +func TestVirtualSparse(t *testing.T) { + t.Parallel() + simpleHash := crypto.Keccak256Hash([]byte("foo")) + makeLeaves := func(n int) []common.Hash { + leaves := make([]common.Hash, n) + for i := range leaves { + leaves[i] = simpleHash + } + return leaves + } + tests := []struct { + name string + real []common.Hash + virt uint64 + full []common.Hash + }{ + { + name: "real 1, virtual 3", + real: makeLeaves(1), + virt: 3, + full: makeLeaves(3), + }, + { + name: "real 2, virtual 3", + real: makeLeaves(2), + virt: 3, + full: makeLeaves(3), + }, + { + name: "real 3, virtual 3", + real: makeLeaves(3), + virt: 3, + full: makeLeaves(3), + }, + { + name: "real 4, virtual 4", + real: makeLeaves(4), + virt: 4, + full: makeLeaves(4), + }, + { + name: "real 1, virtual 5", + real: makeLeaves(1), + virt: 5, + full: makeLeaves(5), + }, + { + name: "real 12, virtual 14", + real: makeLeaves(12), + virt: 14, + full: makeLeaves(14), + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + optCommit, err := NewCommitment(tc.real, tc.virt) + require.NoError(t, err) + + histCommit, err := legacy.NewLegacy(tc.full) + require.NoError(t, err) + require.Equal(t, histCommit.Merkle, optCommit.Merkle) + }) + } +} + +func TestMaximumDepthHistoryCommitment(t *testing.T) { + t.Parallel() + simpleHash := crypto.Keccak256Hash([]byte("foo")) + hashedLeaves := []common.Hash{ + simpleHash, + } + _, err := NewCommitment(hashedLeaves, 1<<26) + require.NoError(t, err) +} + +func BenchmarkMaximumDepthHistoryCommitment(b *testing.B) { + b.StopTimer() + simpleHash := crypto.Keccak256Hash([]byte("foo")) + hashedLeaves := []common.Hash{ + simpleHash, + } + b.StartTimer() + for i := 0; i < b.N; i++ { + _, err := ComputeRoot(hashedLeaves, 1<<26) + _ = err + } +} + +func TestInclusionProofEquivalence(t *testing.T) { + simpleHash := crypto.Keccak256Hash([]byte("foo")) + leaves := []common.Hash{ + simpleHash, + simpleHash, + simpleHash, + simpleHash, + } + commit, err := NewCommitment(leaves, 4) + require.NoError(t, err) + oldCommit, err := legacy.NewLegacy(leaves) + require.NoError(t, err) + require.Equal(t, commit.Merkle, oldCommit.Merkle) +} + +func TestHashInto(t *testing.T) { + simpleHash := crypto.Keccak256Hash([]byte("foo")) + leaves := []common.Hash{ + simpleHash, + simpleHash, + simpleHash, + simpleHash, + } + comm := newCommitter() + want := crypto.Keccak256Hash(simpleHash[:], simpleHash[:], simpleHash[:], simpleHash[:]) + var got common.Hash + comm.hashInto(&got, &leaves[0], &leaves[1], &leaves[2], &leaves[3]) + if got != want { + t.Errorf("got %s, want %s", got.Hex(), want.Hex()) + } +} + +func TestLastLeafProofPositions(t *testing.T) { + tests := []struct { + name string + virtual uint64 + want []treePosition + }{ + {"virtual 1", 1, []treePosition{}}, + {"virtual 2", 2, []treePosition{{0, 0}}}, + {"virtual 9", 9, []treePosition{ + {0, 9}, + {1, 5}, + {2, 3}, + {3, 0}, + }}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := lastLeafProofPositions(tc.virtual) + require.NoError(t, err) + require.Equal(t, tc.want, got) + }) + } +} diff --git a/state-commitments/inclusion-proofs/BUILD.bazel b/state-commitments/inclusion-proofs/BUILD.bazel index df5f32f1e..a0a659f6e 100644 --- a/state-commitments/inclusion-proofs/BUILD.bazel +++ b/state-commitments/inclusion-proofs/BUILD.bazel @@ -1,10 +1,15 @@ load("@rules_go//go:def.bzl", "go_library", "go_test") +package_group( + name = "friends", + packages = ["//state-commitments/legacy"], +) + go_library( name = "inclusion-proofs", srcs = ["inclusion_proofs.go"], importpath = "github.com/OffchainLabs/bold/state-commitments/inclusion-proofs", - visibility = ["//visibility:public"], + visibility = [":friends"], deps = [ "//state-commitments/prefix-proofs", "@com_github_ethereum_go_ethereum//common", diff --git a/state-commitments/legacy/BUILD.bazel b/state-commitments/legacy/BUILD.bazel new file mode 100644 index 000000000..332a17a52 --- /dev/null +++ b/state-commitments/legacy/BUILD.bazel @@ -0,0 +1,25 @@ +load("@rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "legacy", + srcs = ["legacy.go"], + importpath = "github.com/OffchainLabs/bold/state-commitments/legacy", + visibility = ["//visibility:public"], + deps = [ + "//state-commitments/inclusion-proofs", + "//state-commitments/prefix-proofs", + "@com_github_ethereum_go_ethereum//common", + ], +) + +go_test( + name = "legacy_test", + size = "small", + srcs = ["legacy_test.go"], + embed = [":legacy"], + deps = [ + "//state-commitments/inclusion-proofs", + "@com_github_ethereum_go_ethereum//common", + "@com_github_stretchr_testify//require", + ], +) diff --git a/state-commitments/history/commitments.go b/state-commitments/legacy/legacy.go similarity index 90% rename from state-commitments/history/commitments.go rename to state-commitments/legacy/legacy.go index b42fb034b..50b88337f 100644 --- a/state-commitments/history/commitments.go +++ b/state-commitments/legacy/legacy.go @@ -3,29 +3,30 @@ // // Copyright 2023, Offchain Labs, Inc. // For license information, see https://github.com/offchainlabs/bold/blob/main/LICENSE -package history +package legacy import ( "errors" - prefixproofs "github.com/OffchainLabs/bold/state-commitments/prefix-proofs" "sync" + prefixproofs "github.com/OffchainLabs/bold/state-commitments/prefix-proofs" + inclusionproofs "github.com/OffchainLabs/bold/state-commitments/inclusion-proofs" "github.com/ethereum/go-ethereum/common" ) var ( - emptyCommit = History{} + emptyCommit = LegacyHistory{} ) -// History defines a Merkle accumulator over a list of leaves, which +// LegacyHistory defines a Merkle accumulator over a list of leaves, which // are understood to be state roots in the goimpl. A history commitment contains // a "height" value, which can refer to a height of an assertion in the assertions // tree, or a "step" of WAVM states in a big step or small step subchallenge. // A commitment contains a Merkle root over the list of leaves, and can optionally // provide a proof that the last leaf in the accumulator Merkleizes into the // specified root hash, which is required when verifying challenge creation invariants. -type History struct { +type LegacyHistory struct { Height uint64 Merkle common.Hash FirstLeaf common.Hash @@ -34,7 +35,7 @@ type History struct { LastLeaf common.Hash } -func New(leaves []common.Hash) (History, error) { +func NewLegacy(leaves []common.Hash) (LegacyHistory, error) { if len(leaves) == 0 { return emptyCommit, errors.New("must commit to at least one leaf") } @@ -80,7 +81,7 @@ func New(leaves []common.Hash) (History, error) { return emptyCommit, err3 } - return History{ + return LegacyHistory{ Merkle: root, Height: uint64(len(leaves) - 1), FirstLeaf: leaves[0], diff --git a/state-commitments/history/commitments_test.go b/state-commitments/legacy/legacy_test.go similarity index 95% rename from state-commitments/history/commitments_test.go rename to state-commitments/legacy/legacy_test.go index 729d6e39b..f85638432 100644 --- a/state-commitments/history/commitments_test.go +++ b/state-commitments/legacy/legacy_test.go @@ -1,7 +1,7 @@ // Copyright 2023, Offchain Labs, Inc. // For license information, see https://github.com/offchainlabs/bold/blob/main/LICENSE -package history +package legacy import ( "fmt" @@ -17,7 +17,7 @@ func TestHistoryCommitment_LeafProofs(t *testing.T) { for i := 0; i < len(leaves); i++ { leaves[i] = common.BytesToHash([]byte(fmt.Sprintf("%d", i))) } - history, err := New(leaves) + history, err := NewLegacy(leaves) require.NoError(t, err) require.Equal(t, history.FirstLeaf, leaves[0]) require.Equal(t, history.LastLeaf, leaves[len(leaves)-1]) diff --git a/state-commitments/optimized/history_commitment.go b/state-commitments/optimized/history_commitment.go deleted file mode 100644 index 1476beade..000000000 --- a/state-commitments/optimized/history_commitment.go +++ /dev/null @@ -1,511 +0,0 @@ -package optimized - -import ( - "errors" - "fmt" - "math" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" -) - -var emptyHash = common.Hash{} - -type Commitment struct { - Height uint64 - Merkle common.Hash - FirstLeaf common.Hash - LastLeafProof []common.Hash - FirstLeafProof []common.Hash - LastLeaf common.Hash -} - -// NewCommitment produces a history commitment from a list of leaves that are virtually padded using -// the last leaf in the list to some virtual length, without making those extra allocations needed to do so. -// Virtual must be >= len(leaves). -func NewCommitment(leaves []common.Hash, virtual uint64) (*Commitment, error) { - if len(leaves) == 0 { - return nil, errors.New("must commit to at least one leaf") - } - if virtual == 0 { - return nil, errors.New("virtual size cannot be zero") - } - if virtual < uint64(len(leaves)) { - return nil, errors.New("virtual size must be greater than or equal to the number of leaves") - } - comm := NewCommitter() - firstLeaf := leaves[0] - lastLeaf := leaves[len(leaves)-1] - var firstLeafProof, lastLeafProof []common.Hash - ok, err := isPowTwo(virtual) - if err != nil { - return nil, err - } - if ok { - firstLeafProof, err = comm.computeMerkleProof(0, leaves, virtual) - if err != nil { - return nil, err - } - lastLeafProof, err = comm.computeMerkleProof(virtual-1, leaves, virtual) - if err != nil { - return nil, err - } - } - root, err := comm.ComputeRoot(leaves, virtual) - if err != nil { - return nil, err - } - return &Commitment{ - Height: virtual - 1, - Merkle: root, - FirstLeaf: firstLeaf, - LastLeaf: lastLeaf, - FirstLeafProof: firstLeafProof, - LastLeafProof: lastLeafProof, - }, nil -} - -type HistoryCommitter struct { - lastLeafFillers []common.Hash - keccak crypto.KeccakState -} - -func NewCommitter() *HistoryCommitter { - return &HistoryCommitter{ - lastLeafFillers: make([]common.Hash, 0), - keccak: crypto.NewKeccakState(), - } -} - -func (h *HistoryCommitter) hash(item []byte) (common.Hash, error) { - defer h.keccak.Reset() - if _, err := h.keccak.Write(item); err != nil { - return emptyHash, err - } - var result common.Hash - if _, err := h.keccak.Read(result[:]); err != nil { - return emptyHash, err - } - return result, nil -} - -func (h *HistoryCommitter) ComputeRoot(leaves []common.Hash, virtual uint64) (common.Hash, error) { - if len(leaves) == 0 { - return emptyHash, nil - } - rehashedLeaves := make([]common.Hash, len(leaves)) - for i, leaf := range leaves { - result, err := h.hash(leaf[:]) - if err != nil { - return emptyHash, err - } - rehashedLeaves[i] = result - } - return h.computeVirtualSparseTree(rehashedLeaves, virtual, 0) -} - -func (h *HistoryCommitter) GeneratePrefixProof(prefixIndex uint64, leaves []common.Hash, virtual uint64) ([]common.Hash, []common.Hash, error) { - rehashedLeaves := make([]common.Hash, len(leaves)) - for i, leaf := range leaves { - result, err := h.hash(leaf[:]) - if err != nil { - return nil, nil, err - } - rehashedLeaves[i] = result - } - prefixExpansion, proof, err := h.prefixAndProof(prefixIndex, rehashedLeaves, virtual) - if err != nil { - return nil, nil, err - } - prefixExpansion = trimTrailingZeroHashes(prefixExpansion) - proof = trimZeroes(proof) - return prefixExpansion, proof, nil -} - -// computeSparseTree returns the htr of a hashtree with the given leaves and -// limit. Any non-allocated leaf is filled with the passed zeroHash of depth 0. -// Recursively, any non allocated intermediate layer at depth i is filled with -// the passed zeroHash. -// limit is assumed to be a power of two which is higher or equal than the -// length of the leaves. -// fillers is assumed to be precomputed to the necessary limit. -// It is a programming error to call this function with a limit of 0. -// -// Zero allocations -// Computes O(len(leaves)) hashes. -func (h *HistoryCommitter) computeSparseTree(leaves []common.Hash, limit uint64, fillers []common.Hash) (common.Hash, error) { - if limit == 0 { - panic("limit must be greater than 0") - } - m := len(leaves) - if m == 0 { - return emptyHash, nil - } - if limit < 2 { - return leaves[0], nil - } - depth := int(math.Log2(float64(limit))) - for j := 0; j < depth; j++ { - // Check to ensure we don't access out of bounds. - for i := 0; i < m/2; i++ { - if _, err := h.keccak.Write(leaves[2*i][:]); err != nil { - return emptyHash, err - } - if _, err := h.keccak.Write(leaves[2*i+1][:]); err != nil { - return emptyHash, err - } - if _, err := h.keccak.Read(leaves[i][:]); err != nil { - return emptyHash, err - } - h.keccak.Reset() - } - if m&1 == 1 { - // Check to ensure m-1 is a valid index. - if _, err := h.keccak.Write(leaves[m-1][:]); err != nil { - return emptyHash, err - } - if j < len(fillers) { // Check to prevent index out of range for fillers. - if _, err := h.keccak.Write(fillers[j][:]); err != nil { - return emptyHash, err - } - } else { - // Handle the case where j is out of range for fillers. - return emptyHash, errors.New("insufficient fillers") - } - if _, err := h.keccak.Read(leaves[(m-1)/2][:]); err != nil { - return emptyHash, err - } - h.keccak.Reset() - } - m = (m + 1) / 2 - } - return leaves[0], nil -} - -// computeVirtualSparseTree returns the htr of a hashtree where the first layer -// is passed as leaves, the completed with the last leaf until it reaches -// virtual and finally completed with zero hashes until it reaches limit. -// limit is assumed to be either 0 or a power of 2 which is greater or equal to -// virtual. If limit is zero it behaves as if it were the smallest power of two -// that is greater or equal than virtual. -// -// The algorithm is split in three different logic parts: -// -// 1. If the virtual length is less than half the limit (this can never happen -// in the first iteration of the algorithm), then the first half of the tree -// is computed by recursion and the second half is a zero hash of a given -// depth. -// 2. If the leaves all fit in the first half, then we can optimize the first -// half to being a simple sparse tree, just that instead of filling with zero -// hashes we fill with the precomputed virtual hashes. This is the most common -// starting scenario. The second part is computed by recursion. -// 3. If the leaves do not fit in the first half, then we can compute the first half of -// the tree as a normal full hashtree. The second part is computed by recursion. -func (h *HistoryCommitter) computeVirtualSparseTree(leaves []common.Hash, virtual, limit uint64) (common.Hash, error) { - m := uint64(len(leaves)) - if m == 0 { - return emptyHash, errors.New("nil leaves") - } - if virtual < m { - return emptyHash, fmt.Errorf("virtual %d should be >= num leaves %d", virtual, m) - } - var err error - if limit == 0 { - limit = nextPowerOf2(virtual) - n := 1 - if virtual > m { - logValue := math.Log2(float64(limit)) - n = int(logValue) + 1 - } - h.lastLeafFillers, err = h.precomputeRepeatedHashes(&leaves[m-1], n) - if err != nil { - return emptyHash, err - } - } - if limit < virtual { - return emptyHash, fmt.Errorf("limit %d should be >= virtual %d", limit, virtual) - } - if limit == 1 { - return leaves[0], nil - } - var left, right common.Hash - if virtual > limit/2 { - if m > limit/2 { - left, err = h.computeSparseTree(leaves[:limit/2], limit/2, nil) - if err != nil { - return emptyHash, err - } - right, err = h.computeVirtualSparseTree(leaves[limit/2:], virtual-limit/2, limit/2) - if err != nil { - return emptyHash, err - } - } else { - left, err = h.computeSparseTree(leaves, limit/2, h.lastLeafFillers) - if err != nil { - return emptyHash, err - } - if virtual == limit { - if len(h.lastLeafFillers) > int(math.Log2(float64(limit/2))) { - right = h.lastLeafFillers[int(math.Log2(float64(limit/2)))] - } else { - return emptyHash, errors.New("insufficient lastLeafFillers") - } - } else { - if len(h.lastLeafFillers) > 0 { - right, err = h.computeVirtualSparseTree([]common.Hash{h.lastLeafFillers[0]}, virtual-limit/2, limit/2) - if err != nil { - return emptyHash, err - } - } else { - return emptyHash, errors.New("empty lastLeafFillers") - } - } - } - } else { - left, err = h.computeVirtualSparseTree(leaves, virtual, limit/2) - if err != nil { - return emptyHash, err - } - right = emptyHash - } - if _, err = h.keccak.Write(left[:]); err != nil { - return emptyHash, err - } - if _, err = h.keccak.Write(right[:]); err != nil { - return emptyHash, err - } - if _, err = h.keccak.Read(leaves[0][:]); err != nil { - return emptyHash, err - } - h.keccak.Reset() - return leaves[0], nil -} - -func (h *HistoryCommitter) subtreeExpansion(leaves []common.Hash, virtual, limit uint64, stripped bool) (proof []common.Hash, err error) { - m := uint64(len(leaves)) - if m == 0 { - return make([]common.Hash, 0), nil - } - if virtual == 0 { - for i := limit; i > 1; i /= 2 { - proof = append(proof, emptyHash) - } - return - } - if limit == 0 { - limit = nextPowerOf2(virtual) - } - if limit == virtual { - left, err2 := h.computeSparseTree(leaves, limit, h.lastLeafFillers) - if err2 != nil { - return nil, err2 - } - if !stripped { - for i := limit; i > 1; i /= 2 { - proof = append(proof, emptyHash) - } - } - return append(proof, left), nil - } - if m > limit/2 { - left, err2 := h.computeSparseTree(leaves[:limit/2], limit/2, nil) - if err2 != nil { - return nil, err2 - } - proof, err = h.subtreeExpansion(leaves[limit/2:], virtual-limit/2, limit/2, stripped) - if err != nil { - return nil, err - } - return append(proof, left), nil - } - if virtual >= limit/2 { - left, err2 := h.computeSparseTree(leaves, limit/2, h.lastLeafFillers) - if err2 != nil { - return nil, err2 - } - // Check if h.lastLeafFillers is not empty before accessing its first element - if len(h.lastLeafFillers) > 0 { - proof, err = h.subtreeExpansion([]common.Hash{h.lastLeafFillers[0]}, virtual-limit/2, limit/2, stripped) - if err != nil { - return nil, err - } - return append(proof, left), nil - } else { - return nil, errors.New("lastLeafFillers is empty") - } - } - if stripped { - return h.subtreeExpansion(leaves, virtual, limit/2, stripped) - } - expac, err := h.subtreeExpansion(leaves, virtual, limit/2, stripped) - if err != nil { - return nil, err - } - return append(expac, emptyHash), nil -} - -func (h *HistoryCommitter) proof(index uint64, leaves []common.Hash, virtual, limit uint64) (tail []common.Hash, err error) { - m := uint64(len(leaves)) - if m == 0 { - return nil, errors.New("empty leaves slice") - } - if limit == 0 { - limit = nextPowerOf2(virtual) - } - if limit == 1 { - // Can only reach this with index == 0 - return - } - if index >= limit/2 { - if m > limit/2 { - return h.proof(index-limit/2, leaves[limit/2:], virtual-limit/2, limit/2) - } - if len(h.lastLeafFillers) > 0 { - return h.proof(index-limit/2, []common.Hash{h.lastLeafFillers[0]}, virtual-limit/2, limit/2) - } else { - return nil, errors.New("lastLeafFillers is empty") - } - } - if m > limit/2 { - tail, err = h.proof(index, leaves[:limit/2], limit/2, limit/2) - if err != nil { - return nil, err - } - right, err2 := h.subtreeExpansion(leaves[limit/2:], virtual-limit/2, limit/2, true) - if err2 != nil { - return nil, err2 - } - for i := len(right) - 1; i >= 0; i-- { - tail = append(tail, right[i]) - } - return tail, nil - } - if virtual > limit/2 { - tail, err = h.proof(index, leaves, limit/2, limit/2) - if err != nil { - return nil, err - } - if len(h.lastLeafFillers) > 0 { - right, err := h.subtreeExpansion([]common.Hash{h.lastLeafFillers[0]}, virtual-limit/2, limit/2, true) - if err != nil { - return nil, err - } - for i := len(right) - 1; i >= 0; i-- { - tail = append(tail, right[i]) - } - } else { - return nil, errors.New("lastLeafFillers is empty") - } - return tail, nil - } - return h.proof(index, leaves, virtual, limit/2) -} - -func (h *HistoryCommitter) prefixAndProof(index uint64, leaves []common.Hash, virtual uint64) (prefix []common.Hash, tail []common.Hash, err error) { - m := uint64(len(leaves)) - if m == 0 { - return nil, nil, errors.New("nil leaves") - } - if virtual == 0 { - return nil, nil, errors.New("virtual size cannot be zero") - } - if m > virtual { - return nil, nil, fmt.Errorf("num leaves %d should be <= virtual %d", m, virtual) - } - if index+1 > virtual { - return nil, nil, fmt.Errorf("index %d + 1 should be <= virtual %d", index, virtual) - } - logVirtual := int(math.Log2(float64(virtual)) + 1) - h.lastLeafFillers, err = h.precomputeRepeatedHashes(&leaves[m-1], logVirtual) - if err != nil { - return nil, nil, err - } - - if index+1 > m { - prefix, err = h.subtreeExpansion(leaves, index+1, 0, false) - } else { - prefix, err = h.subtreeExpansion(leaves[:index+1], index+1, 0, false) - } - if err != nil { - return nil, nil, err - } - tail, err = h.proof(index, leaves, virtual, 0) - return -} - -// precomputeRepeatedHashes returns a slice where built recursively as -// ret[0] = the passed in leaf -// ret[i+1] = Hash(ret[i] + ret[i]) -// Allocates n hashes -// Computes n-1 hashes -// Copies 1 hash -func (h *HistoryCommitter) precomputeRepeatedHashes(leaf *common.Hash, n int) ([]common.Hash, error) { - if leaf == nil { - return nil, errors.New("nil leaf pointer") - } - if len(h.lastLeafFillers) > 0 && h.lastLeafFillers[0] == *leaf && len(h.lastLeafFillers) >= n { - return h.lastLeafFillers, nil - } - if n < 0 { - return nil, fmt.Errorf("invalid n: %d, must be non-negative", n) - } - ret := make([]common.Hash, n) - copy(ret[0][:], (*leaf)[:]) - for i := 1; i < n; i++ { - if _, err := h.keccak.Write(ret[i-1][:]); err != nil { - return nil, fmt.Errorf("keccak write error: %w", err) - } - if _, err := h.keccak.Write(ret[i-1][:]); err != nil { - return nil, fmt.Errorf("keccak write error: %w", err) - } - if _, err := h.keccak.Read(ret[i][:]); err != nil { - return nil, fmt.Errorf("keccak read error: %w", err) - } - h.keccak.Reset() - } - return ret, nil -} - -func nextPowerOf2(n uint64) uint64 { - if n == 0 { - return 1 - } - n-- // Decrement n to handle the case where n is already a power of 2 - n |= n >> 1 // Propagate the highest bit set - n |= n >> 2 - n |= n >> 4 - n |= n >> 8 - n |= n >> 16 - n |= n >> 32 - return n + 1 // Increment n to get the next power of 2 -} - -func trimTrailingZeroHashes(hashes []common.Hash) []common.Hash { - // Start from the end of the slice - for i := len(hashes) - 1; i >= 0; i-- { - // If we find a non-zero hash, return the slice up to and including this element - if hashes[i] != (common.Hash{}) { - return hashes[:i+1] - } - } - // If all elements are zero, return an empty slice - return []common.Hash{} -} - -func trimZeroes(hashes []common.Hash) []common.Hash { - newHashes := make([]common.Hash, 0, len(hashes)) - for _, h := range hashes { - if h == (common.Hash{}) { - continue - } - newHashes = append(newHashes, h) - } - return newHashes -} - -func isPowTwo(n uint64) (bool, error) { - if n == 0 { - return false, errors.New("n must be non-zero") - } - return (n & (n - 1)) == 0, nil -} diff --git a/state-commitments/optimized/inclusion_proof.go b/state-commitments/optimized/inclusion_proof.go deleted file mode 100644 index 29b73e2d5..000000000 --- a/state-commitments/optimized/inclusion_proof.go +++ /dev/null @@ -1,113 +0,0 @@ -package optimized - -import ( - "errors" - "math" - - "github.com/ethereum/go-ethereum/common" -) - -// Computes the Merkle proof for a leaf at a given index. -// It uses the last leaf to pad the tree up to the 'virtual' size if needed. -func (h *HistoryCommitter) computeMerkleProof(leafIndex uint64, leaves []common.Hash, virtual uint64) ([]common.Hash, error) { - if len(leaves) == 0 { - return nil, nil - } - ok, err := isPowTwo(virtual) - if err != nil { - return nil, err - } - if !ok { - return nil, errors.New("virtual size must be a power of 2") - } - if leafIndex >= uint64(len(leaves)) { - return nil, errors.New("leaf index out of bounds") - } - if virtual < uint64(len(leaves)) { - return nil, errors.New("virtual size must be greater than or equal to the number of leaves") - } - numRealLeaves := uint64(len(leaves)) - lastLeaf, err := h.hash(leaves[numRealLeaves-1][:]) - if err != nil { - return nil, err - } - depth := int(math.Ceil(math.Log2(float64(virtual)))) - - // Precompute virtual hashes - virtualHashes, err := h.precomputeRepeatedHashes(&lastLeaf, depth) - if err != nil { - return nil, err - } - var proof []common.Hash - for level := 0; level < depth; level++ { - nodeIndex := leafIndex >> level - siblingHash, exists, err := h.computeSiblingHash(nodeIndex, uint64(level), numRealLeaves, virtual, leaves, virtualHashes) - if err != nil { - return nil, err - } - if exists { - proof = append(proof, siblingHash) - } - } - return proof, nil -} - -// Computes the hash of a node's sibling at a given index and level. -func (h *HistoryCommitter) computeSiblingHash( - nodeIndex uint64, - level uint64, - N uint64, - virtual uint64, - hLeaves []common.Hash, - hNHashes []common.Hash, -) (common.Hash, bool, error) { - siblingIndex := nodeIndex ^ 1 - // Essentially ceil(virtual / (2 ** level)) - numNodes := (virtual + (1 << level) - 1) / (1 << level) - if siblingIndex >= numNodes { - // No sibling exists, so use a zero hash. - return common.Hash{}, false, nil - } else if siblingIndex >= paddingStartIndexAtLevel(N, level) { - return hNHashes[level], true, nil - } else { - siblingHash, err := h.computeNodeHash(siblingIndex, level, N, hLeaves, hNHashes) - if err != nil { - return emptyHash, false, err - } - return siblingHash, true, nil - } -} - -// Recursively computes the hash of a node at a given index and level. -func (h *HistoryCommitter) computeNodeHash( - nodeIndex uint64, level uint64, numRealLeaves uint64, leaves []common.Hash, virtualHashes []common.Hash, -) (common.Hash, error) { - if level == 0 { - if nodeIndex >= numRealLeaves { - // Node is in padding (the virtual segment of the tree). - return virtualHashes[0], nil - } else { - return h.hash(leaves[nodeIndex][:]) - } - } else { - if nodeIndex >= paddingStartIndexAtLevel(numRealLeaves, level) { - return virtualHashes[level], nil - } else { - leftChild, err := h.computeNodeHash(2*nodeIndex, level-1, numRealLeaves, leaves, virtualHashes) - if err != nil { - return emptyHash, err - } - rightChild, err := h.computeNodeHash(2*nodeIndex+1, level-1, numRealLeaves, leaves, virtualHashes) - if err != nil { - return emptyHash, err - } - data := append(leftChild.Bytes(), rightChild.Bytes()...) - return h.hash(data) - } - } -} - -// Calculates the index at which padding starts at a given tree level. -func paddingStartIndexAtLevel(N uint64, level uint64) uint64 { - return N / (1 << level) -} diff --git a/state-commitments/prefix-proofs/BUILD.bazel b/state-commitments/prefix-proofs/BUILD.bazel index b47cefb63..cd7300e1f 100644 --- a/state-commitments/prefix-proofs/BUILD.bazel +++ b/state-commitments/prefix-proofs/BUILD.bazel @@ -1,5 +1,16 @@ load("@rules_go//go:def.bzl", "go_library", "go_test") +package_group( + name = "friends", + packages = [ + "//layer2-state-provider", + "//state-commitments/history", + "//state-commitments/inclusion-proofs", + "//state-commitments/legacy", + "//testing/integration", + ], +) + go_library( name = "prefix-proofs", srcs = [ @@ -7,7 +18,7 @@ go_library( "prefix_proofs.go", ], importpath = "github.com/OffchainLabs/bold/state-commitments/prefix-proofs", - visibility = ["//visibility:public"], + visibility = [":friends"], deps = [ "@com_github_ethereum_go_ethereum//common", "@com_github_ethereum_go_ethereum//crypto", diff --git a/state-commitments/optimized/BUILD.bazel b/testing/integration/BUILD.bazel similarity index 53% rename from state-commitments/optimized/BUILD.bazel rename to testing/integration/BUILD.bazel index 930b946dd..8a5bcf524 100644 --- a/state-commitments/optimized/BUILD.bazel +++ b/testing/integration/BUILD.bazel @@ -1,23 +1,9 @@ -load("@rules_go//go:def.bzl", "go_library", "go_test") - -go_library( - name = "optimized", - srcs = [ - "history_commitment.go", - "inclusion_proof.go", - ], - importpath = "github.com/OffchainLabs/bold/state-commitments/optimized", - visibility = ["//visibility:public"], - deps = [ - "@com_github_ethereum_go_ethereum//common", - "@com_github_ethereum_go_ethereum//crypto", - ], -) +load("@rules_go//go:def.bzl", "go_test") go_test( - name = "optimized_test", - srcs = ["history_commitment_test.go"], - embed = [":optimized"], + name = "integration_test", + size = "small", + srcs = ["prefixproofs_test.go"], deps = [ "//containers/option", "//layer2-state-provider", diff --git a/state-commitments/optimized/history_commitment_test.go b/testing/integration/prefixproofs_test.go similarity index 50% rename from state-commitments/optimized/history_commitment_test.go rename to testing/integration/prefixproofs_test.go index ac7a664ed..41b463e46 100644 --- a/state-commitments/optimized/history_commitment_test.go +++ b/testing/integration/prefixproofs_test.go @@ -1,4 +1,4 @@ -package optimized +package prefixproofs import ( "context" @@ -21,22 +21,6 @@ import ( "github.com/stretchr/testify/require" ) -func FuzzHistoryCommitter(f *testing.F) { - simpleHash := crypto.Keccak256Hash([]byte("foo")) - f.Fuzz(func(t *testing.T, numReal uint64, virtual uint64, limit uint64) { - // Set some bounds. - numReal = numReal % (1 << 10) - virtual = virtual % (1 << 20) - hashedLeaves := make([]common.Hash, numReal) - for i := range hashedLeaves { - hashedLeaves[i] = simpleHash - } - committer := NewCommitter() - _, err := committer.ComputeRoot(hashedLeaves, virtual) - _ = err - }) -} - func TestPrefixProofGeneration(t *testing.T) { t.Parallel() ctx := context.Background() @@ -107,43 +91,6 @@ func TestPrefixProofGeneration(t *testing.T) { } } -func BenchmarkPrefixProofGeneration_Legacy(b *testing.B) { - for i := 0; i < b.N; i++ { - prefixIndex := 13384 - simpleHash := crypto.Keccak256Hash([]byte("foo")) - hashes := make([]common.Hash, 1<<14) - for i := 0; i < len(hashes); i++ { - hashes[i] = simpleHash - } - - lowCommitmentNumLeaves := prefixIndex + 1 - hiCommitmentNumLeaves := (1 << 14) - prefixExpansion, err := prefixproofs.ExpansionFromLeaves(hashes[:lowCommitmentNumLeaves]) - require.NoError(b, err) - _, err = prefixproofs.GeneratePrefixProof( - uint64(lowCommitmentNumLeaves), - prefixExpansion, - hashes[lowCommitmentNumLeaves:hiCommitmentNumLeaves], - prefixproofs.RootFetcherFromExpansion, - ) - require.NoError(b, err) - } -} - -func BenchmarkPrefixProofGeneration_Optimized(b *testing.B) { - b.StopTimer() - simpleHash := crypto.Keccak256Hash([]byte("foo")) - hashes := []common.Hash{crypto.Keccak256Hash(simpleHash[:])} - prefixIndex := uint64(13384) - virtual := uint64(1 << 14) - committer := NewCommitter() - b.StartTimer() - for i := 0; i < b.N; i++ { - _, _, err := committer.GeneratePrefixProof(prefixIndex, hashes, virtual) - require.NoError(b, err) - } -} - type prefixProofComputation struct { prefixRoot common.Hash fullRoot common.Hash @@ -162,8 +109,7 @@ func computeOptimizedPrefixProof(t *testing.T, numRealHashes uint64, virtual uin } // Computes the prefix root. - committer := NewCommitter() - prefixRoot, err := committer.ComputeRoot(hashes, prefixIndex+1) + prefixRoot, err := history.ComputeRoot(hashes, prefixIndex+1) require.NoError(t, err) // Computes the full tree root. @@ -171,8 +117,7 @@ func computeOptimizedPrefixProof(t *testing.T, numRealHashes uint64, virtual uin for i := 0; i < len(hashes); i++ { hashes[i] = simpleHash } - committer = NewCommitter() - fullTreeRoot, err := committer.ComputeRoot(hashes, virtual) + fullTreeRoot, err := history.ComputeRoot(hashes, virtual) require.NoError(t, err) // Computes the prefix proof. @@ -180,7 +125,7 @@ func computeOptimizedPrefixProof(t *testing.T, numRealHashes uint64, virtual uin for i := 0; i < len(hashes); i++ { hashes[i] = simpleHash } - prefixExp, proof, err := committer.GeneratePrefixProof(uint64(prefixIndex), hashes, virtual) + prefixExp, proof, err := history.GeneratePrefixProof(uint64(prefixIndex), hashes, virtual) require.NoError(t, err) return &prefixProofComputation{ prefixRoot: prefixRoot, @@ -247,229 +192,6 @@ func computeLegacyPrefixProof(t *testing.T, ctx context.Context, numHashes uint6 } } -func TestLegacyVsOptimized(t *testing.T) { - t.Parallel() - end := uint64(1 << 9) - simpleHash := crypto.Keccak256Hash([]byte("foo")) - for i := uint64(1); i < end; i++ { - limit := nextPowerOf2(i) - for j := i; j < limit; j++ { - inputLeaves := make([]common.Hash, i) - for i := range inputLeaves { - inputLeaves[i] = simpleHash - } - committer := NewCommitter() - computedRoot, err := committer.ComputeRoot(inputLeaves, uint64(j)) - require.NoError(t, err) - - legacyInputLeaves := make([]common.Hash, j) - for i := range legacyInputLeaves { - legacyInputLeaves[i] = simpleHash - } - histCommit, err := history.New(legacyInputLeaves) - require.NoError(t, err) - require.Equal(t, computedRoot, histCommit.Merkle) - } - } -} - -func TestLegacyVsOptimizedEdgeCases(t *testing.T) { - t.Parallel() - simpleHash := crypto.Keccak256Hash([]byte("foo")) - - tests := []struct { - realLength int - virtualLength int - }{ - {12, 14}, - {8, 10}, - {6, 6}, - {10, 16}, - {4, 8}, - {1, 5}, - {3, 5}, - {5, 5}, - {1023, 1024}, - {(1 << 14) - 7, (1 << 14) - 7}, - } - - for _, tt := range tests { - t.Run(fmt.Sprintf("real length %d, virtual %d", tt.realLength, tt.virtualLength), func(t *testing.T) { - inputLeaves := make([]common.Hash, tt.realLength) - for i := range inputLeaves { - inputLeaves[i] = simpleHash - } - committer := NewCommitter() - computedRoot, err := committer.ComputeRoot(inputLeaves, uint64(tt.virtualLength)) - require.NoError(t, err) - - leaves := make([]common.Hash, tt.virtualLength) - for i := range leaves { - leaves[i] = simpleHash - } - histCommit, err := history.New(leaves) - require.NoError(t, err) - require.Equal(t, computedRoot, histCommit.Merkle) - }) - } -} - -func TestVirtualSparse(t *testing.T) { - t.Parallel() - simpleHash := crypto.Keccak256Hash([]byte("foo")) - t.Run("real length 1, virtual length 3", func(t *testing.T) { - committer := NewCommitter() - computedRoot, err := committer.ComputeRoot([]common.Hash{simpleHash}, 3) - require.NoError(t, err) - - leaves := []common.Hash{ - simpleHash, - simpleHash, - simpleHash, - } - histCommit, err := history.New(leaves) - require.NoError(t, err) - require.Equal(t, histCommit.Merkle, computedRoot) - }) - t.Run("real length 2, virtual length 3", func(t *testing.T) { - hashedLeaves := []common.Hash{ - simpleHash, - simpleHash, - } - committer := NewCommitter() - computedRoot, err := committer.ComputeRoot(hashedLeaves, 3) - require.NoError(t, err) - leaves := []common.Hash{ - simpleHash, - simpleHash, - simpleHash, - } - histCommit, err := history.New(leaves) - require.NoError(t, err) - require.Equal(t, histCommit.Merkle, computedRoot) - }) - t.Run("real length 3, virtual length 3", func(t *testing.T) { - hashedLeaves := []common.Hash{ - simpleHash, - simpleHash, - simpleHash, - } - committer := NewCommitter() - computedRoot, err := committer.ComputeRoot(hashedLeaves, 3) - require.NoError(t, err) - leaves := []common.Hash{ - simpleHash, - simpleHash, - simpleHash, - } - histCommit, err := history.New(leaves) - require.NoError(t, err) - require.Equal(t, histCommit.Merkle, computedRoot) - }) - t.Run("real length 4, virtual length 4", func(t *testing.T) { - hashedLeaves := []common.Hash{ - simpleHash, - simpleHash, - simpleHash, - simpleHash, - } - committer := NewCommitter() - computedRoot, err := committer.ComputeRoot(hashedLeaves, 4) - require.NoError(t, err) - leaves := []common.Hash{ - simpleHash, - simpleHash, - simpleHash, - simpleHash, - } - histCommit, err := history.New(leaves) - require.NoError(t, err) - require.Equal(t, histCommit.Merkle, computedRoot) - }) - t.Run("real length 1, virtual length 5", func(t *testing.T) { - hashedLeaves := []common.Hash{ - simpleHash, - } - committer := NewCommitter() - computedRoot, err := committer.ComputeRoot(hashedLeaves, 5) - require.NoError(t, err) - - leaves := []common.Hash{ - simpleHash, - simpleHash, - simpleHash, - simpleHash, - simpleHash, - } - histCommit, err := history.New(leaves) - require.NoError(t, err) - require.Equal(t, computedRoot, histCommit.Merkle) - }) - t.Run("real length 12, virtual length 14", func(t *testing.T) { - hashedLeaves := make([]common.Hash, 12) - for i := range hashedLeaves { - hashedLeaves[i] = simpleHash - } - committer := NewCommitter() - computedRoot, err := committer.ComputeRoot(hashedLeaves, 14) - require.NoError(t, err) - - leaves := make([]common.Hash, 14) - for i := range leaves { - leaves[i] = simpleHash - } - histCommit, err := history.New(leaves) - require.NoError(t, err) - require.Equal(t, computedRoot, histCommit.Merkle) - }) -} - -func TestMaximumDepthHistoryCommitment(t *testing.T) { - t.Parallel() - simpleHash := crypto.Keccak256Hash([]byte("foo")) - hashedLeaves := []common.Hash{ - simpleHash, - } - committer := NewCommitter() - _, err := committer.ComputeRoot(hashedLeaves, 1<<26) - require.NoError(t, err) -} - -func BenchmarkMaximumDepthHistoryCommitment(b *testing.B) { - b.StopTimer() - simpleHash := crypto.Keccak256Hash([]byte("foo")) - hashedLeaves := []common.Hash{ - simpleHash, - } - committer := NewCommitter() - b.StartTimer() - for i := 0; i < b.N; i++ { - _, err := committer.ComputeRoot(hashedLeaves, 1<<26) - _ = err - } -} - -func TestInclusionProofEquivalence(t *testing.T) { - simpleHash := crypto.Keccak256Hash([]byte("foo")) - leaves := []common.Hash{ - simpleHash, - simpleHash, - simpleHash, - simpleHash, - } - commit, err := NewCommitment(leaves, 4) - require.NoError(t, err) - oldLeaves := []common.Hash{ - simpleHash, - simpleHash, - simpleHash, - simpleHash, - } - oldCommit, err := history.New(oldLeaves) - require.NoError(t, err) - require.Equal(t, commit.Merkle, oldCommit.Merkle) -} - func setupMerkleTreeContract(t testing.TB) (*mocksgen.MerkleTreeAccess, *simulated.Backend) { numChains := uint64(1) accs, backend := setupAccounts(t, numChains) diff --git a/testing/mocks/mocks.go b/testing/mocks/mocks.go index 7a9267c55..217a9d728 100644 --- a/testing/mocks/mocks.go +++ b/testing/mocks/mocks.go @@ -14,7 +14,7 @@ import ( "github.com/OffchainLabs/bold/containers/option" l2stateprovider "github.com/OffchainLabs/bold/layer2-state-provider" "github.com/OffchainLabs/bold/solgen/go/rollupgen" - commitments "github.com/OffchainLabs/bold/state-commitments/history" + "github.com/OffchainLabs/bold/state-commitments/history" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/stretchr/testify/mock" @@ -84,9 +84,9 @@ type MockStateManager struct { func (m *MockStateManager) HistoryCommitment( ctx context.Context, req *l2stateprovider.HistoryCommitmentRequest, -) (commitments.History, error) { +) (history.History, error) { args := m.Called(ctx, req) - return args.Get(0).(commitments.History), args.Error(1) + return args.Get(0).(history.History), args.Error(1) } func (m *MockStateManager) PrefixProof( @@ -210,7 +210,7 @@ func (m *MockSpecChallengeManager) AddBlockChallengeLevelZeroEdge( ctx context.Context, assertion protocol.Assertion, startCommit, - endCommit commitments.History, + endCommit history.History, startEndPrefixProof []byte, ) (protocol.VerifiedRoyalEdge, error) { args := m.Called(ctx, assertion, startCommit, endCommit, startEndPrefixProof) @@ -221,7 +221,7 @@ func (m *MockSpecChallengeManager) AddSubChallengeLevelZeroEdge( ctx context.Context, challengedEdge protocol.SpecEdge, startCommit, - endCommit commitments.History, + endCommit history.History, startParentInclusionProof []common.Hash, endParentInclusionProof []common.Hash, startEndPrefixProof []byte, diff --git a/testing/mocks/state-provider/layer2_state_provider.go b/testing/mocks/state-provider/layer2_state_provider.go index 9d73e586c..0b3da48a0 100644 --- a/testing/mocks/state-provider/layer2_state_provider.go +++ b/testing/mocks/state-provider/layer2_state_provider.go @@ -212,7 +212,7 @@ func (s *L2StateBackend) UpdateAPIDatabase(database db.Database) { s.HistoryCommitmentProvider = *commitmentProvider } -// ExecutionStateAfterBatchCount produces the l2 state to assert at the message number specified. +// ExecutionStateAfterPreviousState produces the l2 state to assert at the message number specified. func (s *L2StateBackend) ExecutionStateAfterPreviousState(ctx context.Context, maxInboxCount uint64, previousGlobalState *protocol.GoGlobalState, maxNumberOfBlocks uint64) (*protocol.ExecutionState, error) { if len(s.executionStates) == 0 { return nil, errors.New("no execution states") @@ -238,7 +238,7 @@ func (s *L2StateBackend) ExecutionStateAfterPreviousState(ctx context.Context, m if err != nil { return nil, err } - commit, err := history.New(historyCommit) + commit, err := history.NewCommitment(historyCommit, uint64(s.challengeLeafHeights[0])+1) if err != nil { return nil, err } @@ -266,18 +266,13 @@ func (s *L2StateBackend) statesUpTo(blockStart, blockEnd, fromBatch, toBatch uin start := startIndex + blockStart end := start + blockEnd - // The size is the number of elements being committed to. For example, if the height is 7, there will - // be 8 elements being committed to from [0, 7] inclusive. - desiredStatesLen := int(blockEnd - blockStart + 1) var states []common.Hash - var lastState common.Hash for i := start; i <= end; i++ { if i >= uint64(len(s.stateRoots)) { break } state := s.stateRoots[i] states = append(states, state) - lastState = state if len(s.executionStates) == 0 { // should only happen in tests continue @@ -290,9 +285,6 @@ func (s *L2StateBackend) statesUpTo(blockStart, blockEnd, fromBatch, toBatch uin break } } - for len(states) < desiredStatesLen { - states = append(states, lastState) - } return states, nil }