diff --git a/MakefileEks.mk b/MakefileEks.mk index b2c3c7a31..15a67eeb1 100644 --- a/MakefileEks.mk +++ b/MakefileEks.mk @@ -36,20 +36,41 @@ start-bk-test-morph-test-qanet-to-morph-gas-price-oracle-qanet: # mainnet build-bk-prod-morph-prod-mainnet-to-morph-prover: if [ ! -d dist ]; then mkdir -p dist; fi - cd $(PWD)/prover/bin/server && RUSTFLAGS="-C target-feature=+avx2,+avx512f" cargo build --release + cd $(PWD)/prover/bin/server && CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release cp prover/target/release/prover-server dist/ - cp -r prover/configs dist/ aws s3 cp s3://morph-0582-morph-technical-department-mainnet-data/morph-setup/secret-manager-wrapper.tar.gz ./ tar -xvzf secret-manager-wrapper.tar.gz start-bk-prod-morph-prod-mainnet-to-morph-prover: /data/secret-manager-wrapper ./prover-server +# testnet +build-bk-prod-morph-prod-testnet-to-morph-prover-hoodi: + if [ ! -d dist ]; then mkdir -p dist; fi + cd $(PWD)/prover/bin/server && CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release + cp prover/target/release/prover-server dist/ + aws s3 cp s3://morph-0582-morph-technical-department-testnet-data/testnet/hoodi/morph-setup/secret-manager-wrapper.tar.gz ./ + tar -xvzf secret-manager-wrapper.tar.gz + +start-bk-prod-morph-prod-testnet-to-morph-prover-hoodi: + /data/secret-manager-wrapper ./prover-server + +# qanet +build-bk-test-morph-test-qanet-to-morph-prover: + if [ ! -d dist ]; then mkdir -p dist; fi + cd $(PWD)/prover/bin/server && CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release + cp prover/target/release/prover-server dist/ + aws s3 cp s3://morph-7637-morph-technical-department-qanet-data/morph-setup/secret-manager-wrapper.tar.gz ./ + tar -xvzf secret-manager-wrapper.tar.gz + +start-bk-test-morph-test-qanet-to-morph-prover: + /data/secret-manager-wrapper ./prover-server + # challenge-handler # mainnet build-bk-prod-morph-prod-mainnet-to-morph-challenge-handler: if [ ! -d dist ]; then mkdir -p dist; fi - cd $(PWD)/prover/bin/challenge && cargo build --release + cd $(PWD)/prover/bin/challenge && CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release cp prover/bin/challenge/target/release/challenge-handler dist/ aws s3 cp s3://morph-0582-morph-technical-department-mainnet-data/morph-setup/secret-manager-wrapper.tar.gz ./ tar -xvzf secret-manager-wrapper.tar.gz @@ -58,18 +79,63 @@ build-bk-prod-morph-prod-mainnet-to-morph-challenge-handler: start-bk-prod-morph-prod-mainnet-to-morph-challenge-handler: /data/secret-manager-wrapper ./challenge-handler +# testnet +build-bk-prod-morph-prod-testnet-to-morph-challenge-handler-hoodi: + if [ ! -d dist ]; then mkdir -p dist; fi + cd $(PWD)/prover/bin/challenge && CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release + cp prover/bin/challenge/target/release/challenge-handler dist/ + aws s3 cp s3://morph-0582-morph-technical-department-testnet-data/testnet/hoodi/morph-setup/secret-manager-wrapper.tar.gz ./ + tar -xvzf secret-manager-wrapper.tar.gz + +start-bk-prod-morph-prod-testnet-to-morph-challenge-handler-hoodi: + /data/secret-manager-wrapper ./challenge-handler + +# qanet +build-bk-test-morph-test-qanet-to-morph-challenge-handler: + if [ ! -d dist ]; then mkdir -p dist; fi + cd $(PWD)/prover/bin/challenge && CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release + cp prover/bin/challenge/target/release/challenge-handler dist/ + aws s3 cp s3://morph-7637-morph-technical-department-qanet-data/morph-setup/secret-manager-wrapper.tar.gz ./ + tar -xvzf secret-manager-wrapper.tar.gz + +start-bk-test-morph-test-qanet-to-morph-challenge-handler: + /data/secret-manager-wrapper ./challenge-handler + # shadow-proving # mainnet build-bk-prod-morph-prod-mainnet-to-morph-shadow-proving: if [ ! -d dist ]; then mkdir -p dist; fi - cd $(PWD)/prover/bin/shadow-prove && cargo build --release - cp prover/bin/shadow-prove/target/release/shadow-proving dist/ + cd $(PWD)/prover/bin/shadow-prove && CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release + cp prover/target/release/shadow-proving dist/ aws s3 cp s3://morph-0582-morph-technical-department-mainnet-data/morph-setup/secret-manager-wrapper.tar.gz ./ tar -xvzf secret-manager-wrapper.tar.gz start-bk-prod-morph-prod-mainnet-to-morph-shadow-proving: /data/secret-manager-wrapper ./shadow-proving +# testnet +build-bk-prod-morph-prod-testnet-to-morph-shadow-proving: + if [ ! -d dist ]; then mkdir -p dist; fi + cd $(PWD)/prover/bin/shadow-prove && CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release + cp prover/target/release/shadow-proving dist/ + aws s3 cp s3://morph-0582-morph-technical-department-testnet-data/testnet/hoodi/morph-setup/secret-manager-wrapper.tar.gz ./ + tar -xvzf secret-manager-wrapper.tar.gz + +start-bk-prod-morph-prod-testnet-to-morph-shadow-proving: + /data/secret-manager-wrapper ./shadow-proving + +# qanet +build-bk-test-morph-test-qanet-to-morph-shadow-proving: + if [ ! -d dist ]; then mkdir -p dist; fi + cd $(PWD)/prover/bin/shadow-prove && CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release + cp prover/target/release/shadow-proving dist/ + aws s3 cp s3://morph-7637-morph-technical-department-qanet-data/morph-setup/secret-manager-wrapper.tar.gz ./ + tar -xvzf secret-manager-wrapper.tar.gz + +start-bk-test-morph-test-qanet-to-morph-shadow-proving: + /data/secret-manager-wrapper ./shadow-proving + + # staking-oracle # mainnet build-bk-prod-morph-prod-mainnet-to-morph-staking-oracle: @@ -129,19 +195,6 @@ start-bk-prod-morph-prod-mainnet-to-morph-token-price-oracle: /data/secret-manager-wrapper ./token-price-oracle -# gas-oracle -# testnet -build-bk-prod-morph-prod-testnet-to-morph-gas-price-oracle-holesky: - if [ ! -d dist ]; then mkdir -p dist; fi - cd $(PWD)/gas-oracle/app && cargo build --release - cp gas-oracle/app/target/release/app dist/ - aws s3 cp s3://morph-0582-morph-technical-department-testnet-data/testnet/holesky/morph-setup/secret-manager-wrapper.tar.gz ./ - tar -xvzf secret-manager-wrapper.tar.gz - - -start-bk-prod-morph-prod-testnet-to-morph-gas-price-oracle-holesky: - /data/secret-manager-wrapper ./app - # gas-oracle # hoodi build-bk-prod-morph-prod-testnet-to-morph-gas-price-oracle-hoodi: @@ -156,44 +209,6 @@ start-bk-prod-morph-prod-testnet-to-morph-gas-price-oracle-hoodi: /data/secret-manager-wrapper ./app -# prover -# testnet -build-bk-prod-morph-prod-testnet-to-morph-prover-holesky: - if [ ! -d dist ]; then mkdir -p dist; fi - cd $(PWD)/prover/bin/server && RUSTFLAGS="-C target-feature=+avx2,+avx512f" cargo build --release - cp prover/target/release/prover-server dist/ - cp -r prover/configs dist/ - aws s3 cp s3://morph-0582-morph-technical-department-testnet-data/testnet/holesky/morph-setup/secret-manager-wrapper.tar.gz ./ - tar -xvzf secret-manager-wrapper.tar.gz - -start-bk-prod-morph-prod-testnet-to-morph-prover-holesky: - /data/secret-manager-wrapper ./prover-server - -# challenge-handler -# testnet -build-bk-prod-morph-prod-testnet-to-morph-challenge-handler-holesky: - if [ ! -d dist ]; then mkdir -p dist; fi - cd $(PWD)/prover/bin/challenge && cargo build --release - cp prover/bin/challenge/target/release/challenge-handler dist/ - aws s3 cp s3://morph-0582-morph-technical-department-testnet-data/testnet/holesky/morph-setup/secret-manager-wrapper.tar.gz ./ - tar -xvzf secret-manager-wrapper.tar.gz - - -start-bk-prod-morph-prod-testnet-to-morph-challenge-handler-holesky: - /data/secret-manager-wrapper ./challenge-handler - -# shadow-proving -# testnet -build-bk-prod-morph-prod-testnet-to-morph-shadow-proving-holesky: - if [ ! -d dist ]; then mkdir -p dist; fi - cd $(PWD)/prover/bin/shadow-prove && cargo build --release - cp prover/bin/shadow-prove/target/release/shadow-proving dist/ - aws s3 cp s3://morph-0582-morph-technical-department-testnet-data/testnet/holesky/morph-setup/secret-manager-wrapper.tar.gz ./ - tar -xvzf secret-manager-wrapper.tar.gz - -start-bk-prod-morph-prod-testnet-to-morph-shadow-proving-holesky: - /data/secret-manager-wrapper ./shadow-proving - # staking-oracle # testnet build-bk-prod-morph-prod-testnet-to-morph-staking-oracle-holesky: diff --git a/tx-submitter/batch/batch_cache.go b/common/batch/batch_cache.go similarity index 74% rename from tx-submitter/batch/batch_cache.go rename to common/batch/batch_cache.go index 293466552..bef2e0717 100644 --- a/tx-submitter/batch/batch_cache.go +++ b/common/batch/batch_cache.go @@ -9,10 +9,7 @@ import ( "fmt" "math/big" "sync" - - "morph-l2/tx-submitter/db" - "morph-l2/tx-submitter/iface" - "morph-l2/tx-submitter/types" + "sync/atomic" "github.com/morph-l2/go-ethereum/accounts/abi/bind" "github.com/morph-l2/go-ethereum/common" @@ -21,10 +18,12 @@ import ( "github.com/morph-l2/go-ethereum/crypto" "github.com/morph-l2/go-ethereum/eth" "github.com/morph-l2/go-ethereum/log" + + "morph-l2/common/blob" ) -// BatchCache is a structure for caching and building batch data -// Stores all batch information starting from 0, and has the functionality to pack batches +// BatchCache holds sealed and in-progress rollup batches: it syncs from L1/L2 or local DB, +// packs consecutive L2 blocks into a chunk, seals with blob sidecars, and exposes query/delete APIs. type BatchCache struct { mu sync.RWMutex ctx context.Context @@ -59,36 +58,60 @@ type BatchCache struct { currentBlockNumber uint64 currentBlockHash common.Hash - // Function to determine if batch is upgraded + // Function to determine if batch is upgraded (V0 -> V1) isBatchUpgraded func(uint64) bool + // Function to determine if batch is V2 upgraded (V1 -> V2, multi-blob) + isBatchV2Upgraded func(uint64) bool // Clients and contracts - l1Client iface.Client - l2Clients iface.L2Clients - rollupContract iface.IRollup - l2Caller *types.L2Caller + l1Client L1HeaderClient + l2Clients L2MultiClient + rollupContract RollupBatchReader + l2Gov L2GovCaller // config batchTimeOut uint64 blockInterval uint64 + maxBlobCount int + + // replayL1CommittedBatches is true while InitAndSyncFromRollup is rebuilding committed batches from L2. + replayL1CommittedBatches atomic.Bool } +type batchPackProgressState struct { + lastLoggedOverallPercent uint64 +} + +const batchProgressLogStepPercent uint64 = 20 + +// replayProtocolMaxBlobs is the EIP-4844 per-transaction blob ceiling used when re-sealing +// batches already committed on L1 (independent of max_blob_count flag). +const replayProtocolMaxBlobs = 6 + // NewBatchCache creates and initializes a new BatchCache instance func NewBatchCache( isBatchUpgraded func(uint64) bool, - l1Client iface.Client, - l2Clients []iface.L2Client, - rollupContract iface.IRollup, - l2Caller *types.L2Caller, - ldb *db.Db, + isBatchV2Upgraded func(uint64) bool, + maxBlobCount int, + l1Client L1HeaderClient, + l2Clients L2MultiClient, + rollupContract RollupBatchReader, + l2Gov L2GovCaller, + ldb SealedBatchKV, ) *BatchCache { if isBatchUpgraded == nil { // Default implementation: always returns true (use V1 version) isBatchUpgraded = func(uint64) bool { return true } } + if isBatchV2Upgraded == nil { + // Default: V2 not yet activated + isBatchV2Upgraded = func(uint64) bool { return false } + } + if maxBlobCount <= 0 { + maxBlobCount = 2 + } ctx := context.Background() - ifL2Clients := iface.L2Clients{Clients: l2Clients} - _, err := ifL2Clients.BlockNumber(ctx) + _, err := l2Clients.BlockNumber(ctx) if err != nil { log.Error("Error getting block number", "err", err) } @@ -113,11 +136,13 @@ func NewBatchCache( currentBlockNumber: 0, currentBlockHash: common.Hash{}, isBatchUpgraded: isBatchUpgraded, + isBatchV2Upgraded: isBatchV2Upgraded, l1Client: l1Client, - l2Clients: iface.L2Clients{Clients: l2Clients}, + l2Clients: l2Clients, rollupContract: rollupContract, - l2Caller: l2Caller, + l2Gov: l2Gov, batchStorage: NewBatchStorage(ldb), + maxBlobCount: maxBlobCount, } } @@ -269,6 +294,9 @@ func (bc *BatchCache) InitAndSyncFromRollup() error { if bc.initDone { return nil } + bc.replayL1CommittedBatches.Store(true) + defer bc.replayL1CommittedBatches.Store(false) + err := bc.Init() if err != nil { return err @@ -290,7 +318,8 @@ func (bc *BatchCache) InitAndSyncFromRollup() error { return fmt.Errorf("get batch block range err: %w,start %v, end %v", err, startNum, endNum) } log.Info("assemble batch block range", "startNum", startNum, "endNum", endNum) - batchHeaderBytes, err := bc.assembleBatchHeaderFromL2Blocks(startNum, endNum) + replayIdx := i + batchHeaderBytes, err := bc.assembleBatchHeaderFromL2Blocks(startNum, endNum, &replayIdx) if err != nil { return err } @@ -317,11 +346,11 @@ func (bc *BatchCache) LatestBatchIndex() (uint64, error) { } func (bc *BatchCache) updateBatchConfigFromGov() error { - interval, err := bc.l2Caller.BatchBlockInterval(nil) + interval, err := bc.l2Gov.BatchBlockInterval(nil) if err != nil { return err } - timeout, err := bc.l2Caller.BatchTimeout(nil) + timeout, err := bc.l2Gov.BatchTimeout(nil) if err != nil { return err } @@ -452,7 +481,7 @@ func (bc *BatchCache) GetLatestSealedBatchIndex() uint64 { // CalculateCapWithProposalBlock calculates batch capacity after including the specified block func (bc *BatchCache) CalculateCapWithProposalBlock(blockNumber uint64, withdrawRoot common.Hash) (bool, error) { - if len(bc.l2Clients.Clients) == 0 { + if bc.l2Clients.Len() == 0 { return false, fmt.Errorf("l2 client is nil") } @@ -514,11 +543,20 @@ func (bc *BatchCache) CalculateCapWithProposalBlock(blockNumber uint64, withdraw bc.currentWithdrawRoot = withdrawRoot // Check capacity: if compressed size would exceed limit after adding current block + effectiveBlobCount := bc.effectiveMaxBlobCount(header.Time) + log.Debug("batch capacity check", + "proposedBlock", blockNumber, + "blockTime", header.Time, + "compressedLimitBytes", effectiveBlobCount*blob.MaxBlobBytesSize, + "effectiveBlobCount", effectiveBlobCount, + "configuredMaxBlobCount", bc.maxBlobCount, + "v2Upgraded", bc.isBatchV2Upgraded(header.Time), + ) var exceeded bool if bc.isBatchUpgraded(header.Time) { - exceeded, err = bc.batchData.WillExceedCompressedSizeLimit(blockContext, txsPayload) + exceeded, err = bc.batchData.WillExceedCompressedSizeLimit(blockContext, txsPayload, effectiveBlobCount) } else { - exceeded, err = bc.batchData.EstimateCompressedSizeWithNewPayload(txsPayload) + exceeded, err = bc.batchData.EstimateCompressedSizeWithNewPayload(txsPayload, effectiveBlobCount) } if err != nil { return false, fmt.Errorf("failed to estimate compressed size: %w", err) @@ -608,7 +646,11 @@ func (bc *BatchCache) FetchAndCacheHeader(blockNumber uint64, withdrawRoot commo // - error: returns error if sealing fails // // Note: Sealed batch will be stored in BatchCache's sealedBatches, not sent anywhere -func (bc *BatchCache) SealBatch(sequencerSets []byte, blockTimestamp uint64) (uint64, BatchHeaderBytes, bool, error) { +// +// replayCommittedBatchIndex, when non-nil, is the rollup batch index being re-sealed while syncing +// from L1 (InitAndSyncFromRollup). After V2 multi-blob, blob capacity is capped at replayProtocolMaxBlobs +// (6), not max_blob_count, without querying L1 CommitBatch logs. +func (bc *BatchCache) SealBatch(sequencerSets []byte, blockTimestamp uint64, replayCommittedBatchIndex *uint64) (uint64, BatchHeaderBytes, bool, error) { bc.mu.Lock() defer bc.mu.Unlock() @@ -617,24 +659,37 @@ func (bc *BatchCache) SealBatch(sequencerSets []byte, blockTimestamp uint64) (ui return 0, BatchHeaderBytes{}, false, errors.New("failed to seal batch: batch cache is empty") } + sealBlobCap := bc.sealEffectiveBlobCount(blockTimestamp, replayCommittedBatchIndex) + // Compress data and calculate dataHash - compressedPayload, batchDataHash, err := bc.handleBatchSealing(blockTimestamp) + compressedPayload, batchDataHash, sealBlobCap, err := bc.handleBatchSealing(blockTimestamp, sealBlobCap, replayCommittedBatchIndex) if err != nil { return 0, BatchHeaderBytes{}, false, fmt.Errorf("failed to handle batch sealing: %w", err) } // Check if sealed data size reaches expected value - // Expected value: compressed payload size close to or reaches MaxBlobBytesSize - // Use 90% as threshold, i.e., if compressed size >= MaxBlobBytesSize * 0.9, consider it reached expected - threshold := float64(MaxBlobBytesSize) * 0.9 + // Expected value: compressed payload size close to or reaches total blob capacity + // Use 90% as threshold, i.e., if compressed size >= totalCapacity * 0.9, consider it reached expected + effectiveBlobCount := sealBlobCap + totalBlobCapacity := effectiveBlobCount * blob.MaxBlobBytesSize + threshold := float64(totalBlobCapacity) * 0.9 expectedSizeThreshold := uint64(threshold) reachedExpectedSize := uint64(len(compressedPayload)) >= expectedSizeThreshold // Generate blob sidecar - sidecar, err := MakeBlobTxSidecar(compressedPayload) + sidecar, err := blob.MakeBlobTxSidecar(compressedPayload, effectiveBlobCount) if err != nil { return 0, BatchHeaderBytes{}, false, fmt.Errorf("failed to create blob sidecar: %w", err) } + log.Info("Sealing batch payload stats", + "compressedPayloadBytes", len(compressedPayload), + "effectiveBlobCount", effectiveBlobCount, + "configuredMaxBlobCount", bc.maxBlobCount, + "replayCommittedBatchIndex", replayCommittedBatchIndex, + "v2Upgraded", bc.isBatchV2Upgraded(blockTimestamp), + "sidecarBlobCount", len(sidecar.Blobs), + "sidecarCapacityBytes", effectiveBlobCount*blob.MaxBlobBytesSize, + ) // Create batch header batchHeader := bc.createBatchHeader(batchDataHash, sidecar, crypto.Keccak256Hash(sequencerSets), blockTimestamp) @@ -711,7 +766,7 @@ func (bc *BatchCache) SealBatch(sequencerSets []byte, blockTimestamp uint64) (ui // Save block count before resetting batch data for logging blockCount := bc.batchData.BlockNum() - bc.logSealedBatch(batchHeader, batchHash, blockCount) + bc.logSealedBatch(batchHeader, batchHash, blockCount, len(sidecar.Blobs)) // Reset currently accumulated batch data bc.batchData = NewBatchData() @@ -719,8 +774,9 @@ func (bc *BatchCache) SealBatch(sequencerSets []byte, blockTimestamp uint64) (ui return batchIndex, batchHeader, reachedExpectedSize, nil } -// handleBatchSealing determines which version to use for compression and calculates data hash -func (bc *BatchCache) handleBatchSealing(blockTimestamp uint64) ([]byte, common.Hash, error) { +// handleBatchSealing determines which version to use for compression and calculates data hash. +// The returned sealBlobCap may be raised during L1 batch replay so the compressed payload fits. +func (bc *BatchCache) handleBatchSealing(blockTimestamp uint64, sealBlobCap int, replayCommittedBatchIndex *uint64) ([]byte, common.Hash, int, error) { var ( compressedPayload []byte batchDataHash common.Hash @@ -729,33 +785,107 @@ func (bc *BatchCache) handleBatchSealing(blockTimestamp uint64) ([]byte, common. // Check if upgraded version should be used if bc.isBatchUpgraded(blockTimestamp) { - compressedPayload, err = CompressBatchBytes(bc.batchData.TxsPayloadV2()) + compressedPayload, err = blob.CompressBatchBytes(bc.batchData.TxsPayloadV2()) if err != nil { - return nil, common.Hash{}, fmt.Errorf("failed to compress upgraded payload: %w", err) + return nil, common.Hash{}, sealBlobCap, fmt.Errorf("failed to compress upgraded payload: %w", err) } - if len(compressedPayload) <= MaxBlobBytesSize { + replayRaise := replayCommittedBatchIndex != nil || bc.replayL1CommittedBatches.Load() + + if replayRaise { + needed := (len(compressedPayload) + blob.MaxBlobBytesSize - 1) / blob.MaxBlobBytesSize + if needed > replayProtocolMaxBlobs { + if replayCommittedBatchIndex != nil { + return nil, common.Hash{}, sealBlobCap, fmt.Errorf( + "replay batch %d: compressed payload needs %d blobs (protocol max %d)", + *replayCommittedBatchIndex, needed, replayProtocolMaxBlobs, + ) + } + return nil, common.Hash{}, sealBlobCap, fmt.Errorf( + "replay (L1 sync): compressed payload needs %d blobs (protocol max %d)", needed, replayProtocolMaxBlobs, + ) + } + if needed > sealBlobCap { + if replayCommittedBatchIndex != nil { + log.Info("replay: raising seal blob cap to fit compressed V2 payload", + "batchIndex", *replayCommittedBatchIndex, + "fromBlobs", sealBlobCap, "toBlobs", needed, + "compressedBytes", len(compressedPayload)) + } else { + log.Info("replay: raising seal blob cap to fit compressed V2 payload", + "fromBlobs", sealBlobCap, "toBlobs", needed, + "compressedBytes", len(compressedPayload)) + } + sealBlobCap = needed + } + } + + if len(compressedPayload) <= sealBlobCap*blob.MaxBlobBytesSize { batchDataHash, err = bc.batchData.DataHashV2() if err != nil { - return nil, common.Hash{}, fmt.Errorf("failed to calculate upgraded data hash: %w", err) + return nil, common.Hash{}, sealBlobCap, fmt.Errorf("failed to calculate upgraded data hash: %w", err) } - return compressedPayload, batchDataHash, nil + return compressedPayload, batchDataHash, sealBlobCap, nil + } + if bc.isBatchV2Upgraded(blockTimestamp) { + return nil, common.Hash{}, sealBlobCap, fmt.Errorf( + "compressed V2 batch size %d exceeds capacity for %d blobs (%d bytes)", + len(compressedPayload), sealBlobCap, sealBlobCap*blob.MaxBlobBytesSize, + ) } } // Fall back to the old version - compressedPayload, err = CompressBatchBytes(bc.batchData.TxsPayload()) + compressedPayload, err = blob.CompressBatchBytes(bc.batchData.TxsPayload()) if err != nil { - return nil, common.Hash{}, fmt.Errorf("failed to compress payload: %w", err) + return nil, common.Hash{}, sealBlobCap, fmt.Errorf("failed to compress payload: %w", err) } + + replayRaise := replayCommittedBatchIndex != nil || bc.replayL1CommittedBatches.Load() + + if replayRaise { + needed := (len(compressedPayload) + blob.MaxBlobBytesSize - 1) / blob.MaxBlobBytesSize + if needed > replayProtocolMaxBlobs { + if replayCommittedBatchIndex != nil { + return nil, common.Hash{}, sealBlobCap, fmt.Errorf( + "replay batch %d: legacy compressed payload needs %d blobs (protocol max %d)", + *replayCommittedBatchIndex, needed, replayProtocolMaxBlobs, + ) + } + return nil, common.Hash{}, sealBlobCap, fmt.Errorf( + "replay (L1 sync): legacy compressed payload needs %d blobs (protocol max %d)", needed, replayProtocolMaxBlobs, + ) + } + if needed > sealBlobCap { + if replayCommittedBatchIndex != nil { + log.Info("replay: raising seal blob cap to fit legacy compressed payload", + "batchIndex", *replayCommittedBatchIndex, + "fromBlobs", sealBlobCap, "toBlobs", needed, + "compressedBytes", len(compressedPayload)) + } else { + log.Info("replay: raising seal blob cap to fit legacy compressed payload", + "fromBlobs", sealBlobCap, "toBlobs", needed, + "compressedBytes", len(compressedPayload)) + } + sealBlobCap = needed + } + } + + if len(compressedPayload) > sealBlobCap*blob.MaxBlobBytesSize { + return nil, common.Hash{}, sealBlobCap, fmt.Errorf( + "compressed batch size %d exceeds capacity for %d blobs (%d bytes)", + len(compressedPayload), sealBlobCap, sealBlobCap*blob.MaxBlobBytesSize, + ) + } + batchDataHash = bc.batchData.DataHash() - return compressedPayload, batchDataHash, nil + return compressedPayload, batchDataHash, sealBlobCap, nil } // createBatchHeader creates BatchHeader func (bc *BatchCache) createBatchHeader(dataHash common.Hash, sidecar *ethtypes.BlobTxSidecar, sequencerSetVerifyHash common.Hash, blockTimestamp uint64) BatchHeaderBytes { - blobHashes := []common.Hash{EmptyVersionedHash} + blobHashes := []common.Hash{blob.EmptyVersionedHash} if sidecar != nil && len(sidecar.Blobs) > 0 { blobHashes = sidecar.BlobHashes() } @@ -790,6 +920,14 @@ func (bc *BatchCache) createBatchHeader(dataHash common.Hash, sidecar *ethtypes. BatchHeaderV0: batchHeaderV0, LastBlockNumber: bc.lastPackedBlockHeight, } + // V2 is activated: use V1-format header (257 bytes) with version byte 2. + // Store keccak256(concat all blob hashes) at offset 57 as the aggregated blob hash. + if bc.isBatchV2Upgraded(blockTimestamp) { + batchHeaderV1.BlobVersionedHash = aggregateBlobHashes(blobHashes) + h := batchHeaderV1.Bytes() + h[0] = BatchHeaderVersion2 + return h + } return batchHeaderV1.Bytes() } @@ -839,6 +977,39 @@ func isL1MessageTxType(tx *ethtypes.Transaction) bool { return tx.Type() == ethtypes.L1MessageTxType } +// aggregateBlobHashes computes keccak256 of the concatenation of all blob hash bytes. +func aggregateBlobHashes(hashes []common.Hash) common.Hash { + var concat []byte + for _, h := range hashes { + concat = append(concat, h[:]...) + } + return crypto.Keccak256Hash(concat) +} + +// effectiveMaxBlobCount returns the allowed blob count for the given block timestamp. +// V2 multi-blob is only permitted when isBatchV2Upgraded returns true; otherwise cap at 1. +func (bc *BatchCache) effectiveMaxBlobCount(blockTimestamp uint64) int { + if bc.isBatchV2Upgraded(blockTimestamp) { + return bc.maxBlobCount + } + return 1 +} + +// sealEffectiveBlobCount is the blob count used for sealing. +// Live packing uses effectiveMaxBlobCount (max_blob_count flag). +// Replaying an L1-committed batch after V2 multi-blob uses replayProtocolMaxBlobs (6), independent of +// max_blob_count and without L1 log queries; handleBatchSealing tightens from compressed size (still ≤6). +func (bc *BatchCache) sealEffectiveBlobCount(blockTimestamp uint64, replayCommittedBatchIndex *uint64) int { + base := bc.effectiveMaxBlobCount(blockTimestamp) + if replayCommittedBatchIndex == nil { + return base + } + if !bc.isBatchV2Upgraded(blockTimestamp) { + return base + } + return replayProtocolMaxBlobs +} + // buildBlockContext builds BlockContext from block header (60 bytes) // Format: Number(8) || Timestamp(8) || BaseFee(32) || GasLimit(8) || numTxs(2) || numL1Messages(2) func buildBlockContext(header *ethtypes.Header, txsNum, l1MsgNum int) []byte { @@ -871,7 +1042,14 @@ func buildBlockContext(header *ethtypes.Header, txsNum, l1MsgNum int) []byte { func (bc *BatchCache) assembleBatchHeaderFromL2Blocks( startBlockNum, endBlockNum uint64, + replayCommittedBatchIndex *uint64, ) (*BatchHeaderBytes, error) { + // Fresh accumulation for this chain batch; a failed prior SealBatch must not double-pack blocks. + bc.mu.Lock() + bc.batchData = NewBatchData() + bc.ClearCurrent() + bc.mu.Unlock() + ctx := context.Background() callOpts := &bind.CallOpts{ Context: ctx, @@ -879,7 +1057,7 @@ func (bc *BatchCache) assembleBatchHeaderFromL2Blocks( // Fetch blocks from L2 client in the specified range and accumulate to batch for blockNum := startBlockNum; blockNum <= endBlockNum; blockNum++ { callOpts.BlockNumber = new(big.Int).SetUint64(blockNum) - root, err := bc.l2Caller.GetTreeRoot(callOpts) + root, err := bc.l2Gov.GetTreeRoot(callOpts) if err != nil { return nil, fmt.Errorf("failed to get withdraw root at block %d: %w", blockNum, err) } @@ -896,7 +1074,7 @@ func (bc *BatchCache) assembleBatchHeaderFromL2Blocks( } } - sequencerSet, _, err := bc.l2Caller.GetSequencerSetBytes(callOpts) + sequencerSet, _, err := bc.l2Gov.GetSequencerSetBytes(callOpts) if err != nil { return nil, fmt.Errorf("failed to get sequencer set verify hash at block %d: %w", callOpts.BlockNumber.Uint64(), err) } @@ -908,7 +1086,7 @@ func (bc *BatchCache) assembleBatchHeaderFromL2Blocks( blockTimestamp := lastBlock.Time() // Seal batch and generate batchHeader - batchIndex, batchHeader, reachedExpectedSize, err := bc.SealBatch(sequencerSet, blockTimestamp) + batchIndex, batchHeader, reachedExpectedSize, err := bc.SealBatch(sequencerSet, blockTimestamp, replayCommittedBatchIndex) if err != nil { return nil, fmt.Errorf("failed to seal batch: %w", err) } @@ -937,11 +1115,12 @@ func (bc *BatchCache) assembleUnFinalizeBatchHeaderFromL2Blocks() error { return fmt.Errorf("failed to get start block %d: %w", startBlockNum, err) } startBlockTime := startBlock.Time() + progressState := batchPackProgressState{} // Fetch blocks from L2 client in the specified range and accumulate to batch for blockNum := startBlockNum; blockNum <= endBlockNum; blockNum++ { callOpts.BlockNumber = new(big.Int).SetUint64(blockNum) - root, err := bc.l2Caller.GetTreeRoot(callOpts) + root, err := bc.l2Gov.GetTreeRoot(callOpts) if err != nil { return fmt.Errorf("failed to get withdraw root at block %d: %w", blockNum, err) } @@ -958,6 +1137,7 @@ func (bc *BatchCache) assembleUnFinalizeBatchHeaderFromL2Blocks() error { return fmt.Errorf("failed to get block %d: %w", blockNum, err) } nowBlockTime := nowBlock.Time() + bc.logBatchPackingProgress(startBlockNum, blockNum, startBlockTime, nowBlockTime, &progressState) // Check timeout: if elapsed time >= batchTimeOut, must seal batch immediately // This ensures batch is sealed before exceeding the maximum timeout configured in gov contract @@ -989,6 +1169,7 @@ func (bc *BatchCache) assembleUnFinalizeBatchHeaderFromL2Blocks() error { return fmt.Errorf("failed to get start block %d: %w", startBlockNum, err) } startBlockTime = startBlock.Time() + progressState = batchPackProgressState{} index, err := bc.parentBatchHeader.BatchIndex() if err != nil { return err @@ -1005,7 +1186,7 @@ func (bc *BatchCache) assembleUnFinalizeBatchHeaderFromL2Blocks() error { } func (bc *BatchCache) SealBatchAndCheck(callOpts *bind.CallOpts, ci *big.Int) (common.Hash, bool, uint64, error) { - sequencerSetBytes, _, err := bc.l2Caller.GetSequencerSetBytes(callOpts) + sequencerSetBytes, _, err := bc.l2Gov.GetSequencerSetBytes(callOpts) if err != nil { return common.Hash{}, false, 0, err } @@ -1015,7 +1196,7 @@ func (bc *BatchCache) SealBatchAndCheck(callOpts *bind.CallOpts, ci *big.Int) (c } blockTimestamp := lastBlock.Time() // Seal batch and generate batchHeader - batchIndex, batchHeaderBytes, reachedExpectedSize, err := bc.SealBatch(sequencerSetBytes, blockTimestamp) + batchIndex, batchHeaderBytes, reachedExpectedSize, err := bc.SealBatch(sequencerSetBytes, blockTimestamp, nil) if err != nil { return common.Hash{}, false, 0, fmt.Errorf("failed to seal batch: %w", err) } @@ -1078,19 +1259,27 @@ func (bc *BatchCache) Delete(batchIndex uint64) error { } // logSealedBatch logs the details of the sealed batch for debugging purposes. -func (bc *BatchCache) logSealedBatch(batchHeader BatchHeaderBytes, batchHash common.Hash, blockCount uint16) { - log.Info("Sealed batch header", "batchHash", batchHash.Hex()) +func (bc *BatchCache) logSealedBatch(batchHeader BatchHeaderBytes, batchHash common.Hash, blockCount uint16, blobCount int) { + version, err := batchHeader.Version() + if err != nil { + version = 0 + } + blobVersionedHash, _ := batchHeader.BlobVersionedHash() + log.Info("Sealed batch header", "batchHash", batchHash.Hex(), "version", version, "blobVersionedHash", blobVersionedHash.Hex()) batchIndex, _ := batchHeader.BatchIndex() l1MessagePopped, _ := batchHeader.L1MessagePopped() totalL1MessagePopped, _ := batchHeader.TotalL1MessagePopped() dataHash, _ := batchHeader.DataHash() parentBatchHash, _ := batchHeader.ParentBatchHash() - log.Info(fmt.Sprintf("===batchIndex: %d \n===L1MessagePopped: %d \n===TotalL1MessagePopped: %d \n===dataHash: %x \n===blockCount: %d \n===ParentBatchHash: %x \n", + log.Info(fmt.Sprintf("===version: %d \n===batchIndex: %d \n===L1MessagePopped: %d \n===TotalL1MessagePopped: %d \n===dataHash: %x \n===BlobVersionedHash: %x \n===blockCount: %d \n===blobCount: %d \n===ParentBatchHash: %x \n", + version, batchIndex, l1MessagePopped, totalL1MessagePopped, dataHash, + blobVersionedHash, blockCount, + blobCount, parentBatchHash)) } @@ -1144,11 +1333,12 @@ func (bc *BatchCache) AssembleCurrentBatchHeader() error { return fmt.Errorf("failed to get start block %d: %w", startBlockNum, err) } startBlockTime := startBlock.Time() + progressState := batchPackProgressState{} // Fetch blocks from L2 client in the specified range and accumulate to batch for blockNum := currentBlockNum + 1; blockNum <= endBlockNum; blockNum++ { callOpts.BlockNumber = new(big.Int).SetUint64(blockNum) - root, err := bc.l2Caller.GetTreeRoot(callOpts) + root, err := bc.l2Gov.GetTreeRoot(callOpts) if err != nil { return fmt.Errorf("failed to get withdraw root at block %d: %w", blockNum, err) } @@ -1165,6 +1355,7 @@ func (bc *BatchCache) AssembleCurrentBatchHeader() error { return fmt.Errorf("failed to get block %d: %w", blockNum, err) } nowBlockTime := nowBlock.Time() + bc.logBatchPackingProgress(startBlockNum, blockNum, startBlockTime, nowBlockTime, &progressState) // Check timeout: if elapsed time >= batchTimeOut, must seal batch immediately // This ensures batch is sealed before exceeding the maximum timeout configured in gov contract @@ -1182,7 +1373,7 @@ func (bc *BatchCache) AssembleCurrentBatchHeader() error { // check ensures batch is sealed before exceeding the maximum timeout if exceeded || (bc.blockInterval > 0 && (blockNum-startBlockNum+1) == bc.blockInterval) || timeout { log.Info("block exceeds limit", "start", startBlockNum, "to", blockNum, "exceeded", exceeded, "timeout", timeout) - sequencerSetBytes, _, err := bc.l2Caller.GetSequencerSetBytes(callOpts) + sequencerSetBytes, _, err := bc.l2Gov.GetSequencerSetBytes(callOpts) if err != nil { return fmt.Errorf("failed to get sequencer set verify hash at block %d: %w", callOpts.BlockNumber.Uint64(), err) } @@ -1191,7 +1382,7 @@ func (bc *BatchCache) AssembleCurrentBatchHeader() error { return fmt.Errorf("failed to get last block %d: %w", bc.lastPackedBlockHeight, err) } blockTimestamp := lastBlock.Time() - batchIndex, _, _, err := bc.SealBatch(sequencerSetBytes, blockTimestamp) + batchIndex, _, _, err := bc.SealBatch(sequencerSetBytes, blockTimestamp, nil) if err != nil { return fmt.Errorf("failed to seal batch: %w", err) } @@ -1205,6 +1396,7 @@ func (bc *BatchCache) AssembleCurrentBatchHeader() error { return fmt.Errorf("failed to get start block %d: %w", startBlockNum, err) } startBlockTime = startBlock.Time() + progressState = batchPackProgressState{} } // Pack current block (confirm and append to batch) @@ -1215,6 +1407,97 @@ func (bc *BatchCache) AssembleCurrentBatchHeader() error { return nil } +func (bc *BatchCache) logBatchPackingProgress(startBlockNum, currentBlockNum, startBlockTime, currentBlockTime uint64, state *batchPackProgressState) { + if state == nil || currentBlockNum < startBlockNum { + return + } + + elapsedTime := uint64(0) + if currentBlockTime >= startBlockTime { + elapsedTime = currentBlockTime - startBlockTime + } + + packedBlocks := currentBlockNum - startBlockNum + 1 + effectiveBlobCount := bc.effectiveMaxBlobCount(currentBlockTime) + totalBlobCapacity := uint64(effectiveBlobCount * blob.MaxBlobBytesSize) + payloadBytes := uint64(0) + if totalBlobCapacity > 0 { + payloadBytes = bc.estimatedBatchPayloadBytesWithCurrent(currentBlockTime) + } + + timePercent := uint64(0) + if bc.batchTimeOut > 0 { + timePercent = progressPercent(elapsedTime, bc.batchTimeOut) + } + + blockPercent := uint64(0) + if bc.blockInterval > 0 { + blockPercent = progressPercent(packedBlocks, bc.blockInterval) + } + + blobPercent := uint64(0) + if totalBlobCapacity > 0 { + blobPercent = progressPercent(payloadBytes, totalBlobCapacity) + } + + overallPercent := maxUint64(timePercent, blockPercent, blobPercent) + // Throttle progress logs to reduce noisy output. + overallStep := (overallPercent / batchProgressLogStepPercent) * batchProgressLogStepPercent + if overallStep <= state.lastLoggedOverallPercent { + return + } + state.lastLoggedOverallPercent = overallStep + + log.Info("Batch packing progress", + "loadedBlockHeight", currentBlockNum, + "overallPercent", overallPercent, + "timePercent", timePercent, + "blockPercent", blockPercent, + "blobPercent", blobPercent, + ) +} + +func (bc *BatchCache) estimatedBatchPayloadBytesWithCurrent(blockTimestamp uint64) uint64 { + bc.mu.RLock() + defer bc.mu.RUnlock() + + var ( + existingBlockContextLen int + existingTxPayloadLen int + ) + if bc.batchData != nil { + existingBlockContextLen = len(bc.batchData.blockContexts) + existingTxPayloadLen = len(bc.batchData.txsPayload) + } + + if bc.isBatchUpgraded(blockTimestamp) { + return uint64(existingBlockContextLen + len(bc.currentBlockContext) + existingTxPayloadLen + len(bc.currentTxsPayload)) + } + + return uint64(existingTxPayloadLen + len(bc.currentTxsPayload)) +} + +func progressPercent(current, total uint64) uint64 { + if total == 0 { + return 0 + } + p := current * 100 / total + if p > 100 { + return 100 + } + return p +} + +func maxUint64(values ...uint64) uint64 { + var max uint64 + for _, v := range values { + if v > max { + max = v + } + } + return max +} + func (bc *BatchCache) DeleteBatchStorageAndInitFromRollup() error { // should delete invalid batch data and batch header bytes err := bc.batchStorage.DeleteAllSealedBatches() diff --git a/common/batch/batch_cache_genesis_header_test.go b/common/batch/batch_cache_genesis_header_test.go new file mode 100644 index 000000000..fed8f8318 --- /dev/null +++ b/common/batch/batch_cache_genesis_header_test.go @@ -0,0 +1,142 @@ +package batch + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "testing" + "time" + + "github.com/morph-l2/go-ethereum/common/hexutil" + "github.com/morph-l2/go-ethereum/log" + "github.com/stretchr/testify/require" +) + +var ( + // Fill this with hex-encoded batch header bytes, e.g. "0x00....". + // This test will use it as the genesis parent header to initialize cache. + globalGenesisBatchHeaderHex = "0x00000000000000000000000000000000000000000000000000d81a073a4abd227068a2a334f4a41b3abba26144dc866a78ed28e2ae90f86f5a010657f37554c781402a22917dee2f75def7ab966d7b770905398eba3c4440140000000000000000000000000000000000000000000000000000000000000000290233e7a85533655c301d3e1043f03acd5427c73d1bbcbf8784db3f3974327f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + globalGenesisBatchHeader *BatchHeaderBytes + globalGenesisBatchHeaderErr error + globalGenesisBatchHeaderOnce sync.Once + + // Global overrides for cache batch config in tests (instead of updateBatchConfigFromGov). + globalBatchTimeoutForTest uint64 = 10000000 + globalBlockIntervalForTest uint64 = 10000 +) + +func ensureGlobalGenesisBatchHeader() error { + globalGenesisBatchHeaderOnce.Do(func() { + if globalGenesisBatchHeaderHex == "" { + globalGenesisBatchHeaderErr = fmt.Errorf("globalGenesisBatchHeaderHex is empty") + return + } + raw, err := hexutil.Decode(globalGenesisBatchHeaderHex) + if err != nil { + globalGenesisBatchHeaderErr = fmt.Errorf("decode globalGenesisBatchHeaderHex failed: %w", err) + return + } + header := BatchHeaderBytes(raw) + if err := header.validate(); err != nil { + globalGenesisBatchHeaderErr = fmt.Errorf("invalid global genesis batch header: %w", err) + return + } + globalGenesisBatchHeader = &header + }) + return globalGenesisBatchHeaderErr +} + +// initCacheWithGlobalGenesisHeader initializes cache base fields from the +// globally cached genesis batch header, instead of loading through Init(). +func initCacheWithGlobalGenesisHeader(cache *BatchCache) error { + if err := ensureGlobalGenesisBatchHeader(); err != nil { + return err + } + if globalGenesisBatchHeader == nil { + return ErrKeyNotFound + } + // Use global test knobs instead of querying gov config from chain. + cache.batchTimeOut = globalBatchTimeoutForTest + cache.blockInterval = globalBlockIntervalForTest + headerCopy := make(BatchHeaderBytes, len(*globalGenesisBatchHeader)) + copy(headerCopy, *globalGenesisBatchHeader) + cache.parentBatchHeader = &headerCopy + + prevStateRoot, err := cache.parentBatchHeader.PostStateRoot() + if err != nil { + return err + } + cache.prevStateRoot = prevStateRoot + + totalL1MessagePopped, err := cache.parentBatchHeader.TotalL1MessagePopped() + if err != nil { + return err + } + cache.totalL1MessagePopped = totalL1MessagePopped + + lastPackedBlockHeight, err := cache.parentBatchHeader.LastBlockNumber() + if err != nil { + lastPackedBlockHeight = 0 + } + cache.lastPackedBlockHeight = lastPackedBlockHeight + cache.currentBlockNumber = lastPackedBlockHeight + cache.initDone = true + + return nil +} + +func TestBatchCacheInitWithGlobalGenesisHeader(t *testing.T) { + testDB := openTestKV(t) + a := func(uint64) bool { return true } + cache := NewBatchCache(nil, a, 3, l1Client, &SingleL2Client{C: l2Client}, rollupContract, l2Gov, testDB) + + var batchCacheSyncMu sync.Mutex + done := make(chan error, 1) + go func() { + batchCacheSyncMu.Lock() + defer batchCacheSyncMu.Unlock() + for { + if err := initCacheWithGlobalGenesisHeader(cache); err != nil { + log.Error("init with global genesis header failed, wait for next try", "error", err) + time.Sleep(3 * time.Second) + continue + } + done <- nil + return + } + }() + + select { + case err := <-done: + require.NoError(t, err) + case <-time.After(20 * time.Second): + t.Fatal("timeout waiting for cache init with global genesis header") + } + + require.True(t, cache.initDone) + require.NotNil(t, cache.parentBatchHeader) + version, err := cache.parentBatchHeader.Version() + require.NoError(t, err) + require.Equal(t, uint8(BatchHeaderVersion0), version) + require.Equal(t, cache.lastPackedBlockHeight, cache.currentBlockNumber) + _, err = cache.l2Clients.BlockNumber(context.Background()) + require.NoError(t, err) + + go testLoop(cache.ctx, 5*time.Second, func() { + batchCacheSyncMu.Lock() + defer batchCacheSyncMu.Unlock() + err := cache.AssembleCurrentBatchHeader() + if err != nil { + log.Error("Assemble current batch failed, wait for the next try", "error", err) + } + }) + + // Catch CTRL-C to ensure a graceful shutdown. + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt) + + // Wait until the interrupt signal is received from an OS signal. + <-interrupt +} diff --git a/tx-submitter/batch/batch_cache_test.go b/common/batch/batch_cache_test.go similarity index 54% rename from tx-submitter/batch/batch_cache_test.go rename to common/batch/batch_cache_test.go index a15977295..3965fa4aa 100644 --- a/tx-submitter/batch/batch_cache_test.go +++ b/common/batch/batch_cache_test.go @@ -3,16 +3,11 @@ package batch import ( "os" "os/signal" - "path/filepath" "sync" "testing" "time" "morph-l2/bindings/bindings" - "morph-l2/tx-submitter/db" - "morph-l2/tx-submitter/iface" - "morph-l2/tx-submitter/types" - "morph-l2/tx-submitter/utils" "github.com/morph-l2/go-ethereum/log" "github.com/stretchr/testify/require" @@ -24,28 +19,15 @@ func init() { if err != nil { panic(err) } - l2Caller, err = types.NewL2Caller([]iface.L2Client{l2Client}) + l2Gov, err = NewL2Gov(l2Client) if err != nil { panic(err) } } -// setupTestDB creates a temporary database for testing -func setupTestDB(t *testing.T) *db.Db { - testDir := filepath.Join(t.TempDir(), "testleveldb") - os.RemoveAll(testDir) - t.Cleanup(func() { - os.RemoveAll(testDir) - }) - - testDB, err := db.New(testDir) - require.NoError(t, err) - return testDB -} - func TestBatchCacheInitServer(t *testing.T) { - testDB := setupTestDB(t) - cache := NewBatchCache(nil, l1Client, []iface.L2Client{l2Client}, rollupContract, l2Caller, testDB) + testDB := openTestKV(t) + cache := NewBatchCache(nil, nil, 2, l1Client, &SingleL2Client{C: l2Client}, rollupContract, l2Gov, testDB) var batchCacheSyncMu sync.Mutex @@ -62,7 +44,7 @@ func TestBatchCacheInitServer(t *testing.T) { } }() - go utils.Loop(cache.ctx, 5*time.Second, func() { + go testLoop(cache.ctx, 5*time.Second, func() { batchCacheSyncMu.Lock() defer batchCacheSyncMu.Unlock() err := cache.AssembleCurrentBatchHeader() @@ -71,24 +53,21 @@ func TestBatchCacheInitServer(t *testing.T) { } }) - // Catch CTRL-C to ensure a graceful shutdown. interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt) - - // Wait until the interrupt signal is received from an OS signal. <-interrupt } func TestBatchCacheInit(t *testing.T) { - testDB := setupTestDB(t) - cache := NewBatchCache(nil, l1Client, []iface.L2Client{l2Client}, rollupContract, l2Caller, testDB) + testDB := openTestKV(t) + cache := NewBatchCache(nil, nil, 2, l1Client, &SingleL2Client{C: l2Client}, rollupContract, l2Gov, testDB) err := cache.InitAndSyncFromRollup() require.NoError(t, err) } func TestBatchCacheInitByBlockRange(t *testing.T) { - testDB := setupTestDB(t) - cache := NewBatchCache(nil, l1Client, []iface.L2Client{l2Client}, rollupContract, l2Caller, testDB) + testDB := openTestKV(t) + cache := NewBatchCache(nil, nil, 2, l1Client, &SingleL2Client{C: l2Client}, rollupContract, l2Gov, testDB) err := cache.InitFromRollupByRange() require.NoError(t, err) } diff --git a/tx-submitter/batch/batch_data.go b/common/batch/batch_data.go similarity index 86% rename from tx-submitter/batch/batch_data.go rename to common/batch/batch_data.go index b28e2c65f..d5ac89624 100644 --- a/tx-submitter/batch/batch_data.go +++ b/common/batch/batch_data.go @@ -4,15 +4,10 @@ import ( "encoding/binary" "fmt" - "morph-l2/node/zstd" - "morph-l2/tx-submitter/types" - "github.com/morph-l2/go-ethereum/common" "github.com/morph-l2/go-ethereum/crypto" -) -var ( - EmptyVersionedHash = common.HexToHash("0x010657f37554c781402a22917dee2f75def7ab966d7b770905398eba3c444014") + "morph-l2/common/blob" ) type BatchData struct { @@ -95,7 +90,7 @@ func (cks *BatchData) DataHashV2() (common.Hash, error) { lastBlockContext := cks.blockContexts[len(cks.blockContexts)-60:] // Parse block height - height, err := types.HeightFromBlockContextBytes(lastBlockContext) + height, err := HeightFromBlockContextBytes(lastBlockContext) if err != nil { return common.Hash{}, fmt.Errorf("failed to parse blockContext: context length=%d, lastBlockContext=%x, err=%w", len(cks.blockContexts), lastBlockContext, err) @@ -108,8 +103,8 @@ func (cks *BatchData) DataHashV2() (common.Hash, error) { func (cks *BatchData) calculateHash(height uint64) common.Hash { // Preallocate memory for efficiency hashData := make([]byte, 8+2+len(cks.l1TxHashes)) // 8 bytes for height, 2 bytes for l1TxNum - copy(hashData[:8], types.Uint64ToBigEndianBytes(height)) - copy(hashData[8:10], types.Uint16ToBigEndianBytes(cks.l1TxNum)) + copy(hashData[:8], Uint64ToBigEndianBytes(height)) + copy(hashData[8:10], Uint16ToBigEndianBytes(cks.l1TxNum)) copy(hashData[10:], cks.l1TxHashes) return crypto.Keccak256Hash(hashData) @@ -126,16 +121,17 @@ func (cks *BatchData) TxsPayloadV2() []byte { func (cks *BatchData) BlockNum() uint16 { return cks.blockNum } -func (cks *BatchData) EstimateCompressedSizeWithNewPayload(txPayload []byte) (bool, error) { +func (cks *BatchData) EstimateCompressedSizeWithNewPayload(txPayload []byte, maxBlobCount int) (bool, error) { + limit := maxBlobCount * blob.MaxBlobBytesSize blobBytes := append(cks.txsPayload, txPayload...) - if len(blobBytes) <= MaxBlobBytesSize { + if len(blobBytes) <= limit { return false, nil } - compressed, err := zstd.CompressBatchBytes(blobBytes) + compressed, err := blob.CompressBatchBytes(blobBytes) if err != nil { return false, err } - return len(compressed) > MaxBlobBytesSize, nil + return len(compressed) > limit, nil } func (cks *BatchData) combinePayloads(newBlockContext, newTxPayload []byte) []byte { @@ -150,15 +146,16 @@ func (cks *BatchData) combinePayloads(newBlockContext, newTxPayload []byte) []by // WillExceedCompressedSizeLimit checks if the size of the combined block contexts // and transaction payloads (after compression) exceeds the maximum allowed size. -func (cks *BatchData) WillExceedCompressedSizeLimit(newBlockContext, newTxPayload []byte) (bool, error) { +func (cks *BatchData) WillExceedCompressedSizeLimit(newBlockContext, newTxPayload []byte, maxBlobCount int) (bool, error) { + limit := maxBlobCount * blob.MaxBlobBytesSize // Combine the existing and new block contexts and transaction payloads combinedBytes := cks.combinePayloads(newBlockContext, newTxPayload) - if len(combinedBytes) <= MaxBlobBytesSize { + if len(combinedBytes) <= limit { return false, nil } - compressed, err := zstd.CompressBatchBytes(combinedBytes) + compressed, err := blob.CompressBatchBytes(combinedBytes) if err != nil { return false, fmt.Errorf("compression failed: %w", err) } - return len(compressed) > MaxBlobBytesSize, nil + return len(compressed) > limit, nil } diff --git a/tx-submitter/batch/batch_header.go b/common/batch/batch_header.go similarity index 80% rename from tx-submitter/batch/batch_header.go rename to common/batch/batch_header.go index 81d38c691..ab5956312 100644 --- a/tx-submitter/batch/batch_header.go +++ b/common/batch/batch_header.go @@ -16,9 +16,15 @@ type ( const ( expectedLengthV0 = 249 expectedLengthV1 = 257 + // V2 reuses the V1 wire format (257 bytes). The only semantic + // difference is that the 32-byte field at offset 57 stores + // keccak256(blobhash(0) || ... || blobhash(N-1)) instead of a + // single blob versioned hash. + expectedLengthV2 = 257 BatchHeaderVersion0 = 0 BatchHeaderVersion1 = 1 + BatchHeaderVersion2 = 2 ) var ( @@ -42,6 +48,10 @@ func (b BatchHeaderBytes) validate() error { if len(b) != expectedLengthV1 { return ErrInvalidBatchHeaderLength } + case BatchHeaderVersion2: + if len(b) != expectedLengthV2 { + return ErrInvalidBatchHeaderLength + } default: return ErrInvalidBatchHeaderVersion } @@ -94,10 +104,32 @@ func (b BatchHeaderBytes) DataHash() (common.Hash, error) { return common.BytesToHash(b[25:57]), nil } +// BlobVersionedHash returns the EIP-4844 blob versioned hash recorded at +// offset [57:89]. This is only meaningful for V0/V1 batches, where the field +// holds the single blob's versioned hash. For V2 batches the same offset +// holds an aggregated hash; callers must use BlobHashesHash instead. func (b BatchHeaderBytes) BlobVersionedHash() (common.Hash, error) { if err := b.validate(); err != nil { return common.Hash{}, err } + version, _ := b.Version() + if version >= BatchHeaderVersion2 { + return common.Hash{}, errors.New("BlobVersionedHash is not available for V2+; use BlobHashesHash") + } + return common.BytesToHash(b[57:89]), nil +} + +// BlobHashesHash returns the aggregated blob hash recorded at offset [57:89] +// for V2+ batches, defined as keccak256(blobhash(0) || ... || blobhash(N-1)). +// V0/V1 batches do not aggregate and will return an error. +func (b BatchHeaderBytes) BlobHashesHash() (common.Hash, error) { + if err := b.validate(); err != nil { + return common.Hash{}, err + } + version, _ := b.Version() + if version < BatchHeaderVersion2 { + return common.Hash{}, errors.New("BlobHashesHash is only available for V2+; use BlobVersionedHash") + } return common.BytesToHash(b[57:89]), nil } diff --git a/common/batch/batch_header_test.go b/common/batch/batch_header_test.go new file mode 100644 index 000000000..7c3ee046a --- /dev/null +++ b/common/batch/batch_header_test.go @@ -0,0 +1,88 @@ +package batch + +import ( + "math/big" + "testing" + + "github.com/morph-l2/go-ethereum/common" + "github.com/stretchr/testify/require" + + "morph-l2/common/blob" +) + +// TestBatchHeaderV2 exercises the V2 header variant: it reuses the V1 wire +// layout (257 bytes) but the 32-byte field at offset 57 carries an aggregated +// blob hash (keccak256(blobhash(0)||...||blobhash(N-1))) rather than a single +// versioned hash. Parsing helpers must route callers accordingly. +func TestBatchHeaderV2(t *testing.T) { + aggregated := common.BigToHash(big.NewInt(0xABCDEF)) + + // Start from a V1 encoding (identical byte layout), then flip the version + // byte to V2. This matches the on-chain behavior where a V2 header is + // produced by tx-submitter with the aggregated hash stored at offset 57. + raw := BatchHeaderV1{ + BatchHeaderV0: BatchHeaderV0{ + BatchIndex: 42, + L1MessagePopped: 1, + TotalL1MessagePopped: 3, + DataHash: common.BigToHash(big.NewInt(0x11)), + BlobVersionedHash: aggregated, + PrevStateRoot: common.BigToHash(big.NewInt(0x22)), + PostStateRoot: common.BigToHash(big.NewInt(0x33)), + WithdrawalRoot: common.BigToHash(big.NewInt(0x44)), + SequencerSetVerifyHash: common.BigToHash(big.NewInt(0x55)), + ParentBatchHash: common.BigToHash(big.NewInt(0x66)), + }, + LastBlockNumber: 9_876, + }.Bytes() + raw[0] = BatchHeaderVersion2 + + version, err := raw.Version() + require.NoError(t, err) + require.EqualValues(t, BatchHeaderVersion2, version) + + batchIndex, err := raw.BatchIndex() + require.NoError(t, err) + require.EqualValues(t, 42, batchIndex) + + lastBlockNumber, err := raw.LastBlockNumber() + require.NoError(t, err) + require.EqualValues(t, 9_876, lastBlockNumber) + + // V2 headers must route callers to BlobHashesHash; the legacy accessor + // intentionally errors to prevent silent mis-use. + _, err = raw.BlobVersionedHash() + require.Error(t, err) + + aggHash, err := raw.BlobHashesHash() + require.NoError(t, err) + require.EqualValues(t, aggregated, aggHash) + + // Length check: a V2 header with the wrong length must fail validate(). + short := make(BatchHeaderBytes, expectedLengthV2-1) + short[0] = BatchHeaderVersion2 + _, err = short.BatchIndex() + require.ErrorIs(t, err, ErrInvalidBatchHeaderLength) +} + +// TestBlobHashesHashUnavailableForLegacy guards the inverse direction: V0 and +// V1 headers must reject BlobHashesHash so that callers reach for the correct +// accessor. +func TestBlobHashesHashUnavailableForLegacy(t *testing.T) { + v0 := BatchHeaderV0{ + BatchIndex: 1, + BlobVersionedHash: blob.EmptyVersionedHash, + }.Bytes() + _, err := v0.BlobHashesHash() + require.Error(t, err) + + v1 := BatchHeaderV1{ + BatchHeaderV0: BatchHeaderV0{ + BatchIndex: 2, + BlobVersionedHash: blob.EmptyVersionedHash, + }, + LastBlockNumber: 10, + }.Bytes() + _, err = v1.BlobHashesHash() + require.Error(t, err) +} diff --git a/tx-submitter/batch/batch_query.go b/common/batch/batch_query.go similarity index 100% rename from tx-submitter/batch/batch_query.go rename to common/batch/batch_query.go diff --git a/tx-submitter/batch/batch_restart_test.go b/common/batch/batch_restart_test.go similarity index 94% rename from tx-submitter/batch/batch_restart_test.go rename to common/batch/batch_restart_test.go index f4db2f47f..87cdf1393 100644 --- a/tx-submitter/batch/batch_restart_test.go +++ b/common/batch/batch_restart_test.go @@ -7,14 +7,9 @@ import ( "errors" "fmt" "math/big" - "os" - "path/filepath" "testing" "morph-l2/bindings/bindings" - "morph-l2/tx-submitter/db" - "morph-l2/tx-submitter/iface" - "morph-l2/tx-submitter/types" "github.com/morph-l2/go-ethereum/accounts/abi/bind" "github.com/morph-l2/go-ethereum/common" @@ -37,7 +32,7 @@ var ( rollupContract *bindings.Rollup - l2Caller *types.L2Caller + l2Gov *L2Gov ) func init() { @@ -46,22 +41,16 @@ func init() { if err != nil { panic(err) } - l2Caller, err = types.NewL2Caller([]iface.L2Client{l2Client}) + l2Gov, err = NewL2Gov(l2Client) if err != nil { panic(err) } } func Test_GetFinalizeBatchHeader(t *testing.T) { - testDir := filepath.Join(t.TempDir(), "testleveldb") - os.RemoveAll(testDir) - t.Cleanup(func() { - os.RemoveAll(testDir) - }) - testDB, err := db.New(testDir) - require.NoError(t, err) + testDB := openTestKV(t) - bc := NewBatchCache(nil, l1Client, []iface.L2Client{l2Client}, rollupContract, l2Caller, testDB) + bc := NewBatchCache(nil, nil, 2, l1Client, &SingleL2Client{C: l2Client}, rollupContract, l2Gov, testDB) headerBytes, err := bc.getLastFinalizeBatchHeaderFromRollupByIndex(0) require.NoError(t, err) t.Log("headerBytes", hex.EncodeToString(headerBytes.Bytes())) @@ -82,20 +71,14 @@ func Test_CommitBatchParse(t *testing.T) { } func TestBatchRestartInit(t *testing.T) { - testDir := filepath.Join(t.TempDir(), "testleveldb") - os.RemoveAll(testDir) - t.Cleanup(func() { - os.RemoveAll(testDir) - }) - testDB, err := db.New(testDir) - require.NoError(t, err) + testDB := openTestKV(t) - sequencerSetBytes, sequencerSetVerifyHash, err := l2Caller.GetSequencerSetBytes(nil) + sequencerSetBytes, sequencerSetVerifyHash, err := l2Gov.GetSequencerSetBytes(nil) require.NoError(t, err) t.Log("sequencer set verify hash", hex.EncodeToString(sequencerSetVerifyHash[:])) ci, fi := getInfosFromContract() t.Log("commit index", ci, " ", "finalize index", fi) - bc := NewBatchCache(nil, l1Client, []iface.L2Client{l2Client}, rollupContract, l2Caller, testDB) + bc := NewBatchCache(nil, nil, 2, l1Client, &SingleL2Client{C: l2Client}, rollupContract, l2Gov, testDB) startBlockNum, endBlockNum, err := getFirstUnFinalizeBatchBlockNumRange(fi) require.NoError(t, err) startBlockNum = new(big.Int).Add(startBlockNum, new(big.Int).SetUint64(1)) @@ -127,7 +110,7 @@ func TestBatchRestartInit(t *testing.T) { t.Logf("First unfinalize batch index: %d, block range: %d - %d", firstUnfinalizedIndex, startBlockNum.Uint64(), endBlockNum.Uint64()) // Fetch blocks from L2 client in this range and assemble batchHeader - assembledBatchHeader, err := assembleBatchHeaderFromL2Blocks(bc, startBlockNum.Uint64(), endBlockNum.Uint64(), sequencerSetBytes, l2Client, l2Caller) + assembledBatchHeader, err := assembleBatchHeaderFromL2Blocks(bc, startBlockNum.Uint64(), endBlockNum.Uint64(), sequencerSetBytes, l2Client, l2Gov) require.NoError(t, err, "failed to assemble batch header from L2 blocks") t.Log("assembled batch header success", hex.EncodeToString(assembledBatchHeader.Bytes())) // Verify the assembled batchHeader @@ -458,14 +441,14 @@ func assembleBatchHeaderFromL2Blocks( bc *BatchCache, startBlockNum, endBlockNum uint64, sequencerBytes []byte, - l2Client iface.L2Client, - l2Caller *types.L2Caller, + l2Client *ethclient.Client, + l2Gov L2GovCaller, ) (*BatchHeaderBytes, error) { ctx := context.Background() // Fetch blocks from L2 client in the specified range and accumulate to batch for blockNum := startBlockNum; blockNum <= endBlockNum; blockNum++ { - root, err := l2Caller.GetTreeRoot(&bind.CallOpts{ + root, err := l2Gov.GetTreeRoot(&bind.CallOpts{ Context: ctx, BlockNumber: new(big.Int).SetUint64(blockNum), }) @@ -495,7 +478,7 @@ func assembleBatchHeaderFromL2Blocks( blockTimestamp := lastBlock.Time() // Seal batch and generate batchHeader - batchIndex, batchHeaderBytes, _, err := bc.SealBatch(sequencerBytes, blockTimestamp) + batchIndex, batchHeaderBytes, _, err := bc.SealBatch(sequencerBytes, blockTimestamp, nil) if err != nil { return nil, fmt.Errorf("failed to seal batch: %w", err) } diff --git a/tx-submitter/batch/batch_storage.go b/common/batch/batch_storage.go similarity index 96% rename from tx-submitter/batch/batch_storage.go rename to common/batch/batch_storage.go index 1f7c1e0f7..a681bbee8 100644 --- a/tx-submitter/batch/batch_storage.go +++ b/common/batch/batch_storage.go @@ -8,10 +8,9 @@ import ( "fmt" "sync" - "morph-l2/tx-submitter/db" - "github.com/morph-l2/go-ethereum/eth" "github.com/morph-l2/go-ethereum/log" + ldberrors "github.com/syndtr/goleveldb/leveldb/errors" ) const ( @@ -23,12 +22,12 @@ const ( // BatchStorage handles persistence of sealed batches using JSON encoding type BatchStorage struct { - db db.Database + db SealedBatchKV mu sync.RWMutex } // NewBatchStorage creates a new BatchStorage instance -func NewBatchStorage(db db.Database) *BatchStorage { +func NewBatchStorage(db SealedBatchKV) *BatchStorage { return &BatchStorage{ db: db, } @@ -69,7 +68,7 @@ func (s *BatchStorage) LoadSealedBatch(batchIndex uint64) (*eth.RPCRollupBatch, key := encodeBatchKey(batchIndex) encoded, err := s.db.GetBytes(key) if err != nil { - if errors.Is(err, db.ErrKeyNotFound) { + if isKVNotFound(err) { return nil, fmt.Errorf("sealed batch %d not found", batchIndex) } return nil, fmt.Errorf("failed to get sealed batch %d: %w", batchIndex, err) @@ -92,7 +91,7 @@ func (s *BatchStorage) LoadAllSealedBatches() (map[uint64]*eth.RPCRollupBatch, [ indices, err := s.loadBatchIndices() s.mu.RUnlock() if err != nil { - if errors.Is(err, db.ErrKeyNotFound) { + if isKVNotFound(err) { // No batches stored yet return make(map[uint64]*eth.RPCRollupBatch), nil, nil } @@ -121,7 +120,7 @@ func (s *BatchStorage) LoadAllSealedBatchesAndHeader() (map[uint64]*eth.RPCRollu indices, err := s.loadBatchIndices() s.mu.RUnlock() if err != nil { - if errors.Is(err, db.ErrKeyNotFound) { + if isKVNotFound(err) { // No batches stored yet return make(map[uint64]*eth.RPCRollupBatch), make(map[uint64]*BatchHeaderBytes), nil, nil } @@ -206,7 +205,7 @@ func (s *BatchStorage) DeleteAllSealedBatches() error { indices, err := s.loadBatchIndices() s.mu.RUnlock() if err != nil { - if errors.Is(err, db.ErrKeyNotFound) { + if isKVNotFound(err) { // No batches stored yet return nil } @@ -244,7 +243,7 @@ func encodeBatchKey(batchIndex uint64) []byte { func (s *BatchStorage) updateBatchIndices(batchIndex uint64, add bool) error { indices, err := s.loadBatchIndices() if err != nil { - if errors.Is(err, db.ErrKeyNotFound) { + if isKVNotFound(err) { indices = []uint64{} } else { return err @@ -324,7 +323,7 @@ func (s *BatchStorage) LoadSealedBatchHeader(batchIndex uint64) (*BatchHeaderByt key := encodeBatchHeaderKey(batchIndex) headerBytes, err := s.db.GetBytes(key) if err != nil { - if errors.Is(err, db.ErrKeyNotFound) { + if isKVNotFound(err) { return nil, fmt.Errorf("sealed batch header %d not found", batchIndex) } return nil, fmt.Errorf("failed to get sealed batch header %d: %w", batchIndex, err) @@ -342,7 +341,7 @@ func (s *BatchStorage) LoadAllSealedBatchHeaders() (map[uint64]*BatchHeaderBytes indices, err := s.loadBatchIndices() s.mu.RUnlock() if err != nil { - if errors.Is(err, db.ErrKeyNotFound) { + if isKVNotFound(err) { // No batches stored yet return make(map[uint64]*BatchHeaderBytes), nil } @@ -384,3 +383,7 @@ func encodeBatchHeaderKey(batchIndex uint64) []byte { binary.BigEndian.PutUint64(key[len(SealedBatchHeaderKeyPrefix):], batchIndex) return key } + +func isKVNotFound(err error) bool { + return errors.Is(err, ErrKeyNotFound) || errors.Is(err, ldberrors.ErrNotFound) +} diff --git a/node/types/blob.go b/common/batch/blob.go similarity index 63% rename from node/types/blob.go rename to common/batch/blob.go index 49b158bc1..088e55a6f 100644 --- a/node/types/blob.go +++ b/common/batch/blob.go @@ -1,4 +1,4 @@ -package types +package batch import ( "bytes" @@ -6,6 +6,8 @@ import ( "fmt" "io" + "morph-l2/node/zstd" + eth "github.com/morph-l2/go-ethereum/core/types" "github.com/morph-l2/go-ethereum/crypto/kzg4844" "github.com/morph-l2/go-ethereum/rlp" @@ -13,6 +15,32 @@ import ( const MaxBlobBytesSize = 4096 * 31 +var ( + emptyBlob = new(kzg4844.Blob) + emptyBlobCommit, _ = kzg4844.BlobToCommitment(emptyBlob) + emptyBlobProof, _ = kzg4844.ComputeBlobProof(emptyBlob, emptyBlobCommit) +) + +// MakeBlobCanonical converts the raw blob data into the canonical blob representation of 4096 BLSFieldElements. +func MakeBlobCanonical(blobBytes []byte) (b *kzg4844.Blob, err error) { + if len(blobBytes) > MaxBlobBytesSize { + return nil, fmt.Errorf("data is too large for blob. len=%v", len(blobBytes)) + } + offset := 0 + b = new(kzg4844.Blob) + // encode (up to) 31 bytes of remaining input data at a time into the subsequent field element + for i := 0; i < 4096; i++ { + offset += copy(b[i*32+1:i*32+32], blobBytes[offset:]) + if offset == len(blobBytes) { + break + } + } + if offset < len(blobBytes) { + return nil, fmt.Errorf("failed to fit all data into blob. bytes remaining: %v", len(blobBytes)-offset) + } + return +} + func RetrieveBlobBytes(blob *kzg4844.Blob) ([]byte, error) { data := make([]byte, MaxBlobBytesSize) for i := 0; i < 4096; i++ { @@ -24,6 +52,67 @@ func RetrieveBlobBytes(blob *kzg4844.Blob) ([]byte, error) { return data, nil } +func makeBlobCommitment(bz []byte) (b kzg4844.Blob, c kzg4844.Commitment, err error) { + blob, err := MakeBlobCanonical(bz) + if err != nil { + return + } + b = *blob + c, err = kzg4844.BlobToCommitment(&b) + if err != nil { + return + } + return +} + +func MakeBlobTxSidecar(blobBytes []byte, maxBlobs int) (*eth.BlobTxSidecar, error) { + if len(blobBytes) == 0 { + return ð.BlobTxSidecar{ + Blobs: []kzg4844.Blob{*emptyBlob}, + Commitments: []kzg4844.Commitment{emptyBlobCommit}, + Proofs: []kzg4844.Proof{emptyBlobProof}, + }, nil + } + if maxBlobs <= 0 { + maxBlobs = 1 + } + if len(blobBytes) > maxBlobs*MaxBlobBytesSize { + return nil, fmt.Errorf("data size %d exceeds %d blobs capacity (%d bytes)", len(blobBytes), maxBlobs, maxBlobs*MaxBlobBytesSize) + } + blobCount := (len(blobBytes) + MaxBlobBytesSize - 1) / MaxBlobBytesSize + var ( + err error + blobs = make([]kzg4844.Blob, blobCount) + commitments = make([]kzg4844.Commitment, blobCount) + ) + for i := 0; i < blobCount; i++ { + start := i * MaxBlobBytesSize + end := start + MaxBlobBytesSize + if end > len(blobBytes) { + end = len(blobBytes) + } + blobs[i], commitments[i], err = makeBlobCommitment(blobBytes[start:end]) + if err != nil { + return nil, err + } + } + return ð.BlobTxSidecar{ + Blobs: blobs, + Commitments: commitments, + }, nil +} + +func CompressBatchBytes(batchBytes []byte) ([]byte, error) { + if len(batchBytes) == 0 { + return nil, nil + } + compressedBatchBytes, err := zstd.CompressBatchBytes(batchBytes) + if err != nil { + return nil, fmt.Errorf("failed to compress batch bytes, err: %w", err) + } + return compressedBatchBytes, nil +} + func DecodeTxsFromBytes(txsBytes []byte) (eth.Transactions, error) { reader := bytes.NewReader(txsBytes) txs := make(eth.Transactions, 0) diff --git a/tx-submitter/batch/commit_test.go b/common/batch/commit_test.go similarity index 81% rename from tx-submitter/batch/commit_test.go rename to common/batch/commit_test.go index 2acb26d37..28efe8a94 100644 --- a/tx-submitter/batch/commit_test.go +++ b/common/batch/commit_test.go @@ -3,18 +3,14 @@ package batch import ( "context" "crypto/ecdsa" + "errors" "fmt" "math/big" - "os" - "path/filepath" "testing" "time" "morph-l2/bindings/bindings" - "morph-l2/tx-submitter/db" - "morph-l2/tx-submitter/iface" - "morph-l2/tx-submitter/types" - "morph-l2/tx-submitter/utils" + "morph-l2/common/blob" "github.com/holiman/uint256" "github.com/morph-l2/go-ethereum/common" @@ -27,19 +23,16 @@ import ( "github.com/stretchr/testify/require" ) -var pk = "" +var ( + pk = "" + errExceedFeeLimit = errors.New("exceed fee limit") +) func TestRollupWithProof(t *testing.T) { - testDir := filepath.Join(t.TempDir(), "testleveldb") - os.RemoveAll(testDir) - t.Cleanup(func() { - os.RemoveAll(testDir) - }) - testDB, err := db.New(testDir) - require.NoError(t, err) + testDB := openTestKV(t) - cache := NewBatchCache(nil, l1Client, []iface.L2Client{l2Client}, rollupContract, l2Caller, testDB) - err = cache.InitFromRollupByRange() + cache := NewBatchCache(nil, nil, 2, l1Client, &SingleL2Client{C: l2Client}, rollupContract, l2Gov, testDB) + err := cache.InitFromRollupByRange() require.NoError(t, err) privateKey, err := crypto.HexToECDSA(pk[2:]) @@ -90,7 +83,6 @@ func TestRollupWithProof(t *testing.T) { require.NoError(t, err) t.Log("receipt status", receipt.Status) t.Log("receipt", receipt) - } func sign(tx *ethtypes.Transaction, signer ethtypes.Signer, prv *ecdsa.PrivateKey) (*ethtypes.Transaction, error) { @@ -102,7 +94,7 @@ func sign(tx *ethtypes.Transaction, signer ethtypes.Signer, prv *ecdsa.PrivateKe } func createBlobTx(l1client *ethclient.Client, batch *eth.RPCRollupBatch, nonce, gas uint64, tip, gasFeeCap, blobFee *big.Int, calldata []byte, head *ethtypes.Header) (*ethtypes.Transaction, error) { - versionedHashes := types.BlobHashes(batch.Sidecar.Blobs, batch.Sidecar.Commitments) + versionedHashes := blob.BlobHashes(batch.Sidecar.Blobs, batch.Sidecar.Commitments) sidecar := ðtypes.BlobTxSidecar{ Blobs: batch.Sidecar.Blobs, Commitments: batch.Sidecar.Commitments, @@ -111,17 +103,17 @@ func createBlobTx(l1client *ethclient.Client, batch *eth.RPCRollupBatch, nonce, if err != nil { return nil, err } - switch types.DetermineBlobVersion(head, chainID.Uint64()) { + switch blob.DetermineBlobVersion(head, chainID.Uint64()) { case ethtypes.BlobSidecarVersion0: sidecar.Version = ethtypes.BlobSidecarVersion0 - proof, err := types.MakeBlobProof(sidecar.Blobs, sidecar.Commitments) + proof, err := blob.MakeBlobProof(sidecar.Blobs, sidecar.Commitments) if err != nil { return nil, fmt.Errorf("gen blob proof failed %v", err) } sidecar.Proofs = proof case ethtypes.BlobSidecarVersion1: sidecar.Version = ethtypes.BlobSidecarVersion1 - proof, err := types.MakeCellProof(sidecar.Blobs) + proof, err := blob.MakeCellProof(sidecar.Blobs) if err != nil { return nil, fmt.Errorf("gen cell proof failed %v", err) } @@ -172,7 +164,6 @@ func getGasTipAndCap(l1client *ethclient.Client) (*big.Int, *big.Int, *big.Int, gasFeeCap = new(big.Int).Set(tip) } - // calc blob fee cap var blobFee *big.Int if head.ExcessBlobGas != nil { id, err := l1client.ChainID(context.Background()) @@ -180,13 +171,12 @@ func getGasTipAndCap(l1client *ethclient.Client) (*big.Int, *big.Int, *big.Int, return nil, nil, nil, nil, err } log.Info("market blob fee info", "excess blob gas", *head.ExcessBlobGas) - blobConfig, exist := types.ChainConfigMap[id.Uint64()] + blobConfig, exist := blob.ChainConfigMap[id.Uint64()] if !exist { - blobConfig = types.DefaultBlobConfig + blobConfig = blob.DefaultBlobConfig } - blobFeeDenominator := types.GetBlobFeeDenominator(blobConfig, head.Time) + blobFeeDenominator := blob.GetBlobFeeDenominator(blobConfig, head.Time) blobFee = eip4844.CalcBlobFee(*head.ExcessBlobGas, blobFeeDenominator.Uint64()) - // Set to 3x to handle blob market congestion blobFee = new(big.Int).Mul(blobFee, big.NewInt(3)) } @@ -202,26 +192,22 @@ func buildSigInput(batch *eth.RPCRollupBatch) (*bindings.IRollupBatchSignatureIn return sigData, nil } -// send tx to l1 with business logic check -func sendTx(client iface.Client, txFeeLimit uint64, tx *ethtypes.Transaction) error { - // fee limit +func sendTx(client *ethclient.Client, txFeeLimit uint64, tx *ethtypes.Transaction) error { if txFeeLimit > 0 { var fee uint64 - // calc tx gas fee if tx.Type() == ethtypes.BlobTxType { blobFee := new(big.Int).Mul(tx.BlobGasFeeCap(), new(big.Int).SetUint64(tx.BlobGas())) txFee := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) totalFee := new(big.Int).Add(blobFee, txFee) if !totalFee.IsUint64() || totalFee.Uint64() > txFeeLimit { - return fmt.Errorf("%v:limit=%v,but got=%v", utils.ErrExceedFeeLimit, txFeeLimit, totalFee) + return fmt.Errorf("%v:limit=%v,but got=%v", errExceedFeeLimit, txFeeLimit, totalFee) } return client.SendTransaction(context.Background(), tx) - } else { - fee = tx.GasPrice().Uint64() * tx.Gas() } + fee = tx.GasPrice().Uint64() * tx.Gas() if fee > txFeeLimit { - return fmt.Errorf("%v:limit=%v,but got=%v", utils.ErrExceedFeeLimit, txFeeLimit, fee) + return fmt.Errorf("%v:limit=%v,but got=%v", errExceedFeeLimit, txFeeLimit, fee) } } diff --git a/common/batch/encoding.go b/common/batch/encoding.go new file mode 100644 index 000000000..a2ba1fc43 --- /dev/null +++ b/common/batch/encoding.go @@ -0,0 +1,25 @@ +package batch + +import ( + "encoding/binary" + "fmt" +) + +func Uint64ToBigEndianBytes(value uint64) []byte { + valueBytes := make([]byte, 8) + binary.BigEndian.PutUint64(valueBytes, value) + return valueBytes +} + +func Uint16ToBigEndianBytes(value uint16) []byte { + valueBytes := make([]byte, 2) + binary.BigEndian.PutUint16(valueBytes, value) + return valueBytes +} + +func HeightFromBlockContextBytes(blockContextBytes []byte) (uint64, error) { + if len(blockContextBytes) != 60 { + return 0, fmt.Errorf("wrong block context bytes length, input: %x", blockContextBytes) + } + return binary.BigEndian.Uint64(blockContextBytes[:8]), nil +} diff --git a/common/batch/helpers_test.go b/common/batch/helpers_test.go new file mode 100644 index 000000000..7a493dba3 --- /dev/null +++ b/common/batch/helpers_test.go @@ -0,0 +1,57 @@ +package batch + +import ( + "context" + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/syndtr/goleveldb/leveldb" + ldberrors "github.com/syndtr/goleveldb/leveldb/errors" +) + +type testLevelDB struct { + db *leveldb.DB +} + +func openTestKV(t *testing.T) SealedBatchKV { + t.Helper() + dir := filepath.Join(t.TempDir(), "ldb") + _ = os.RemoveAll(dir) + db, err := leveldb.OpenFile(dir, nil) + require.NoError(t, err) + t.Cleanup(func() { _ = db.Close() }) + return &testLevelDB{db: db} +} + +func (d *testLevelDB) GetBytes(key []byte) ([]byte, error) { + v, err := d.db.Get(key, nil) + if err == ldberrors.ErrNotFound { + return nil, fmt.Errorf("%w", ErrKeyNotFound) + } + return v, err +} + +func (d *testLevelDB) PutBytes(key, val []byte) error { + return d.db.Put(key, val, nil) +} + +func (d *testLevelDB) Delete(key []byte) error { + return d.db.Delete(key, nil) +} + +func testLoop(ctx context.Context, d time.Duration, fn func()) { + ticker := time.NewTicker(d) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + fn() + } + } +} diff --git a/common/batch/interfaces.go b/common/batch/interfaces.go new file mode 100644 index 000000000..3051383b1 --- /dev/null +++ b/common/batch/interfaces.go @@ -0,0 +1,76 @@ +package batch + +import ( + "context" + "errors" + "math/big" + + "morph-l2/bindings/bindings" + + "github.com/morph-l2/go-ethereum/accounts/abi/bind" + "github.com/morph-l2/go-ethereum/common" + ethtypes "github.com/morph-l2/go-ethereum/core/types" +) + +// ErrKeyNotFound is returned by SealedBatchKV implementations when a key is absent. +var ErrKeyNotFound = errors.New("batch storage: key not found") + +// SealedBatchKV is a minimal key-value store used by BatchStorage. +type SealedBatchKV interface { + GetBytes(key []byte) ([]byte, error) + PutBytes(key, val []byte) error + Delete(key []byte) error +} + +// L1HeaderClient is the L1 RPC surface required to recover batch headers from events. +type L1HeaderClient interface { + BlockNumber(ctx context.Context) (uint64, error) + TransactionByHash(ctx context.Context, hash common.Hash) (*ethtypes.Transaction, bool, error) +} + +// L2MultiClient fans out read calls across multiple L2 endpoints (same role as tx-submitter iface.L2Clients). +type L2MultiClient interface { + BlockNumber(ctx context.Context) (uint64, error) + BlockByNumber(ctx context.Context, number *big.Int) (*ethtypes.Block, error) + Len() int +} + +// SingleL2Client adapts a single L2 RPC backend as L2MultiClient (Len is always 1). +type SingleL2Client struct { + C interface { + BlockNumber(ctx context.Context) (uint64, error) + BlockByNumber(ctx context.Context, number *big.Int) (*ethtypes.Block, error) + } +} + +func (s *SingleL2Client) BlockNumber(ctx context.Context) (uint64, error) { + return s.C.BlockNumber(ctx) +} + +func (s *SingleL2Client) BlockByNumber(ctx context.Context, number *big.Int) (*ethtypes.Block, error) { + return s.C.BlockByNumber(ctx, number) +} + +func (s *SingleL2Client) Len() int { return 1 } + +// RollupBatchReader is the rollup contract view BatchCache needs (subset of generated Rollup bindings). +type RollupBatchReader interface { + CommittedBatches(opts *bind.CallOpts, batchIndex *big.Int) ([32]byte, error) + LastCommittedBatchIndex(opts *bind.CallOpts) (*big.Int, error) + LastFinalizedBatchIndex(opts *bind.CallOpts) (*big.Int, error) + BatchDataStore(opts *bind.CallOpts, batchIndex *big.Int) (struct { + OriginTimestamp *big.Int + FinalizeTimestamp *big.Int + BlockNumber *big.Int + SignedSequencersBitmap *big.Int + }, error) + FilterFinalizeBatch(opts *bind.FilterOpts, batchIndex []*big.Int, batchHash [][32]byte) (*bindings.RollupFinalizeBatchIterator, error) +} + +// L2GovCaller reads batch-related Gov / bridge / sequencer data on L2. +type L2GovCaller interface { + BatchBlockInterval(opts *bind.CallOpts) (*big.Int, error) + BatchTimeout(opts *bind.CallOpts) (*big.Int, error) + GetTreeRoot(opts *bind.CallOpts) ([32]byte, error) + GetSequencerSetBytes(opts *bind.CallOpts) ([]byte, common.Hash, error) +} diff --git a/common/batch/l2_gov.go b/common/batch/l2_gov.go new file mode 100644 index 000000000..acfe0c0d0 --- /dev/null +++ b/common/batch/l2_gov.go @@ -0,0 +1,82 @@ +package batch + +import ( + "bytes" + "fmt" + "math/big" + + "morph-l2/bindings/bindings" + "morph-l2/bindings/predeploys" + + "github.com/morph-l2/go-ethereum/accounts/abi/bind" + "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/common/hexutil" + "github.com/morph-l2/go-ethereum/crypto" +) + +// L2Gov bundles read-only L2 contracts used when assembling rollup batches. +type L2Gov struct { + sequencerContract *bindings.SequencerCaller + l2MessagePasserContract *bindings.L2ToL1MessagePasserCaller + govContract *bindings.GovCaller +} + +// NewL2Gov builds an L2Gov using any ContractCaller (e.g. *ethclient.Client or a multi-client backend). +func NewL2Gov(backend bind.ContractCaller) (*L2Gov, error) { + if backend == nil { + return nil, fmt.Errorf("nil contract backend") + } + sequencerContract, err := bindings.NewSequencerCaller(predeploys.SequencerAddr, backend) + if err != nil { + return nil, err + } + l2MessagePasserContract, err := bindings.NewL2ToL1MessagePasserCaller(predeploys.L2ToL1MessagePasserAddr, backend) + if err != nil { + return nil, err + } + govContract, err := bindings.NewGovCaller(predeploys.GovAddr, backend) + if err != nil { + return nil, err + } + return &L2Gov{ + sequencerContract: sequencerContract, + l2MessagePasserContract: l2MessagePasserContract, + govContract: govContract, + }, nil +} + +// SequencerSetVerifyHash gets the sequencer set verify hash from the Sequencer contract. +func (c *L2Gov) SequencerSetVerifyHash(opts *bind.CallOpts) ([32]byte, error) { + return c.sequencerContract.SequencerSetVerifyHash(opts) +} + +// GetTreeRoot gets the tree root from the L2ToL1MessagePasser contract. +func (c *L2Gov) GetTreeRoot(opts *bind.CallOpts) ([32]byte, error) { + return c.l2MessagePasserContract.GetTreeRoot(opts) +} + +// BatchBlockInterval gets the batch block interval from the Gov contract. +func (c *L2Gov) BatchBlockInterval(opts *bind.CallOpts) (*big.Int, error) { + return c.govContract.BatchBlockInterval(opts) +} + +// BatchTimeout gets the batch timeout from the Gov contract. +func (c *L2Gov) BatchTimeout(opts *bind.CallOpts) (*big.Int, error) { + return c.govContract.BatchTimeout(opts) +} + +// GetSequencerSetBytes returns sequencer set bytes after hash consistency check. +func (c *L2Gov) GetSequencerSetBytes(opts *bind.CallOpts) ([]byte, common.Hash, error) { + hash, err := c.sequencerContract.SequencerSetVerifyHash(opts) + if err != nil { + return nil, common.Hash{}, err + } + setBytes, err := c.sequencerContract.GetSequencerSetBytes(opts) + if err != nil { + return nil, common.Hash{}, err + } + if bytes.Equal(hash[:], crypto.Keccak256Hash(setBytes).Bytes()) { + return setBytes, hash, nil + } + return nil, common.Hash{}, fmt.Errorf("sequencer set hash verify failed %v: %v", hexutil.Encode(setBytes), common.BytesToHash(hash[:]).String()) +} diff --git a/tx-submitter/types/blob_config.go b/common/blob/fee.go similarity index 51% rename from tx-submitter/types/blob_config.go rename to common/blob/fee.go index 70dc371a3..cb0497b98 100644 --- a/tx-submitter/types/blob_config.go +++ b/common/blob/fee.go @@ -1,8 +1,12 @@ -package types +package blob import ( + "crypto/sha256" "math/big" + "github.com/morph-l2/go-ethereum/common" + ethtypes "github.com/morph-l2/go-ethereum/core/types" + "github.com/morph-l2/go-ethereum/crypto/kzg4844" "github.com/morph-l2/go-ethereum/log" ) @@ -42,7 +46,6 @@ type BlobConfig struct { UpdateFraction uint64 } -// Time determination methods (referencing go-ethereum logic). // IsCancun returns whether time is either equal to the Cancun fork time or greater. func (c *BlobFeeConfig) IsCancun(num *big.Int, time uint64) bool { return c.IsLondon(num) && isTimestampForked(c.CancunTime, time) @@ -91,19 +94,16 @@ func (c *BlobFeeConfig) IsLondon(num *big.Int) bool { // GetBlobFeeDenominator returns the corresponding UpdateFraction based on the time. func GetBlobFeeDenominator(blobFeeConfig *BlobFeeConfig, blockTime uint64) *big.Int { if blobFeeConfig == nil { - // If not configured, use default value. log.Warn("BlobFeeConfig not set, using default denominator", "default", DefaultOsakaBlobConfig) return new(big.Int).SetUint64(DefaultOsakaBlobConfig.UpdateFraction) } cfg := blobFeeConfig - londonBlock := cfg.LondonBlock // London block number for fork determination. + londonBlock := cfg.LondonBlock - // Check in priority order from high to low (BPO5 -> BPO4 -> ... -> Cancun). var blobConfig *BlobConfig - // Check BPO5 if cfg.BPO5Time != nil && cfg.IsBPO5(londonBlock, blockTime) && cfg.BPO5 != nil { blobConfig = cfg.BPO5 } else if cfg.BPO4Time != nil && cfg.IsBPO4(londonBlock, blockTime) && cfg.BPO4 != nil { @@ -135,9 +135,6 @@ func GetBlobFeeDenominator(blobFeeConfig *BlobFeeConfig, blockTime uint64) *big. return new(big.Int).SetUint64(blobConfig.UpdateFraction) } -// isBlockForked returns whether a fork scheduled at block s is active at the -// given head block. Whilst this method is the same as isTimestampForked, they -// are explicitly separate for clearer reading. func isBlockForked(s, head *big.Int) bool { if s == nil || head == nil { return false @@ -145,12 +142,165 @@ func isBlockForked(s, head *big.Int) bool { return s.Cmp(head) <= 0 } -// isTimestampForked returns whether a fork scheduled at timestamp s is active -// at the given head timestamp. Whilst this method is the same as isBlockForked, -// they are explicitly separate for clearer reading. func isTimestampForked(s *uint64, head uint64) bool { if s == nil { return false } return *s <= head } + +func newUint64(val uint64) *uint64 { return &val } + +var ( + DefaultCancunBlobConfig = &BlobConfig{ + UpdateFraction: 3338477, + } + DefaultPragueBlobConfig = &BlobConfig{ + UpdateFraction: 5007716, + } + DefaultOsakaBlobConfig = &BlobConfig{ + UpdateFraction: 5007716, + } + DefaultBPO1BlobConfig = &BlobConfig{ + UpdateFraction: 8346193, + } + DefaultBPO2BlobConfig = &BlobConfig{ + UpdateFraction: 11684671, + } + DefaultBPO3BlobConfig = &BlobConfig{ + UpdateFraction: 20609697, + } + DefaultBPO4BlobConfig = &BlobConfig{ + UpdateFraction: 13739630, + } +) + +var ( + MainnetChainConfig = &BlobFeeConfig{ + ChainID: big.NewInt(1), + LondonBlock: big.NewInt(12_965_000), + CancunTime: newUint64(1710338135), + PragueTime: newUint64(1746612311), + OsakaTime: newUint64(1764798551), + BPO1Time: newUint64(1765290071), + BPO2Time: newUint64(1767747671), + Cancun: DefaultCancunBlobConfig, + Prague: DefaultPragueBlobConfig, + Osaka: DefaultOsakaBlobConfig, + BPO1: DefaultBPO1BlobConfig, + BPO2: DefaultBPO2BlobConfig, + Default: DefaultOsakaBlobConfig, + } + + HoodiChainConfig = &BlobFeeConfig{ + ChainID: big.NewInt(560048), + LondonBlock: big.NewInt(0), + CancunTime: newUint64(0), + PragueTime: newUint64(1742999832), + OsakaTime: newUint64(1761677592), + BPO1Time: newUint64(1762365720), + BPO2Time: newUint64(1762955544), + Cancun: DefaultCancunBlobConfig, + Prague: DefaultPragueBlobConfig, + Osaka: DefaultOsakaBlobConfig, + BPO1: DefaultBPO1BlobConfig, + BPO2: DefaultBPO2BlobConfig, + Default: DefaultOsakaBlobConfig, + } + + DevnetChainConfig = &BlobFeeConfig{ + ChainID: big.NewInt(900), + LondonBlock: big.NewInt(0), + CancunTime: newUint64(0), + PragueTime: newUint64(1742999832), + OsakaTime: newUint64(1761677592), + BPO1Time: newUint64(1762365720), + BPO2Time: newUint64(1762955544), + Cancun: DefaultCancunBlobConfig, + Prague: DefaultPragueBlobConfig, + Osaka: DefaultOsakaBlobConfig, + BPO1: DefaultBPO1BlobConfig, + BPO2: DefaultBPO2BlobConfig, + Default: DefaultOsakaBlobConfig, + } +) + +// ChainBlobConfigs maps chain ID to blob fee configuration. +type ChainBlobConfigs map[uint64]*BlobFeeConfig + +var ( + DefaultBlobConfig = HoodiChainConfig + + ChainConfigMap = ChainBlobConfigs{ + 1: MainnetChainConfig, + 560048: HoodiChainConfig, + 900: DevnetChainConfig, + } +) + +// BlobHashes computes the blob hashes of the given blobs. +func BlobHashes(blobs []kzg4844.Blob, commitments []kzg4844.Commitment) []common.Hash { + hasher := sha256.New() + h := make([]common.Hash, len(commitments)) + for i := range blobs { + h[i] = kzg4844.CalcBlobHashV1(hasher, &commitments[i]) + } + return h +} + +// MakeBlobProof builds KZG proofs for blob transactions (sidecar v0). +func MakeBlobProof(blobs []kzg4844.Blob, commitment []kzg4844.Commitment) ([]kzg4844.Proof, error) { + proofs := make([]kzg4844.Proof, len(blobs)) + for i := range blobs { + proof, err := kzg4844.ComputeBlobProof(&blobs[i], commitment[i]) + if err != nil { + return nil, err + } + proofs[i] = proof + } + return proofs, nil +} + +// MakeCellProof builds cell proofs for blob sidecar v1. +func MakeCellProof(blobs []kzg4844.Blob) ([]kzg4844.Proof, error) { + proofs := make([]kzg4844.Proof, 0, len(blobs)*kzg4844.CellProofsPerBlob) + for _, blob := range blobs { + cellProofs, err := kzg4844.ComputeCellProofs(&blob) + if err != nil { + return nil, err + } + proofs = append(proofs, cellProofs...) + } + return proofs, nil +} + +// DetermineBlobVersion selects blob sidecar version from header time and chain config. +func DetermineBlobVersion(head *ethtypes.Header, chainID uint64) byte { + if head == nil { + return ethtypes.BlobSidecarVersion0 + } + blobConfig, exist := ChainConfigMap[chainID] + if !exist { + blobConfig = DefaultBlobConfig + } + if blobConfig.OsakaTime != nil && blobConfig.IsOsaka(head.Number, head.Time) { + return ethtypes.BlobSidecarVersion1 + } + return ethtypes.BlobSidecarVersion0 +} + +// BlobSidecarVersionToV1 converts the BlobSidecar to version 1, attaching the cell proofs. +func BlobSidecarVersionToV1(sc *ethtypes.BlobTxSidecar) error { + if sc.Version == ethtypes.BlobSidecarVersion1 { + return nil + } + if sc.Version == ethtypes.BlobSidecarVersion0 { + proofs, err := MakeCellProof(sc.Blobs) + if err != nil { + return err + } + sc.Version = ethtypes.BlobSidecarVersion1 + sc.Proofs = proofs + } + return nil +} diff --git a/common/blob/payload.go b/common/blob/payload.go new file mode 100644 index 000000000..03a0d4746 --- /dev/null +++ b/common/blob/payload.go @@ -0,0 +1,113 @@ +package blob + +import ( + "fmt" + + "morph-l2/node/zstd" + + "github.com/morph-l2/go-ethereum/common" + eth "github.com/morph-l2/go-ethereum/core/types" + "github.com/morph-l2/go-ethereum/crypto/kzg4844" +) + +const MaxBlobBytesSize = 4096 * 31 + +var ( + emptyBlob = new(kzg4844.Blob) + emptyBlobCommit, _ = kzg4844.BlobToCommitment(emptyBlob) + emptyBlobProof, _ = kzg4844.ComputeBlobProof(emptyBlob, emptyBlobCommit) +) + +// EmptyVersionedHash is the versioned hash of the canonical empty blob (all-zero payload). +var EmptyVersionedHash = common.HexToHash("0x010657f37554c781402a22917dee2f75def7ab966d7b770905398eba3c444014") + +// MakeBlobCanonical converts the raw blob data into the canonical blob representation of 4096 BLSFieldElements. +func MakeBlobCanonical(blobBytes []byte) (b *kzg4844.Blob, err error) { + if len(blobBytes) > MaxBlobBytesSize { + return nil, fmt.Errorf("data is too large for blob. len=%v", len(blobBytes)) + } + offset := 0 + b = new(kzg4844.Blob) + for i := 0; i < 4096; i++ { + offset += copy(b[i*32+1:i*32+32], blobBytes[offset:]) + if offset == len(blobBytes) { + break + } + } + if offset < len(blobBytes) { + return nil, fmt.Errorf("failed to fit all data into blob. bytes remaining: %v", len(blobBytes)-offset) + } + return +} + +func RetrieveBlobBytes(blob *kzg4844.Blob) ([]byte, error) { + data := make([]byte, MaxBlobBytesSize) + for i := 0; i < 4096; i++ { + if blob[i*32] != 0 { + return nil, fmt.Errorf("invalid blob, found non-zero high order byte %x of field element %d", data[i*32], i) + } + copy(data[i*31:i*31+31], blob[i*32+1:i*32+32]) + } + return data, nil +} + +func makeBlobCommitment(bz []byte) (b kzg4844.Blob, c kzg4844.Commitment, err error) { + blob, err := MakeBlobCanonical(bz) + if err != nil { + return + } + b = *blob + c, err = kzg4844.BlobToCommitment(&b) + if err != nil { + return + } + return +} + +func MakeBlobTxSidecar(blobBytes []byte, maxBlobs int) (*eth.BlobTxSidecar, error) { + if len(blobBytes) == 0 { + return ð.BlobTxSidecar{ + Blobs: []kzg4844.Blob{*emptyBlob}, + Commitments: []kzg4844.Commitment{emptyBlobCommit}, + Proofs: []kzg4844.Proof{emptyBlobProof}, + }, nil + } + if maxBlobs <= 0 { + maxBlobs = 1 + } + if len(blobBytes) > maxBlobs*MaxBlobBytesSize { + return nil, fmt.Errorf("data size %d exceeds %d blobs capacity (%d bytes)", len(blobBytes), maxBlobs, maxBlobs*MaxBlobBytesSize) + } + blobCount := (len(blobBytes) + MaxBlobBytesSize - 1) / MaxBlobBytesSize + var ( + err error + blobs = make([]kzg4844.Blob, blobCount) + commitments = make([]kzg4844.Commitment, blobCount) + ) + for i := 0; i < blobCount; i++ { + start := i * MaxBlobBytesSize + end := start + MaxBlobBytesSize + if end > len(blobBytes) { + end = len(blobBytes) + } + blobs[i], commitments[i], err = makeBlobCommitment(blobBytes[start:end]) + if err != nil { + return nil, err + } + } + return ð.BlobTxSidecar{ + Blobs: blobs, + Commitments: commitments, + }, nil +} + +func CompressBatchBytes(batchBytes []byte) ([]byte, error) { + if len(batchBytes) == 0 { + return nil, nil + } + compressedBatchBytes, err := zstd.CompressBatchBytes(batchBytes) + if err != nil { + return nil, fmt.Errorf("failed to compress batch bytes, err: %w", err) + } + return compressedBatchBytes, nil +} diff --git a/common/go.mod b/common/go.mod new file mode 100644 index 000000000..7cee9aa04 --- /dev/null +++ b/common/go.mod @@ -0,0 +1,74 @@ +module morph-l2/common + +go 1.24.0 + +replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.7 + +require ( + github.com/holiman/uint256 v1.2.4 + github.com/morph-l2/go-ethereum v1.10.14-0.20260506071313-045be0fdc7ca + github.com/stretchr/testify v1.10.0 + github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a +) + +require ( + github.com/VictoriaMetrics/fastcache v1.12.2 // indirect + github.com/bits-and-blooms/bitset v1.20.0 // indirect + github.com/btcsuite/btcd v0.20.1-beta // indirect + github.com/btcsuite/btcd/btcec/v2 v2.2.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/consensys/bavard v0.1.27 // indirect + github.com/consensys/gnark-crypto v0.16.0 // indirect + github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/deckarep/golang-set v1.8.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/edsrzf/mmap-go v1.1.0 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect + github.com/ethereum/go-ethereum v1.10.26 // indirect + github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-stack/stack v1.8.1 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.1 // indirect + github.com/hashicorp/go-bexpr v0.1.13 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/holiman/bloomfilter/v2 v2.0.3 // indirect + github.com/huin/goupnp v1.3.0 // indirect + github.com/iden3/go-iden3-crypto v0.0.16 // indirect + github.com/jackpal/go-nat-pmp v1.0.2 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/pointerstructure v1.2.1 // indirect + github.com/mmcloughlin/addchain v0.4.0 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/tsdb v0.10.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rjeczalik/notify v0.9.3 // indirect + github.com/rs/cors v1.11.0 // indirect + github.com/scroll-tech/zktrie v0.8.4 // indirect + github.com/shirou/gopsutil v3.21.11+incompatible // indirect + github.com/status-im/keycard-go v0.3.2 // indirect + github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect + github.com/tklauser/go-sysconf v0.3.13 // indirect + github.com/tklauser/numcpus v0.7.0 // indirect + github.com/tyler-smith/go-bip39 v1.1.0 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/time v0.12.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect + gopkg.in/urfave/cli.v1 v1.20.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + rsc.io/tmplfunc v0.0.3 // indirect +) diff --git a/common/go.sum b/common/go.sum new file mode 100644 index 000000000..7570e7331 --- /dev/null +++ b/common/go.sum @@ -0,0 +1,327 @@ +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= +github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= +github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/btcsuite/btcd v0.20.1-beta h1:Ik4hyJqN8Jfyv3S4AGBOmyouMsYE3EdYODkMbQjwPGw= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd/btcec/v2 v2.2.1 h1:xP60mv8fvp+0khmrN0zTdPC3cNm24rfeE6lh2R/Yv3E= +github.com/btcsuite/btcd/btcec/v2 v2.2.1/go.mod h1:9/CSmJxmuvqzX9Wh2fXMWToLOHhPd11lSPuIupwTkI8= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= +github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/consensys/bavard v0.1.27 h1:j6hKUrGAy/H+gpNrpLU3I26n1yc+VMGmd6ID5+gAhOs= +github.com/consensys/bavard v0.1.27/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs= +github.com/consensys/gnark-crypto v0.16.0 h1:8Dl4eYmUWK9WmlP1Bj6je688gBRJCJbT8Mw4KoTAawo= +github.com/consensys/gnark-crypto v0.16.0/go.mod h1:Ke3j06ndtPTVvo++PhGNgvm+lgpLvzbcE2MqljY7diU= +github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= +github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= +github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= +github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= +github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= +github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= +github.com/ethereum/go-ethereum v1.10.26 h1:i/7d9RBBwiXCEuyduBQzJw/mKmnvzsN14jqBmytw72s= +github.com/ethereum/go-ethereum v1.10.26/go.mod h1:EYFyF19u3ezGLD4RqOkLq+ZCXzYbLoNDdZlMt7kyKFg= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays= +github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= +github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/hashicorp/go-bexpr v0.1.13 h1:HNwp7vZrMpRq8VZXj8VF90LbZpRjQQpim1oJF0DgSwg= +github.com/hashicorp/go-bexpr v0.1.13/go.mod h1:gN7hRKB3s7yT+YvTdnhZVLTENejvhlkZ8UE4YVBS+Q8= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU= +github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/iden3/go-iden3-crypto v0.0.16 h1:zN867xiz6HgErXVIV/6WyteGcOukE9gybYTorBMEdsk= +github.com/iden3/go-iden3-crypto v0.0.16/go.mod h1:dLpM4vEPJ3nDHzhWFXDjzkn1qHoBeOT/3UEhXsEsP3E= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.1 h1:ZhBBeX8tSlRpu/FFhXH4RC4OJzFlqsQhoHZAz4x7TIw= +github.com/mitchellh/pointerstructure v1.2.1/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= +github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= +github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/morph-l2/go-ethereum v1.10.14-0.20260506071313-045be0fdc7ca h1:ogHsgxvm1wzyNKYDSAsIi0PJZeu9VhQECSL91X/KTWI= +github.com/morph-l2/go-ethereum v1.10.14-0.20260506071313-045be0fdc7ca/go.mod h1:nkVzHjQWCOjvukQW8ittlwX+Xz9gmVHrP7mUi7zoHTs= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.10.0 h1:If5rVCMTp6W2SiRAQFlbpJNgVlgMEd+U2GZckwK38ic= +github.com/prometheus/tsdb v0.10.0/go.mod h1:oi49uRhEe9dPUTlS3JRZOwJuVi6tmh10QSgwXEyGCt4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= +github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= +github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/scroll-tech/zktrie v0.8.4 h1:UagmnZ4Z3ITCk+aUq9NQZJNAwnWl4gSxsLb2Nl7IgRE= +github.com/scroll-tech/zktrie v0.8.4/go.mod h1:XvNo7vAk8yxNyTjBDj5WIiFzYW4bx/gJ78+NK6Zn6Uk= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/status-im/keycard-go v0.3.2 h1:YusIF/bHx6YZis8UTOJrpZFnTs4IkRBdmJXqdiXkpFE= +github.com/status-im/keycard-go v0.3.2/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI= +github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= +github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4= +github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0= +github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4= +github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY= +github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= +github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/urfave/cli.v1 v1.20.0 h1:NdAVW6RYxDif9DhDHaAortIu956m2c0v+09AZBPTbE0= +gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= +rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= diff --git a/contracts/contracts/l1/rollup/Rollup.sol b/contracts/contracts/l1/rollup/Rollup.sol index 5b6de82f2..659a6860f 100644 --- a/contracts/contracts/l1/rollup/Rollup.sol +++ b/contracts/contracts/l1/rollup/Rollup.sol @@ -5,6 +5,7 @@ import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Own import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; import {BatchHeaderCodecV0} from "../../libraries/codec/BatchHeaderCodecV0.sol"; import {BatchHeaderCodecV1} from "../../libraries/codec/BatchHeaderCodecV1.sol"; + import {IRollupVerifier} from "../../libraries/verifier/IRollupVerifier.sol"; import {IL1MessageQueue} from "./IL1MessageQueue.sol"; import {IRollup} from "./IRollup.sol"; @@ -247,7 +248,7 @@ contract Rollup is IRollup, OwnableUpgradeable, PausableUpgradeable { require(batchDataInput.numL1Messages > 0, "l1msg delay"); } uint256 submitterBitmap = IL1Staking(l1StakingContract).getStakerBitmap(_msgSender()); - bytes32 _blobVersionedHash = (blobhash(0) == bytes32(0)) ? ZERO_VERSIONED_HASH : blobhash(0); + bytes32 _blobVersionedHash = _computeBlobVersionedHash(batchDataInput.version); _commitBatchWithBatchData(batchDataInput, batchSignatureInput, submitterBitmap, _blobVersionedHash); } @@ -281,7 +282,7 @@ contract Rollup is IRollup, OwnableUpgradeable, PausableUpgradeable { uint256 submitterBitmap, bytes32 blobVersionedHash ) internal { - require(batchDataInput.version == 0 || batchDataInput.version == 1, "invalid version"); + require(batchDataInput.version <= 2, "invalid version"); require(batchDataInput.prevStateRoot != bytes32(0), "previous state root is zero"); require(batchDataInput.postStateRoot != bytes32(0), "new state root is zero"); @@ -318,11 +319,10 @@ contract Rollup is IRollup, OwnableUpgradeable, PausableUpgradeable { assembly { _batchIndex := add(_batchIndex, 1) // increase batch index } - bytes32 _blobVersionedHash = blobVersionedHash; - { + // Determine header length: V0 = 249, V1/V2 = 257 uint256 _headerLength = BatchHeaderCodecV0.BATCH_HEADER_LENGTH; - if (batchDataInput.version == 1) { + if (batchDataInput.version >= 1) { _headerLength = BatchHeaderCodecV1.BATCH_HEADER_LENGTH; } assembly { @@ -330,25 +330,28 @@ contract Rollup is IRollup, OwnableUpgradeable, PausableUpgradeable { mstore(0x40, add(_batchPtr, _headerLength)) } - // store entries, the order matters + // Store header fields (identical layout for all versions) BatchHeaderCodecV0.storeVersion(_batchPtr, batchDataInput.version); BatchHeaderCodecV0.storeBatchIndex(_batchPtr, _batchIndex); BatchHeaderCodecV0.storeL1MessagePopped(_batchPtr, batchDataInput.numL1Messages); BatchHeaderCodecV0.storeTotalL1MessagePopped(_batchPtr, _totalL1MessagesPoppedOverall); BatchHeaderCodecV0.storeDataHash(_batchPtr, dataHash); - BatchHeaderCodecV0.storeBlobVersionedHash(_batchPtr, _blobVersionedHash); + BatchHeaderCodecV0.storeBlobVersionedHash(_batchPtr, blobVersionedHash); BatchHeaderCodecV0.storePrevStateHash(_batchPtr, batchDataInput.prevStateRoot); BatchHeaderCodecV0.storePostStateHash(_batchPtr, batchDataInput.postStateRoot); BatchHeaderCodecV0.storeWithdrawRootHash(_batchPtr, batchDataInput.withdrawalRoot); - BatchHeaderCodecV0.storeSequencerSetVerifyHash(_batchPtr, keccak256(batchSignatureInput.sequencerSets)); + BatchHeaderCodecV0.storeSequencerSetVerifyHash( + _batchPtr, + keccak256(batchSignatureInput.sequencerSets) + ); BatchHeaderCodecV0.storeParentBatchHash(_batchPtr, _parentBatchHash); - // store last block number if version >= 1 if (batchDataInput.version >= 1) { BatchHeaderCodecV1.storeLastBlockNumber(_batchPtr, batchDataInput.lastBlockNumber); } + committedBatches[_batchIndex] = BatchHeaderCodecV0.computeBatchHash(_batchPtr, _headerLength); committedStateRoots[_batchIndex] = batchDataInput.postStateRoot; - batchBlobVersionedHashes[_batchIndex] = _blobVersionedHash; + batchBlobVersionedHashes[_batchIndex] = blobVersionedHash; uint256 proveRemainingTime = 0; if (inChallenge) { // Make the batch finalize time longer than the time required for the current challenge @@ -406,12 +409,12 @@ contract Rollup is IRollup, OwnableUpgradeable, PausableUpgradeable { // check if the next batch has a stored blob hash (uint256 _batchPtr, ) = _loadBatchHeader(batchDataInput.parentBatchHeader); uint256 _nextBatchIndex = BatchHeaderCodecV0.getBatchIndex(_batchPtr) + 1; - bytes32 _blobVersionedHash = bytes32(0); + bytes32 _blobVersionedHash; if (batchBlobVersionedHashes[_nextBatchIndex] != bytes32(0)) { require(blobhash(0) == bytes32(0), "must not carry blob when using stored blob hash"); _blobVersionedHash = batchBlobVersionedHashes[_nextBatchIndex]; } else { - _blobVersionedHash = (blobhash(0) == bytes32(0)) ? ZERO_VERSIONED_HASH : blobhash(0); + _blobVersionedHash = _computeBlobVersionedHash(batchDataInput.version); } _commitBatchWithBatchData(batchDataInput, batchSignatureInput, 0, _blobVersionedHash); @@ -755,7 +758,11 @@ contract Rollup is IRollup, OwnableUpgradeable, PausableUpgradeable { require(_batchProof.length > 0, "Invalid batch proof"); uint256 _batchIndex = BatchHeaderCodecV0.getBatchIndex(memPtr); - bytes32 _blobVersionedHash = BatchHeaderCodecV0.getBlobVersionedHash(memPtr); + + // All versions (V0, V1, V2) store the blob hash input at offset 57. + // For V2, this is the aggregated blob hash (keccak256 of all blob hashes concatenated), + // stored at commit time. For V0/V1, it is the single blob versioned hash. + bytes32 _blobHashInput = BatchHeaderCodecV0.getBlobVersionedHash(memPtr); bytes32 _publicInputHash = keccak256( abi.encodePacked( @@ -765,7 +772,7 @@ contract Rollup is IRollup, OwnableUpgradeable, PausableUpgradeable { BatchHeaderCodecV0.getWithdrawRootHash(memPtr), BatchHeaderCodecV0.getSequencerSetVerifyHash(memPtr), BatchHeaderCodecV0.getDataHash(memPtr), - _blobVersionedHash + _blobHashInput ) ); @@ -861,7 +868,8 @@ contract Rollup is IRollup, OwnableUpgradeable, PausableUpgradeable { uint256 _length; if (_version == 0) { (_memPtr, _length) = BatchHeaderCodecV0.loadAndValidate(_batchHeader); - } else if (_version == 1) { + } else if (_version == 1 || _version == 2) { + // V2 uses same 257-byte format as V1 (aggregated blob hash at offset 57) (_memPtr, _length) = BatchHeaderCodecV1.loadAndValidate(_batchHeader); } else { revert("Unsupported batch version"); @@ -907,6 +915,36 @@ contract Rollup is IRollup, OwnableUpgradeable, PausableUpgradeable { } } + /// @dev Compute the blob versioned hash for the current transaction. + /// V0/V1: blobhash(0), or ZERO_VERSIONED_HASH if no blob is attached. + /// At most one blob is allowed: extra blobs are ignored by this hash but + /// would still be delivered to L2 derivation, which concatenates all blobs. + /// V2: keccak256(blobhash(0) || ... || blobhash(N-1)), requires at least 1 blob. + function _computeBlobVersionedHash(uint256 _version) internal view returns (bytes32 _blobVersionedHash) { + if (_version == 2) { + uint256 _blobCount; + assembly { + let scratchPtr := mload(0x40) + let i := 0 + for {} 1 {} { + let h := blobhash(i) + if iszero(h) { break } + mstore(add(scratchPtr, mul(i, 32)), h) + i := add(i, 1) + } + _blobCount := i + } + require(_blobCount > 0, "V2 requires at least 1 blob"); + assembly { + let scratchPtr := mload(0x40) + _blobVersionedHash := keccak256(scratchPtr, mul(_blobCount, 32)) + } + } else { + require(blobhash(1) == bytes32(0), "legacy batches support exactly 1 blob"); + _blobVersionedHash = (blobhash(0) == bytes32(0)) ? ZERO_VERSIONED_HASH : blobhash(0); + } + } + /// @dev Internal function to load L1 message hashes from the message queue. /// @param _ptr The memory offset to store the transaction hash. /// @param _numL1Messages The number of L1 messages to load. diff --git a/contracts/contracts/test/BlobVersionedHashLegacy.t.sol b/contracts/contracts/test/BlobVersionedHashLegacy.t.sol new file mode 100644 index 000000000..16f9fb998 --- /dev/null +++ b/contracts/contracts/test/BlobVersionedHashLegacy.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity =0.8.24; + +import "forge-std/Test.sol"; + +/// @dev Mirrors Rollup._computeBlobVersionedHash legacy branch (V0/V1) for isolated testing. +contract BlobVersionedHashLegacyHarness { + bytes32 internal constant ZERO_VERSIONED_HASH = + 0x010657f37554c781402a22917dee2f75def7ab966d7b770905398eba3c444014; + + function legacyBlobVersionedHash() external view returns (bytes32) { + require(blobhash(1) == bytes32(0), "legacy batches support exactly 1 blob"); + return (blobhash(0) == bytes32(0)) ? ZERO_VERSIONED_HASH : blobhash(0); + } +} + +contract BlobVersionedHashLegacyTest is Test { + function test_legacyBlobVersionedHash_noBlobs_returnsZeroSentinel() public { + BlobVersionedHashLegacyHarness h = new BlobVersionedHashLegacyHarness(); + bytes32 expected = + 0x010657f37554c781402a22917dee2f75def7ab966d7b770905398eba3c444014; + assertEq(h.legacyBlobVersionedHash(), expected); + } +} diff --git a/contracts/contracts/test/Rollup.t.sol b/contracts/contracts/test/Rollup.t.sol index 9d40569ed..dbcc95c1c 100644 --- a/contracts/contracts/test/Rollup.t.sol +++ b/contracts/contracts/test/Rollup.t.sol @@ -149,14 +149,14 @@ contract RollupCommitBatchWithProofTest is L1MessageBaseTest { } /// @notice Test: commitBatchWithProof reverts on version mismatch in consistency check - /// Note: Version 1 requires different header length, so this tests the "invalid version" error from _commitBatchWithBatchData + /// Note: Version 3+ is invalid, so this tests the "invalid version" error from _commitBatchWithBatchData function test_commitBatchWithProof_reverts_on_invalid_version() public { _mockMessageQueueStalled(); hevm.warp(block.timestamp + 7200); - - // Create batchDataInput with version 2 (invalid) + + // Create batchDataInput with version 3 (invalid, since V2 is now supported) IRollup.BatchDataInput memory batchDataInput = IRollup.BatchDataInput({ - version: 2, // Invalid version + version: 3, // Invalid version parentBatchHeader: batchHeader0, lastBlockNumber: 1, numL1Messages: 0, @@ -164,9 +164,9 @@ contract RollupCommitBatchWithProofTest is L1MessageBaseTest { postStateRoot: bytes32(uint256(2)), withdrawalRoot: getTreeRoot() }); - + bytes memory batchHeader1 = _createMatchingBatchHeader(1, 0, bytes32(uint256(1)), bytes32(uint256(2)), getTreeRoot()); - + hevm.prank(alice); hevm.expectRevert("invalid version"); rollup.commitBatchWithProof( @@ -176,6 +176,33 @@ contract RollupCommitBatchWithProofTest is L1MessageBaseTest { bytes("") ); } + + /// @notice Test: commitBatchWithProof with V2 requires blobs (reverts without blob in test env) + function test_commitBatchWithProof_v2_reverts_without_blob() public { + _mockMessageQueueStalled(); + hevm.warp(block.timestamp + 7200); + + IRollup.BatchDataInput memory batchDataInput = IRollup.BatchDataInput({ + version: 2, + parentBatchHeader: batchHeader0, + lastBlockNumber: 1, + numL1Messages: 0, + prevStateRoot: bytes32(uint256(1)), + postStateRoot: bytes32(uint256(2)), + withdrawalRoot: getTreeRoot() + }); + + bytes memory batchHeader1 = _createMatchingBatchHeader(1, 0, bytes32(uint256(1)), bytes32(uint256(2)), getTreeRoot()); + + hevm.prank(alice); + hevm.expectRevert("V2 requires at least 1 blob"); + rollup.commitBatchWithProof( + batchDataInput, + batchSignatureInput, + batchHeader1, + bytes("") + ); + } /// @notice Test: commitBatchWithProof reverts when paused function test_commitBatchWithProof_reverts_when_paused() public { @@ -920,10 +947,10 @@ contract RollupTest is L1MessageBaseTest { rollup.commitBatch(batchDataInput, batchSignatureInput); hevm.stopPrank(); - // invalid version, revert + // invalid version, revert (version 3+ is invalid; version 2 is now valid V2 multi-blob) hevm.startPrank(alice); hevm.expectRevert("invalid version"); - batchDataInput = IRollup.BatchDataInput(2, batchHeader0, 0, 0, stateRoot, stateRoot, getTreeRoot()); + batchDataInput = IRollup.BatchDataInput(3, batchHeader0, 0, 0, stateRoot, stateRoot, getTreeRoot()); rollup.commitBatch(batchDataInput, batchSignatureInput); hevm.stopPrank(); @@ -1440,4 +1467,282 @@ contract RollupCommitStateTest is L1MessageBaseTest { hevm.expectRevert("commitBatch requires no stored blob hash"); rollup.commitBatch(batchDataInput, batchSignatureInput); } + +} + +/// @dev Tests for Rollup V2 multi-blob batch header support (simplified: 257-byte header with aggregated blob hash). +contract RollupCommitBatchV2Test is L1MessageBaseTest { + bytes32 public stateRoot = bytes32(uint256(1)); + IRollup.BatchSignatureInput public batchSignatureInput; + bytes public batchHeader0; + bytes32 public batchHash0; + + function setUp() public virtual override { + super.setUp(); + + batchSignatureInput = IRollup.BatchSignatureInput( + uint256(0), + abi.encode(uint256(0), new address[](0), uint256(0), new address[](0), uint256(0), new address[](0)), + bytes("0x") + ); + + // Register staker + hevm.deal(alice, 5 * STAKING_VALUE); + Types.StakerInfo memory stakerInfo = ffi.generateStakerInfo(alice); + address[] memory addrs = new address[](1); + addrs[0] = alice; + hevm.prank(multisig); + l1Staking.updateWhitelist(addrs, new address[](0)); + hevm.prank(alice); + l1Staking.register{value: STAKING_VALUE}(stakerInfo.tmKey, stakerInfo.blsKey); + + // Import genesis batch (V0) + bytes memory _genesis = new bytes(249); + assembly { + let p := add(_genesis, 0x20) + mstore(add(p, 25), 1) // dataHash not zero + mstore(add(p, 57), 0x010657f37554c781402a22917dee2f75def7ab966d7b770905398eba3c444014) // ZERO_VERSIONED_HASH + mstore(add(p, 89), 0) // prevStateHash + mstore(add(p, 121), 1) // postStateHash + } + batchHeader0 = _genesis; + hevm.prank(multisig); + rollup.importGenesisBatch(batchHeader0); + batchHash0 = rollup.committedBatches(0); + } + + /// @dev Helper: build a valid V2 header (257 bytes, same as V1 format) with aggregated blob hash at offset 57. + /// @param aggregatedBlobHash keccak256(blobhash(0) || ... || blobhash(N-1)) — the aggregated hash to store. + /// @param lastBlockNumber The last block number in this batch. + function _buildV2Header( + bytes32 aggregatedBlobHash, + uint64 lastBlockNumber + ) internal pure returns (bytes memory header) { + header = new bytes(BatchHeaderCodecV1.BATCH_HEADER_LENGTH); // 257 bytes + assembly { + let p := add(header, 0x20) + mstore8(p, 2) // version = 2 + mstore(add(p, 1), shl(192, 1)) // batchIndex = 1 + // l1MessagePopped = 0, totalL1MessagePopped = 0 (already zero) + mstore(add(p, 25), 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef) // dataHash + mstore(add(p, 57), aggregatedBlobHash) // aggregated blob hash at offset 57 + mstore(add(p, 89), 0x0000000000000000000000000000000000000000000000000000000000000001) // prevStateHash + mstore(add(p, 121), 0x0000000000000000000000000000000000000000000000000000000000000002) // postStateHash + mstore(add(p, 153), 0x0000000000000000000000000000000000000000000000000000000000000003) // withdrawRootHash + mstore(add(p, 185), 0x0000000000000000000000000000000000000000000000000000000000000004) // seqSetVerifyHash + mstore(add(p, 217), 0x0000000000000000000000000000000000000000000000000000000000000005) // parentBatchHash + mstore(add(p, 249), shl(192, lastBlockNumber)) // lastBlockNumber + } + } + + /// @dev V2 header is 257 bytes (same as V1 format). + function test_v2Header_is_257_bytes() public { + bytes32 aggHash = keccak256(abi.encodePacked(bytes32(uint256(0x1111)))); + bytes memory header = _buildV2Header(aggHash, 100); + assertEq(header.length, 257); + } + + /// @dev V2 header shorter than 257 bytes reverts via V1 loadAndValidate. + function test_v2Header_reverts_tooShort() public { + bytes memory header = new bytes(256); + header[0] = bytes1(uint8(2)); // version = 2 + hevm.expectRevert("batch header length is incorrect"); + // Call _loadBatchHeader indirectly via revertBatch (passes through V1 loader for V2) + // We can't call _loadBatchHeader directly; use a function that calls it + hevm.prank(multisig); + rollup.revertBatch(header, 1); + } + + /*////////////////////////////////////////////////////////////// + Rollup Integration Tests + //////////////////////////////////////////////////////////////*/ + + /// @dev commitBatch with V2 requires at least 1 blob (reverts in test env where blobhash(0)=0). + function test_commitBatchV2_reverts_without_blob() public { + IRollup.BatchDataInput memory batchDataInput = IRollup.BatchDataInput({ + version: 2, + parentBatchHeader: batchHeader0, + lastBlockNumber: 1, + numL1Messages: 0, + prevStateRoot: stateRoot, + postStateRoot: bytes32(uint256(2)), + withdrawalRoot: getTreeRoot() + }); + + hevm.prank(alice); + hevm.expectRevert("V2 requires at least 1 blob"); + rollup.commitBatch(batchDataInput, batchSignatureInput); + } + + /// @dev V2 _loadBatchHeader accepts a valid 257-byte V2 header (uses V1 loader). + /// Tested indirectly: we build a V2 header and verify revertBatch can parse it. + function test_loadBatchHeaderV2_accepts_257_bytes() public { + // Build a V2 header with a known aggregated blob hash + bytes32 aggHash = keccak256(abi.encodePacked(bytes32(uint256(0x1111)), bytes32(uint256(0x2222)))); + bytes memory header = _buildV2Header(aggHash, 42); + + // 257 bytes, version=2 + assertEq(header.length, 257); + assertEq(uint8(header[0]), 2); // version = 2 + // aggregated hash stored at offset 57 + bytes32 storedHash; + assembly { + storedHash := mload(add(add(header, 0x20), 57)) + } + assertEq(storedHash, aggHash); + } + + /*////////////////////////////////////////////////////////////// + Multi-blob aggregated hash tests + //////////////////////////////////////////////////////////////*/ + + /// @dev V2 aggregated hash for N=1: keccak256(h0) — differs from h0 itself (V1 incompatibility). + function test_v2AggregatedHash_single_differs_from_raw() public { + bytes32 h0 = bytes32(uint256(0xBEEF)); + bytes32 aggHash = keccak256(abi.encodePacked(h0)); + assertTrue(aggHash != h0, "keccak(h0) must differ from h0"); + } + + /// @dev V2 aggregated hash for N=2: keccak256(h0 || h1). + function test_v2AggregatedHash_two_blobs() public { + bytes32 h0 = bytes32(uint256(0xAAAA)); + bytes32 h1 = bytes32(uint256(0xBBBB)); + bytes32 expected = keccak256(abi.encodePacked(h0, h1)); + + // Recompute with the same assembly logic used in _computeBlobVersionedHash + bytes32 computed; + assembly { + let ptr := mload(0x40) + mstore(ptr, h0) + mstore(add(ptr, 32), h1) + computed := keccak256(ptr, 64) + } + assertEq(computed, expected); + } + + /// @dev V2 aggregated hash for N=3: keccak256(h0 || h1 || h2). + function test_v2AggregatedHash_three_blobs() public { + bytes32 h0 = bytes32(uint256(0xAAAA)); + bytes32 h1 = bytes32(uint256(0xBBBB)); + bytes32 h2 = bytes32(uint256(0xCCCC)); + bytes32 expected = keccak256(abi.encodePacked(h0, h1, h2)); + + bytes32 computed; + assembly { + let ptr := mload(0x40) + mstore(ptr, h0) + mstore(add(ptr, 32), h1) + mstore(add(ptr, 64), h2) + computed := keccak256(ptr, 96) + } + assertEq(computed, expected); + } + + /// @dev V2 aggregated hash is order-sensitive: (h0,h1) != (h1,h0). + function test_v2AggregatedHash_order_sensitive() public { + bytes32 h0 = bytes32(uint256(0xAAAA)); + bytes32 h1 = bytes32(uint256(0xBBBB)); + bytes32 fwd = keccak256(abi.encodePacked(h0, h1)); + bytes32 rev = keccak256(abi.encodePacked(h1, h0)); + assertTrue(fwd != rev, "aggregated hash must be order-sensitive"); + } + + /*////////////////////////////////////////////////////////////// + _verifyProof public input hash tests + //////////////////////////////////////////////////////////////*/ + + /// @dev _verifyProof uses the 32-byte value at offset 57 (aggregated blob hash for V2) + /// as blobHashInput in publicInputHash. + /// Verified by: + /// 1. Showing publicInputHash with aggregated hash (V2) != publicInputHash with raw hash (V1) + /// 2. Confirming V2 header's offset 57 holds the aggregated hash + /// 3. Confirming publicInputHash derived from offset 57 equals the expected V2 value + function test_verifyProof_v2_publicInput_uses_aggregated_hash() public { + bytes32 prevStateRoot = bytes32(uint256(0x1)); + bytes32 postStateRoot = bytes32(uint256(0x2)); + bytes32 withdrawRoot = bytes32(uint256(0x3)); + bytes32 seqVerifyHash = bytes32(uint256(0x4)); + bytes32 dataHash = bytes32(uint256(0xDEAD)); + bytes32 h0 = bytes32(uint256(0xAAAA)); + bytes32 h1 = bytes32(uint256(0xBBBB)); + + // V2 aggregated hash: keccak256(h0 || h1) + bytes32 aggregatedHash = keccak256(abi.encodePacked(h0, h1)); + + // Expected V2 publicInputHash (Rollup._verifyProof reads offset 57 for all versions) + bytes32 v2PublicInput = keccak256(abi.encodePacked( + uint64(layer2ChainID), + prevStateRoot, postStateRoot, withdrawRoot, seqVerifyHash, dataHash, + aggregatedHash + )); + + // V1 would put h0 directly at offset 57 — must differ from V2 + bytes32 v1PublicInput = keccak256(abi.encodePacked( + uint64(layer2ChainID), + prevStateRoot, postStateRoot, withdrawRoot, seqVerifyHash, dataHash, + h0 + )); + assertTrue(v2PublicInput != v1PublicInput, "V2 publicInputHash must differ from V1"); + + // V2 single blob also differs from V1 (keccak(h0) != h0) + bytes32 v2SinglePublicInput = keccak256(abi.encodePacked( + uint64(layer2ChainID), + prevStateRoot, postStateRoot, withdrawRoot, seqVerifyHash, dataHash, + keccak256(abi.encodePacked(h0)) + )); + assertTrue(v2SinglePublicInput != v1PublicInput, "V2 single-blob publicInputHash must differ from V1"); + + // Confirm V2 header stores aggregated hash at offset 57 + bytes32 _batchHash0 = batchHash0; + bytes memory header = new bytes(BatchHeaderCodecV1.BATCH_HEADER_LENGTH); + assembly { + let p := add(header, 0x20) + mstore8(p, 2) + mstore(add(p, 1), shl(192, 1)) + mstore(add(p, 25), dataHash) + mstore(add(p, 57), aggregatedHash) + mstore(add(p, 89), prevStateRoot) + mstore(add(p, 121), postStateRoot) + mstore(add(p, 153), withdrawRoot) + mstore(add(p, 185), seqVerifyHash) + mstore(add(p, 217), _batchHash0) + mstore(add(p, 249), shl(192, 1)) + } + bytes32 offset57; + assembly { offset57 := mload(add(add(header, 0x20), 57)) } + assertEq(offset57, aggregatedHash, "offset 57 must hold aggregated hash"); + + // Confirm publicInputHash derived from offset 57 equals v2PublicInput + bytes32 derivedPublicInput = keccak256(abi.encodePacked( + uint64(layer2ChainID), + prevStateRoot, postStateRoot, withdrawRoot, seqVerifyHash, dataHash, + offset57 + )); + assertEq(derivedPublicInput, v2PublicInput, "publicInputHash from header must match expected"); + } + + /// @dev V2 single-blob publicInputHash != V1 single-blob publicInputHash (not backward-compatible). + function test_verifyProof_v2_single_blob_differs_from_v1() public { + bytes32 versioned_hash = bytes32(uint256(0xBEEF)); + + // V1: blob input = versioned_hash directly + bytes32 v1Input = keccak256( + abi.encodePacked( + uint64(layer2ChainID), + bytes32(0), bytes32(0), bytes32(0), bytes32(0), bytes32(0), + versioned_hash + ) + ); + + // V2: blob input = keccak256(versioned_hash) + bytes32 v2Input = keccak256( + abi.encodePacked( + uint64(layer2ChainID), + bytes32(0), bytes32(0), bytes32(0), bytes32(0), bytes32(0), + keccak256(abi.encodePacked(versioned_hash)) + ) + ); + + assertTrue(v1Input != v2Input, "V2 single-blob must differ from V1"); + } } diff --git a/contracts/src/deploy-config/holesky.ts b/contracts/src/deploy-config/holesky.ts index fb4bfbcbd..f492fcdeb 100644 --- a/contracts/src/deploy-config/holesky.ts +++ b/contracts/src/deploy-config/holesky.ts @@ -14,7 +14,7 @@ const config = { l2BaseFee: 0.1, // Gwei // verify contract config - programVkey: '0x00940d658cf507217304ec5f7ca5558e2e0fd67881485f604b63588c31a8792f', + programVkey: '0x00b149e7386afc945dfabb81a4e678eaf4f625bd2a687831295cb9f650ee1807', // rollup contract config // initialize config finalizationPeriodSeconds: 600, diff --git a/contracts/src/deploy-config/hoodi.ts b/contracts/src/deploy-config/hoodi.ts index e8b869b6d..70f4e9122 100644 --- a/contracts/src/deploy-config/hoodi.ts +++ b/contracts/src/deploy-config/hoodi.ts @@ -17,7 +17,7 @@ const config = { l2BaseFee: 0.1, // Gwei // verify contract config - programVkey: '0x00940d658cf507217304ec5f7ca5558e2e0fd67881485f604b63588c31a8792f', + programVkey: '0x00b149e7386afc945dfabb81a4e678eaf4f625bd2a687831295cb9f650ee1807', // rollup contract config // initialize config finalizationPeriodSeconds: 600, diff --git a/contracts/src/deploy-config/l1.ts b/contracts/src/deploy-config/l1.ts index b49c5a0ce..3162e4c9d 100644 --- a/contracts/src/deploy-config/l1.ts +++ b/contracts/src/deploy-config/l1.ts @@ -17,7 +17,7 @@ const config = { l2BaseFee: 0.1, // Gwei // verify contract config - programVkey: '0x00940d658cf507217304ec5f7ca5558e2e0fd67881485f604b63588c31a8792f', + programVkey: '0x00b149e7386afc945dfabb81a4e678eaf4f625bd2a687831295cb9f650ee1807', // rollup contract config // initialize config finalizationPeriodSeconds: 10, diff --git a/contracts/src/deploy-config/qanetl1.ts b/contracts/src/deploy-config/qanetl1.ts index 57bc2223a..c40e6642e 100644 --- a/contracts/src/deploy-config/qanetl1.ts +++ b/contracts/src/deploy-config/qanetl1.ts @@ -14,7 +14,7 @@ const config = { l2BaseFee: 0.1, // Gwei // verify contract config - programVkey: '0x00940d658cf507217304ec5f7ca5558e2e0fd67881485f604b63588c31a8792f', + programVkey: '0x00b149e7386afc945dfabb81a4e678eaf4f625bd2a687831295cb9f650ee1807', // rollup contract config // initialize config finalizationPeriodSeconds: 600, diff --git a/contracts/src/deploy-config/sepolia.ts b/contracts/src/deploy-config/sepolia.ts index c5c4b046c..3cb891111 100644 --- a/contracts/src/deploy-config/sepolia.ts +++ b/contracts/src/deploy-config/sepolia.ts @@ -18,7 +18,7 @@ const config = { /** * ---to---legacy property */ - programVkey: '0x00940d658cf507217304ec5f7ca5558e2e0fd67881485f604b63588c31a8792f', + programVkey: '0x00b149e7386afc945dfabb81a4e678eaf4f625bd2a687831295cb9f650ee1807', rollupMinDeposit: 0.0001, rollupProofWindow: 86400, rollupGenesisBlockNumber: 0, diff --git a/contracts/src/deploy-config/testnetl1.ts b/contracts/src/deploy-config/testnetl1.ts index f46a89eca..4cc4487bc 100644 --- a/contracts/src/deploy-config/testnetl1.ts +++ b/contracts/src/deploy-config/testnetl1.ts @@ -13,7 +13,7 @@ const config = { sequencerWindowSize: 200, channelTimeout: 120, - programVkey: '0x00940d658cf507217304ec5f7ca5558e2e0fd67881485f604b63588c31a8792f', + programVkey: '0x00b149e7386afc945dfabb81a4e678eaf4f625bd2a687831295cb9f650ee1807', rollupMinDeposit: 1, rollupProofWindow: 100, rollupGenesisBlockNumber: 0, diff --git a/gas-oracle/app/src/da_scalar/blob.rs b/gas-oracle/app/src/da_scalar/blob.rs index 18765cc17..dea157f3c 100644 --- a/gas-oracle/app/src/da_scalar/blob.rs +++ b/gas-oracle/app/src/da_scalar/blob.rs @@ -14,14 +14,14 @@ const MAX_BLOB_TX_PAYLOAD_SIZE: usize = 131072; // 131072 = 4096 * 32 = 1024 * 4 pub struct Blob(pub [u8; MAX_BLOB_TX_PAYLOAD_SIZE]); impl Blob { - pub fn get_origin_batch(&self) -> Result, BlobError> { - let compressed_data = self.get_compressed_batch()?; - decompress_batch(&compressed_data) - } - - pub fn get_compressed_batch(&self) -> Result, BlobError> { - // Decode blob, recovering BLS12-381 scalars. - let mut data = vec![0u8; MAX_BLOB_TX_PAYLOAD_SIZE]; + /// Extract the raw payload segment from a blob by removing the BLS12-381 field encoding, + /// without performing zstd decompression. + /// Under the new format, the concatenation of multiple blob segments forms the full zstd + /// payload. + pub fn get_payload_bytes(&self) -> Result, BlobError> { + // Decode blob and recover BLS12-381 scalars. + // Each field element is 32 bytes, with 31 bytes of usable payload. + let mut data = vec![0u8; 4096 * 31]; for i in 0..4096 { if self.0[i * 32] != 0 { return Err(BlobError::InvalidBlob(anyhow!(format!( @@ -32,12 +32,13 @@ impl Blob { } data[i * 31..i * 31 + 31].copy_from_slice(&self.0[i * 32 + 1..i * 32 + 32]); } - - // detect_zstd_compressed - Ok(Self::detect_zstd_compressed(data)?) + Ok(data) } - fn detect_zstd_compressed(decoded_blob: Vec) -> Result, BlobError> { + pub fn detect_zstd_compressed( + decoded_blob: Vec, + num_blobs: usize, + ) -> Result, BlobError> { // The format of zstd_compression is shown in the following link: // https://github.com/facebook/zstd/blob/dev/doc/zstd_compression_format.md#frame_header let fcs_field_size = match parse_frame_header_descriptor(&decoded_blob) { @@ -74,13 +75,14 @@ impl Blob { // compressed_data = frame_header + frame_content_field_size + zstd_blocks let compressed_len = get_blocks_size(&decoded_blob, fcs_field_size)? + 1; - if compressed_len as usize > MAX_BLOB_TX_PAYLOAD_SIZE - 4096 { + let max_payload_size = num_blobs * (MAX_BLOB_TX_PAYLOAD_SIZE - 4096); + if compressed_len as usize > max_payload_size { return Err(BlobError::Error(anyhow!("oversized batch payload"))) } let compressed_batch = decoded_blob[..compressed_len].to_vec(); // check data - Self::check_data(&compressed_batch, &decoded_blob, fcs_field_size)?; + Self::check_data(&compressed_batch, &decoded_blob, fcs_field_size, num_blobs)?; Ok(compressed_batch) } @@ -89,6 +91,7 @@ impl Blob { compressed_data: &Vec, decoded_blob: &[u8], fcs_field_size: usize, + num_blobs: usize, ) -> Result<(), BlobError> { let origin_batch = decompress_batch(compressed_data)?; @@ -103,9 +106,11 @@ impl Blob { ))) } + let total_blob_payload = num_blobs as f32 * (MAX_BLOB_TX_PAYLOAD_SIZE - 4096) as f32; log::info!( - "check_blob_data, blob usage {:.3}, batch_compression_ratio: {:.3}", - compressed_data.len() as f32 / MAX_BLOB_TX_PAYLOAD_SIZE as f32, + "check_blob_data, num_blobs: {}, blob usage {:.3}, batch_compression_ratio: {:.3}", + num_blobs, + compressed_data.len() as f32 / total_blob_payload, orgin_content_size as f32 / compressed_data.len() as f32 ); Ok(()) @@ -146,12 +151,6 @@ fn parse_block_header( return Err("Compressed batch is too small to contain a valid block header".into()); } - // Make sure we have enough data to parse - if compressed_data.len() < 1 + fcs_field_size + 3 { - // 2 (minimum starting point) + 3 (block header size) - return Err("Compressed batch is too small to contain a valid block header".into()); - } - // Extract the 3-byte header // data_block_start_index = fcs_field_size + 1(frame block size); let header = &compressed_data[1 + fcs_field_size..1 + fcs_field_size + 3]; @@ -200,10 +199,11 @@ mod tests { let blob_bytes = load_zstd_blob(); let blob = Blob(blob_bytes); - let result = blob.get_compressed_batch(); - assert!(result.is_ok(), "{}", result.err().unwrap()); - - let compressed_batch: Vec = result.unwrap(); + // Under the new format, a single blob still uses the multi-blob decoding path + // (get_payload_bytes -> detect_zstd_compressed -> decompress_batch). + let payload = blob.get_payload_bytes().expect("get_payload_bytes failed"); + let compressed_batch = + Blob::detect_zstd_compressed(payload, 1).expect("detect_zstd_compressed failed"); assert_eq!(compressed_batch.len(), 60576); let origin_batch = super::decompress_batch(&compressed_batch).unwrap(); @@ -239,25 +239,19 @@ mod tests { encoded_bytes }; - let origin_batch = decompress_batch(&encoded_bytes).unwrap(); - println!( - "=======origin_batch_len: {:?}, batch_data_bytes_len: {:?}", - origin_batch.len(), - batch_data_bytes.len() - ); - - // Encode to blob + // Encode to blob under the new format: encode compressed bytes into BLS12-381 + // field elements in 31-byte groups. let mut blob_data = [0u8; MAX_BLOB_TX_PAYLOAD_SIZE]; for (i, &byte) in encoded_bytes.iter().enumerate() { blob_data[1 + (i % 31) + 32 * (i / 31)] = byte; } let blob = Blob(blob_data); - // Test compressed_batch from blob - let result = blob.get_compressed_batch(); - assert!(result.is_ok(), "{}", result.err().unwrap()); - - let compressed_batch: Vec = result.unwrap(); + // Under the new format, a single blob still uses the multi-blob decoding path + // (get_payload_bytes -> detect_zstd_compressed -> decompress_batch). + let payload = blob.get_payload_bytes().expect("get_payload_bytes failed"); + let compressed_batch = + Blob::detect_zstd_compressed(payload, 1).expect("detect_zstd_compressed failed"); println!("encoded_bytes_len: {:?}", encoded_bytes.len()); assert_eq!(compressed_batch.len(), encoded_bytes.len()); assert_eq!(compressed_batch, encoded_bytes); diff --git a/gas-oracle/app/src/da_scalar/calculate.rs b/gas-oracle/app/src/da_scalar/calculate.rs index 92b0e537e..7e7df970a 100644 --- a/gas-oracle/app/src/da_scalar/calculate.rs +++ b/gas-oracle/app/src/da_scalar/calculate.rs @@ -7,17 +7,23 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use super::{ - blob::{kzg_to_versioned_hash, Blob}, + blob::{decompress_batch, kzg_to_versioned_hash, Blob}, error::ScalarError, typed_tx::TypedTransaction, MAX_BLOB_TX_PAYLOAD_SIZE, }; +/// Extract the full batch data from multiple blobs. +/// All blobs belong to the same batch: the batch is compressed as a whole, split into +/// multiple segments (each at most 4096 * 31 bytes), and then encoded into BLS12-381 +/// field elements stored in the blobs. +/// This function remains compatible with the single-blob case. pub(super) fn extract_tx_payload( indexed_hashes: Vec, sidecars: &[Value], -) -> Result>, ScalarError> { - let mut batch_bytes = Vec::>::new(); +) -> Result, ScalarError> { + let num_blobs = indexed_hashes.len(); + let mut combined_payload = Vec::::new(); for i_h in indexed_hashes { if let Some(sidecar) = sidecars.iter().find(|sidecar| { sidecar["index"].as_str().unwrap_or("1000").parse::().unwrap_or(1000) == i_h.index @@ -62,14 +68,15 @@ pub(super) fn extract_tx_payload( let blob_array: [u8; MAX_BLOB_TX_PAYLOAD_SIZE] = decoded_blob.try_into().unwrap(); let blob_struct = Blob(blob_array); - let origin_batch = blob_struct.get_origin_batch().map_err(|e| { + // Extract the raw payload segment by removing only the BLS12-381 encoding, + // without zstd decompression. + let payload_bytes = blob_struct.get_payload_bytes().map_err(|e| { ScalarError::CalculateError(anyhow!(format!( - "Failed to decode blob tx payload: {}", - e + "Failed to get payload bytes from blob, blob_hash: {:?}, err: {}", + i_h.hash, e ))) })?; - - batch_bytes.push(origin_batch); + combined_payload.extend_from_slice(&payload_bytes); } else { return Err(ScalarError::CalculateError(anyhow!(format!( "no blob in response matches desired index: {}", @@ -77,7 +84,21 @@ pub(super) fn extract_tx_payload( )))); } } - Ok(batch_bytes) + + // After concatenation, use detect_zstd_compressed to trim the valid compressed payload + // (excluding trailing zero padding), then decompress the batch as a whole. + let compressed_data = Blob::detect_zstd_compressed(combined_payload, num_blobs).map_err(|e| { + ScalarError::CalculateError(anyhow!(format!( + "Failed to detect zstd compressed data from combined blob payload: {}", + e + ))) + })?; + decompress_batch(&compressed_data).map_err(|e| { + ScalarError::CalculateError(anyhow!(format!( + "Failed to decompress combined blob payload: {}", + e + ))) + }) } pub fn extract_txn_count(origin_batch: &Vec, last_block_num: u64) -> Option { diff --git a/gas-oracle/app/src/da_scalar/l1_scalar.rs b/gas-oracle/app/src/da_scalar/l1_scalar.rs index 1f653915b..bbd6d8e7a 100644 --- a/gas-oracle/app/src/da_scalar/l1_scalar.rs +++ b/gas-oracle/app/src/da_scalar/l1_scalar.rs @@ -17,8 +17,8 @@ use crate::{ metrics::ORACLE_SERVICE_METRICS, signer::send_transaction, }; -use remote_signer_client::SignerClient; use ethers::{abi::AbiDecode, prelude::*, utils::hex}; +use remote_signer_client::SignerClient; use serde_json::Value; const PRECISION: u64 = 10u64.pow(9); @@ -218,7 +218,7 @@ impl ScalarUpdater { block_num: U64, ) -> Result<(u64, u64), ScalarError> { //Step1. get_data_from_blob - let (l2_data_len, l2_txn) = + let (l2_data_len, num_blobs, l2_txn) = self.get_data_from_blob(tx_hash, block_num).await.map_err(|e| { log::error!("get_data_from_blob error: {:#?}", e); e @@ -249,7 +249,7 @@ impl ScalarUpdater { let commit_scalar = (rollup_gas_used.as_u64() + self.finalize_batch_gas_used) * PRECISION / l2_txn.max(self.txn_per_batch); let blob_scalar = if l2_data_len > 0 { - MAX_BLOB_TX_PAYLOAD_SIZE as u64 * PRECISION / l2_data_len + num_blobs.max(1) * MAX_BLOB_TX_PAYLOAD_SIZE as u64 * PRECISION / l2_data_len } else { MAX_BLOB_SCALAR }; @@ -272,7 +272,7 @@ impl ScalarUpdater { &self, tx_hash: TxHash, block_num: U64, - ) -> Result<(u64, u64), ScalarError> { + ) -> Result<(u64, u64, u64), ScalarError> { let blob_tx = self .l1_provider .get_transaction(tx_hash) @@ -300,7 +300,7 @@ impl ScalarUpdater { let indexed_hashes = data_and_hashes_from_txs(&blob_block.transactions, &blob_tx); if indexed_hashes.is_empty() { log::info!("no blob in this batch, batch_tx_hash: {:#?}", tx_hash); - return Ok((0, 0)); + return Ok((0, 0, 0)); } // Waiting for the next L1 block to be produced. @@ -369,65 +369,25 @@ impl ScalarUpdater { )))); } - let tx_payloads = extract_tx_payload(indexed_hashes, sidecars)?; - let data_with_txn_count: Vec<(u64, u64)> = tx_payloads - .iter() - .map(|batch: &Vec| { - (batch.len() as u64, extract_txn_count(batch, last_block_num).unwrap_or_default()) - }) - .collect(); + // All blobs belong to the same batch: the batch is compressed as a whole, + // split into multiple segments across blobs, then reconstructed by concatenating + // the segments, trimming the valid compressed payload, and decompressing once. + // This also remains compatible with the single-blob case. + let num_blobs = indexed_hashes.len() as u64; + let origin_batch = extract_tx_payload(indexed_hashes, sidecars)?; - let (total_size, total_count) = data_with_txn_count - .iter() - .fold((0u64, 0u64), |acc, &(size, count)| (acc.0 + size, acc.1 + count)); + let batch_size = origin_batch.len() as u64; + let txn_count = extract_txn_count(&origin_batch, last_block_num).unwrap_or_default(); - Ok((total_size, total_count)) + Ok((batch_size, num_blobs, txn_count)) } } #[cfg(test)] mod tests { - use crate::da_scalar::blob::Blob; use super::*; - use std::{env::var, fs, path::Path, str::FromStr, sync::Arc}; - - #[test] - fn test_blob_data() { - let blob_data_path = Path::new("data/blob_with_context.data"); - let data = fs::read_to_string(blob_data_path).expect("Unable to read file"); - let hex_data: Vec = hex::decode(data.trim()).unwrap(); - - let mut blob_array = [0u8; 131072]; - blob_array.copy_from_slice(&hex_data); - - let blob_struct = Blob(blob_array); - let origin_batch = blob_struct - .get_origin_batch() - .map_err(|e| { - ScalarError::CalculateError(anyhow!(format!( - "Failed to decode blob tx payload: {}", - e - ))) - }) - .unwrap(); - - let mut tx_payloads: Vec> = vec![]; - tx_payloads.push(origin_batch); - - let data_with_txn_count: Vec<(u64, u64)> = tx_payloads - .iter() - .map(|batch: &Vec| { - (batch.len() as u64, extract_txn_count(batch, 328208).unwrap_or_default()) - }) - .collect(); - - let (total_size, total_count) = data_with_txn_count - .iter() - .fold((0u64, 0u64), |acc, &(size, count)| (acc.0 + size, acc.1 + count)); - - println!("total_size: {}, total_count: {}", total_size, total_count) - } + use std::{env::var, str::FromStr, sync::Arc}; #[tokio::test] #[ignore] @@ -464,7 +424,8 @@ mod tests { let l2_oracle_contract = GasPriceOracle::new(l2_oracle_address, l2_signer); - let ext_signer = SignerClient::new("appid", "privkey_pem", "address", "chain", "url").unwrap(); + let ext_signer = + SignerClient::new("appid", "privkey_pem", "address", "chain", "url").unwrap(); let mut overhead: ScalarUpdater = ScalarUpdater::new( l1_provider, l2_provider, diff --git a/go.work b/go.work index d29dbaad9..e64d53272 100644 --- a/go.work +++ b/go.work @@ -2,6 +2,7 @@ go 1.24.0 use ( ./bindings + ./common ./contracts ./node ./ops/l2-genesis diff --git a/go.work.sum b/go.work.sum index c3da941e8..8f40420be 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1286,6 +1286,7 @@ github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk= github.com/yeya24/promlinter v0.2.0 h1:xFKDQ82orCU5jQujdaD8stOHiv8UN68BSdn2a8u8Y3o= github.com/yeya24/promlinter v0.2.0/go.mod h1:u54lkmBOZrpEbQQ6gox2zWKKLKu2SGe+2KOiextY+IA= +github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -1363,6 +1364,7 @@ golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0Y golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20230206171751-46f607a40771 h1:xP7rWLUr1e1n2xkK5YB4LI0hPEy3LJC6Wk+D4pGlOJg= golang.org/x/exp v0.0.0-20230206171751-46f607a40771/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp/typeparams v0.0.0-20220613132600-b0d781184e0d h1:+W8Qf4iJtMGKkyAygcKohjxTk4JPsL9DpzApJ22m5Ic= golang.org/x/exp/typeparams v0.0.0-20220613132600-b0d781184e0d/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= @@ -1377,6 +1379,7 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -1456,6 +1459,7 @@ golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= diff --git a/node/derivation/batch_info.go b/node/derivation/batch_info.go index d795092b8..add7efe36 100644 --- a/node/derivation/batch_info.go +++ b/node/derivation/batch_info.go @@ -1,10 +1,8 @@ package derivation import ( - "bytes" "encoding/binary" "fmt" - "io" "math/big" "github.com/morph-l2/go-ethereum/common" @@ -13,6 +11,7 @@ import ( geth "github.com/morph-l2/go-ethereum/eth" "github.com/morph-l2/go-ethereum/eth/catalyst" + commonbatch "morph-l2/common/batch" "morph-l2/node/types" "morph-l2/node/zstd" ) @@ -83,7 +82,7 @@ func (bi *BatchInfo) ParseBatch(batch geth.RPCRollupBatch) error { if len(batch.Sidecar.Blobs) == 0 { return fmt.Errorf("blobs length can not be zero") } - parentBatchHeader := types.BatchHeaderBytes(batch.ParentBatchHeader) + parentBatchHeader := commonbatch.BatchHeaderBytes(batch.ParentBatchHeader) parentBatchIndex, err := parentBatchHeader.BatchIndex() if err != nil { return fmt.Errorf("decode batch header index error:%v", err) @@ -98,8 +97,27 @@ func (bi *BatchInfo) ParseBatch(batch geth.RPCRollupBatch) error { bi.withdrawalRoot = batch.WithdrawRoot bi.version = uint64(batch.Version) tq := newTxQueue() - var rawBlockContexts hexutil.Bytes - var txsData []byte + + // Multi-blob batches (V2+) are produced by zstd-compressing the entire + // batch payload as a single stream and then splitting the compressed + // bytes across N blobs in submission order. To recover the payload we + // must concatenate all blob bodies first and decompress once; per-blob + // decompression would fail on the second blob since it is not a + // standalone zstd stream. + compressed := make([]byte, 0, len(batch.Sidecar.Blobs)*commonbatch.MaxBlobBytesSize) + for i := range batch.Sidecar.Blobs { + blobCopy := batch.Sidecar.Blobs[i] + blobData, err := commonbatch.RetrieveBlobBytes(&blobCopy) + if err != nil { + return err + } + compressed = append(compressed, blobData...) + } + batchBytes, err := zstd.DecompressBatchBytes(compressed) + if err != nil { + return fmt.Errorf("decompress batch bytes error:%v", err) + } + var blockCount uint64 if batch.Version > 0 { parentVersion, err := parentBatchHeader.Version() @@ -107,13 +125,11 @@ func (bi *BatchInfo) ParseBatch(batch geth.RPCRollupBatch) error { return fmt.Errorf("decode batch header version error:%v", err) } if parentVersion == 0 { - blobData, err := types.RetrieveBlobBytes(&batch.Sidecar.Blobs[0]) - if err != nil { - return err - } - batchBytes, err := zstd.DecompressBatchBytes(blobData) - if err != nil { - return fmt.Errorf("decompress batch bytes error:%v", err) + // V0 -> V1+ transition: parent header carries no LastBlockNumber, + // so derive blockCount from the first block context embedded at + // the start of the decompressed batch. + if len(batchBytes) < 60 { + return fmt.Errorf("decompressed batch too short for start block context: have %d, need 60", len(batchBytes)) } var startBlock BlockContext if err := startBlock.Decode(batchBytes[:60]); err != nil { @@ -127,48 +143,35 @@ func (bi *BatchInfo) ParseBatch(batch geth.RPCRollupBatch) error { } blockCount = batch.LastBlockNumber - parentBatchBlock } - - } - // If BlockContexts is not nil, the block context should not be included in the blob. - // Therefore, the required length must be zero. - length := blockCount * 60 - for _, blob := range batch.Sidecar.Blobs { - blobCopy := blob - blobData, err := types.RetrieveBlobBytes(&blobCopy) - if err != nil { - return err - } - batchBytes, err := zstd.DecompressBatchBytes(blobData) - if err != nil { - return err - } - reader := bytes.NewReader(batchBytes) - if batch.BlockContexts == nil { - if len(batchBytes) < int(length) { - rawBlockContexts = append(rawBlockContexts, batchBytes...) - length -= uint64(len(batchBytes)) - reader.Reset(nil) - } else { - bcBytes := make([]byte, length) - _, err = reader.Read(bcBytes) - if err != nil { - return fmt.Errorf("read block context error:%s", err.Error()) - } - rawBlockContexts = append(rawBlockContexts, bcBytes...) - length = 0 - } - } - data, err := io.ReadAll(reader) - if err != nil { - return fmt.Errorf("read txBytes error:%s", err.Error()) - } - txsData = append(txsData, data...) } + + var rawBlockContexts hexutil.Bytes + var txsData []byte if batch.BlockContexts != nil { + // Block contexts come from calldata; the entire decompressed stream + // is tx payload data. ABI-decoded `bytes` can be a non-nil zero/short + // slice, so guard the 2-byte block-count prefix read explicitly. + if len(batch.BlockContexts) < 2 { + return fmt.Errorf("calldata block contexts too short for block count prefix: have %d, need 2", len(batch.BlockContexts)) + } blockCount = uint64(binary.BigEndian.Uint16(batch.BlockContexts[:2])) + if uint64(len(batch.BlockContexts)) < 2+60*blockCount { + return fmt.Errorf("calldata block contexts too short: have %d, need %d", len(batch.BlockContexts), 2+60*blockCount) + } rawBlockContexts = batch.BlockContexts[2 : 60*blockCount+2] + txsData = batchBytes + } else { + // Block contexts are at the head of the decompressed stream, + // immediately followed by the tx payload bytes. + bcLen := blockCount * 60 + if uint64(len(batchBytes)) < bcLen { + return fmt.Errorf("decompressed batch too short for block contexts: have %d, need %d", len(batchBytes), bcLen) + } + rawBlockContexts = batchBytes[:bcLen] + txsData = batchBytes[bcLen:] } - data, err := types.DecodeTxsFromBytes(txsData) + + data, err := commonbatch.DecodeTxsFromBytes(txsData) if err != nil { return err } diff --git a/node/derivation/batch_info_test.go b/node/derivation/batch_info_test.go new file mode 100644 index 000000000..416aafbdc --- /dev/null +++ b/node/derivation/batch_info_test.go @@ -0,0 +1,226 @@ +package derivation + +import ( + "crypto/rand" + "math/big" + "testing" + + "github.com/morph-l2/go-ethereum/common" + eth "github.com/morph-l2/go-ethereum/core/types" + "github.com/morph-l2/go-ethereum/crypto/kzg4844" + geth "github.com/morph-l2/go-ethereum/eth" + "github.com/stretchr/testify/require" + + commonbatch "morph-l2/common/batch" + "morph-l2/common/blob" + "morph-l2/node/types" + "morph-l2/node/zstd" +) + +// buildBlockContexts returns the concatenated 60-byte encoding of `count` +// sequential, tx-empty blocks starting at `startBlock`. The produced layout +// matches what tx-submitter places at the head of a V1/V2 batch payload. +func buildBlockContexts(startBlock uint64, count int) []byte { + buf := make([]byte, 0, count*60) + for i := 0; i < count; i++ { + wb := &types.WrappedBlock{ + Number: startBlock + uint64(i), + Timestamp: 1_700_000_000 + uint64(i)*6, + BaseFee: big.NewInt(1_000_000_000), + GasLimit: 30_000_000, + } + buf = append(buf, wb.BlockContextBytes(0, 0)...) + } + return buf +} + +// buildV1ParentHeader encodes a minimal V1 parent header whose LastBlockNumber +// is one below `nextStartBlock`, so that ParseBatch can derive blockCount via +// the (batch.LastBlockNumber - parent.LastBlockNumber) path. +func buildV1ParentHeader(parentIndex, nextStartBlock uint64) []byte { + return commonbatch.BatchHeaderV1{ + BatchHeaderV0: commonbatch.BatchHeaderV0{ + BatchIndex: parentIndex, + BlobVersionedHash: blob.EmptyVersionedHash, + }, + LastBlockNumber: nextStartBlock - 1, + }.Bytes() +} + +// splitCompressedIntoBlobs mirrors the tx-submitter strategy of compressing +// the entire payload as a single zstd stream and then slicing the compressed +// bytes into MaxBlobBytesSize chunks, each packed into a canonical blob. +func splitCompressedIntoBlobs(t *testing.T, compressed []byte) []kzg4844.Blob { + t.Helper() + var blobs []kzg4844.Blob + for offset := 0; offset < len(compressed); offset += commonbatch.MaxBlobBytesSize { + end := offset + commonbatch.MaxBlobBytesSize + if end > len(compressed) { + end = len(compressed) + } + blob, err := commonbatch.MakeBlobCanonical(compressed[offset:end]) + require.NoError(t, err) + blobs = append(blobs, *blob) + } + if len(blobs) == 0 { + // An empty payload still requires at least one (empty) blob so that + // downstream consumers can iterate. Production submitters never emit + // an empty batch, but the helper should remain total. + blobs = append(blobs, kzg4844.Blob{}) + } + return blobs +} + +// TestParseBatchSingleBlob covers the backward-compatible path where a V1 +// batch fits in a single blob. It guards against regressions in the recent +// "concatenate then decompress" refactor: the single-blob flow must still +// yield the same block contexts it did before multi-blob support landed. +func TestParseBatchSingleBlob(t *testing.T) { + const ( + parentIndex = 99 + startBlock = 1_000 + blockCount = 5 + ) + + blockCtx := buildBlockContexts(startBlock, blockCount) + payload := append(blockCtx, 0x00) // empty tx stream terminator + + compressed, err := zstd.CompressBatchBytes(payload) + require.NoError(t, err) + require.LessOrEqual(t, len(compressed), commonbatch.MaxBlobBytesSize, + "single-blob test expects compressed payload to fit in one blob") + + blobs := splitCompressedIntoBlobs(t, compressed) + require.Len(t, blobs, 1) + + batch := geth.RPCRollupBatch{ + Version: 1, + ParentBatchHeader: buildV1ParentHeader(parentIndex, startBlock), + LastBlockNumber: startBlock + blockCount - 1, + Sidecar: eth.BlobTxSidecar{Blobs: blobs}, + } + + var bi BatchInfo + require.NoError(t, bi.ParseBatch(batch)) + + require.EqualValues(t, parentIndex+1, bi.batchIndex) + require.EqualValues(t, startBlock, bi.FirstBlockNumber()) + require.EqualValues(t, startBlock+blockCount-1, bi.LastBlockNumber()) + require.Len(t, bi.blockContexts, blockCount) + for i, bc := range bi.blockContexts { + require.EqualValues(t, uint64(startBlock+i), bc.Number, + "block %d number mismatch", i) + } +} + +// TestParseBatchMultiBlob is the core multi-blob regression: it forces the +// compressed payload to exceed a single blob's capacity and verifies that +// ParseBatch reconstructs the decompressed stream by concatenating all blob +// bodies before running zstd.Decompress. A naive per-blob decompression loop +// would fail on blob[1] since it is mid-zstd-frame data, so a successful +// parse here proves the concatenation path is wired correctly. +// +// Compression-resistant random bytes are appended after the block-context +// header (past the tx terminator) purely to inflate the compressed size; the +// tx decoder stops at the first 0x00 byte and trailing random bytes are never +// interpreted as transactions. +func TestParseBatchMultiBlob(t *testing.T) { + const ( + parentIndex = 123 + startBlock = 2_000 + blockCount = 8 + ) + + blockCtx := buildBlockContexts(startBlock, blockCount) + + // 1 byte tx terminator + ~1.2x blob capacity of incompressible noise to + // guarantee the zstd output straddles a blob boundary. + padLen := commonbatch.MaxBlobBytesSize + commonbatch.MaxBlobBytesSize/5 + pad := make([]byte, padLen) + _, err := rand.Read(pad) + require.NoError(t, err) + + payload := make([]byte, 0, len(blockCtx)+1+padLen) + payload = append(payload, blockCtx...) + payload = append(payload, 0x00) + payload = append(payload, pad...) + + compressed, err := zstd.CompressBatchBytes(payload) + require.NoError(t, err) + require.Greater(t, len(compressed), commonbatch.MaxBlobBytesSize, + "multi-blob test requires compressed payload to overflow a single blob") + + blobs := splitCompressedIntoBlobs(t, compressed) + require.GreaterOrEqual(t, len(blobs), 2, "expected at least 2 blobs for multi-blob path") + + batch := geth.RPCRollupBatch{ + Version: 2, + ParentBatchHeader: buildV1ParentHeader(parentIndex, startBlock), + LastBlockNumber: startBlock + blockCount - 1, + PrevStateRoot: common.BigToHash(big.NewInt(1)), + PostStateRoot: common.BigToHash(big.NewInt(2)), + WithdrawRoot: common.BigToHash(big.NewInt(3)), + Sidecar: eth.BlobTxSidecar{Blobs: blobs}, + } + + var bi BatchInfo + require.NoError(t, bi.ParseBatch(batch)) + + require.EqualValues(t, parentIndex+1, bi.batchIndex) + require.EqualValues(t, 2, bi.version) + require.EqualValues(t, startBlock, bi.FirstBlockNumber()) + require.EqualValues(t, startBlock+blockCount-1, bi.LastBlockNumber()) + require.Len(t, bi.blockContexts, blockCount) + for i, bc := range bi.blockContexts { + require.EqualValues(t, uint64(startBlock+i), bc.Number, + "block %d number mismatch", i) + require.EqualValues(t, 1_700_000_000+uint64(i)*6, bc.Timestamp, + "block %d timestamp mismatch", i) + } + require.EqualValues(t, batch.PostStateRoot, bi.root) + require.EqualValues(t, batch.WithdrawRoot, bi.withdrawalRoot) +} + +// TestParseBatchMultiBlobConcatDecompressInvariant directly exercises the +// low-level invariant that multi-blob ParseBatch relies on: the compressed +// stream can only be recovered by concatenating blob bodies in submission +// order and decompressing once. Per-blob decompression must fail on any +// non-initial blob, and reordering blobs must corrupt the decompressed +// output. Keeping this explicit protects the invariant even if ParseBatch is +// later refactored to hide the concatenation step. +func TestParseBatchMultiBlobConcatDecompressInvariant(t *testing.T) { + pad := make([]byte, commonbatch.MaxBlobBytesSize+commonbatch.MaxBlobBytesSize/5) + _, err := rand.Read(pad) + require.NoError(t, err) + + compressed, err := zstd.CompressBatchBytes(pad) + require.NoError(t, err) + require.Greater(t, len(compressed), commonbatch.MaxBlobBytesSize) + + blobs := splitCompressedIntoBlobs(t, compressed) + require.GreaterOrEqual(t, len(blobs), 2) + + // In-order concatenation round-trips. + var concat []byte + for i := range blobs { + body, err := commonbatch.RetrieveBlobBytes(&blobs[i]) + require.NoError(t, err) + concat = append(concat, body...) + } + decoded, err := zstd.DecompressBatchBytes(concat) + require.NoError(t, err) + require.Equal(t, pad, decoded) + + // Reversing blob order must corrupt the stream; decompression should + // either error or yield a different payload. + var reversed []byte + for i := len(blobs) - 1; i >= 0; i-- { + body, err := commonbatch.RetrieveBlobBytes(&blobs[i]) + require.NoError(t, err) + reversed = append(reversed, body...) + } + if out, err := zstd.DecompressBatchBytes(reversed); err == nil { + require.NotEqual(t, pad, out, + "reversed-blob decompression unexpectedly matched payload") + } +} diff --git a/node/derivation/beacon.go b/node/derivation/beacon.go index 50bd0802a..ac663241f 100644 --- a/node/derivation/beacon.go +++ b/node/derivation/beacon.go @@ -159,8 +159,29 @@ func KZGToVersionedHash(commitment kzg4844.Commitment) (out common.Hash) { return out } -func VerifyBlobProof(blob *Blob, commitment kzg4844.Commitment, proof kzg4844.Proof) error { - return kzg4844.VerifyBlobProof(blob.KZGBlob(), commitment, proof) +// verifyBlob authenticates a blob against the L1-signed versioned blob hash +// by recomputing the KZG commitment locally and checking +// +// KZGToVersionedHash(BlobToCommitment(blob)) == expectedHash +// +// We deliberately do NOT verify a beacon-supplied kzg_proof. After +// EIP-7594 (PeerDAS / Osaka) the beacon /eth/v1/beacon/blob_sidecars +// endpoint's kzg_proof field is no longer guaranteed to be a legacy +// single-blob proof across forks/clients, and the new +// /eth/v1/beacon/blobs endpoint does not return proofs at all. The +// commitment round-trip gives us the same security property +// (blob bytes -> commitment -> versioned hash matches the L1-signed +// hash) without depending on those fields. +func verifyBlob(blob *Blob, expectedHash common.Hash) error { + commitment, err := kzg4844.BlobToCommitment(blob.KZGBlob()) + if err != nil { + return fmt.Errorf("cannot compute KZG commitment for blob: %w", err) + } + got := KZGToVersionedHash(commitment) + if got != expectedHash { + return fmt.Errorf("recomputed blob hash %s does not match expected %s", got.Hex(), expectedHash.Hex()) + } + return nil } // dataAndHashesFromTxs extracts calldata and datahashes from the input transactions and returns them. It diff --git a/node/derivation/derivation.go b/node/derivation/derivation.go index d5bf58681..05c4606b6 100644 --- a/node/derivation/derivation.go +++ b/node/derivation/derivation.go @@ -338,49 +338,64 @@ func (d *Derivation) fetchRollupDataByTxHash(txHash common.Hash, blockNumber uin return nil, fmt.Errorf("failed to get blobs, continuing processing:%v", err) } if len(blobSidecars) > 0 { - // Create blob sidecar - var blobTxSidecar eth.BlobTxSidecar - matchedCount := 0 - - // Match blobs + // Index beacon sidecars by their KZG-derived versioned hash so we + // can assemble the local sidecar in the exact order the L1 tx + // declared its blobs. Multi-blob batches are decoded by + // concatenating blob bodies in tx order; any reordering here + // would corrupt the resulting zstd stream. The map key is + // derived from the beacon-supplied commitment; verifyBlob below + // re-derives the same hash from the actual blob bytes, so a + // malicious beacon cannot forge an entry by lying about the + // commitment. + byHash := make(map[common.Hash]*BlobSidecar, len(blobSidecars)) for _, sidecar := range blobSidecars { var commitment kzg4844.Commitment copy(commitment[:], sidecar.KZGCommitment[:]) - versionedHash := KZGToVersionedHash(commitment) - - for _, expectedHash := range blobHashes { - if bytes.Equal(versionedHash[:], expectedHash[:]) { - matchedCount++ - d.logger.Info("Found matching blob", "index", sidecar.Index, "hash", versionedHash.Hex()) - - // Decode and process blob data - var blob Blob - b, err := hexutil.Decode(sidecar.Blob) - if err != nil { - d.logger.Error("Failed to decode blob data", "error", err) - continue - } - copy(blob[:], b) - - // Verify blob - //if err := VerifyBlobProof(&blob, commitment, kzg4844.Proof(sidecar.KZGProof)); err != nil { - // d.logger.Error("Blob verification failed", "error", err) - // continue - //} - - // Add to sidecar - blobTxSidecar.Blobs = append(blobTxSidecar.Blobs, *blob.KZGBlob()) - blobTxSidecar.Commitments = append(blobTxSidecar.Commitments, commitment) - blobTxSidecar.Proofs = append(blobTxSidecar.Proofs, kzg4844.Proof(sidecar.KZGProof)) - break - } - } + byHash[KZGToVersionedHash(commitment)] = sidecar } - d.logger.Info("Blob matching results", "matched", matchedCount, "expected", len(blobHashes)) - if matchedCount == 0 { - return nil, fmt.Errorf("no matching versionedHash was found") + // Downstream (ParseBatch) only consumes Sidecar.Blobs and + // Sidecar.Commitments; Proofs is intentionally left empty to + // avoid an extra ~O(n) KZG op per blob per batch on every + // sync. If a future consumer needs Proofs, compute them + // lazily there or call kzg4844.ComputeBlobProof here. + var blobTxSidecar eth.BlobTxSidecar + for i, expectedHash := range blobHashes { + sidecar, ok := byHash[expectedHash] + if !ok { + return nil, fmt.Errorf("blob %d (hash=%s) not found in beacon sidecars", i, expectedHash.Hex()) + } + + b, err := hexutil.Decode(sidecar.Blob) + if err != nil { + return nil, fmt.Errorf("failed to decode blob %d: %w", i, err) + } + // Reject malformed beacon responses up front. copy(blob[:], b) + // silently: + // - zero-pads when len(b) < BlobSize (tail of the + // zero-initialized array stays zero) + // - truncates when len(b) > BlobSize (extra bytes dropped) + // Either case would otherwise surface later as a confusing + // blob-hash mismatch instead of a clear length error. + if len(b) != BlobSize { + return nil, fmt.Errorf("blob %d: unexpected length %d (want %d, hash=%s)", i, len(b), BlobSize, expectedHash.Hex()) + } + var blob Blob + copy(blob[:], b) + + if err := verifyBlob(&blob, expectedHash); err != nil { + return nil, fmt.Errorf("blob %d: %w", i, err) + } + + var commitment kzg4844.Commitment + copy(commitment[:], sidecar.KZGCommitment[:]) + + d.logger.Info("Matched blob", "txOrder", i, "beaconIndex", sidecar.Index, "hash", expectedHash.Hex()) + blobTxSidecar.Blobs = append(blobTxSidecar.Blobs, *blob.KZGBlob()) + blobTxSidecar.Commitments = append(blobTxSidecar.Commitments, commitment) } + + d.logger.Info("Blob matching results", "matched", len(blobTxSidecar.Blobs), "expected", len(blobHashes)) batch.Sidecar = blobTxSidecar } else { return nil, fmt.Errorf("not matched blob,txHash:%v,blockNumber:%v", txHash, blockNumber) diff --git a/node/types/batch_header.go b/node/types/batch_header.go index 1616d8962..c9dba67ca 100644 --- a/node/types/batch_header.go +++ b/node/types/batch_header.go @@ -1,5 +1,21 @@ package types +// DEPRECATED: this file is a duplicate of morph-l2/common/batch's +// batch_header.go and is kept alive only because tx-submitter/utils/utils.go +// still imports BatchHeaderBytes from here. node/types cannot be turned into +// a thin shim re-exporting common/batch because that would close an import +// cycle: common/batch already depends on tx-submitter/db (via BatchCache), +// which depends on tx-submitter/utils, which would then depend back on +// common/batch. +// +// Cleanup path (out of scope for this PR; should be done by the tx-submitter +// owners alongside moving BatchCache out of common/batch): +// 1. Move common/batch/batch_cache.go, batch_storage.go, batch_query.go +// down to tx-submitter/batch/, so common/batch becomes a true leaf +// (depends on nothing under tx-submitter/). +// 2. Switch tx-submitter/utils/utils.go to import morph-l2/common/batch. +// 3. Delete this file. + import ( "encoding/binary" "errors" @@ -15,9 +31,15 @@ type ( const ( expectedLengthV0 = 249 expectedLengthV1 = 257 + // V2 reuses the V1 wire format (257 bytes). The only semantic + // difference is that the 32-byte field at offset 57 stores + // keccak256(blobhash(0) || ... || blobhash(N-1)) instead of a + // single blob versioned hash. + expectedLengthV2 = 257 BatchHeaderVersion0 = 0 BatchHeaderVersion1 = 1 + BatchHeaderVersion2 = 2 ) var ( @@ -41,6 +63,10 @@ func (b BatchHeaderBytes) validate() error { if len(b) != expectedLengthV1 { return ErrInvalidBatchHeaderLength } + case BatchHeaderVersion2: + if len(b) != expectedLengthV2 { + return ErrInvalidBatchHeaderLength + } default: return ErrInvalidBatchHeaderVersion } @@ -93,10 +119,32 @@ func (b BatchHeaderBytes) DataHash() (common.Hash, error) { return common.BytesToHash(b[25:57]), nil } +// BlobVersionedHash returns the EIP-4844 blob versioned hash recorded at +// offset [57:89]. This is only meaningful for V0/V1 batches, where the field +// holds the single blob's versioned hash. For V2 batches the same offset +// holds an aggregated hash; callers must use BlobHashesHash instead. func (b BatchHeaderBytes) BlobVersionedHash() (common.Hash, error) { if err := b.validate(); err != nil { return common.Hash{}, err } + version, _ := b.Version() + if version >= BatchHeaderVersion2 { + return common.Hash{}, errors.New("BlobVersionedHash is not available for V2+; use BlobHashesHash") + } + return common.BytesToHash(b[57:89]), nil +} + +// BlobHashesHash returns the aggregated blob hash recorded at offset [57:89] +// for V2+ batches, defined as keccak256(blobhash(0) || ... || blobhash(N-1)). +// V0/V1 batches do not aggregate and will return an error. +func (b BatchHeaderBytes) BlobHashesHash() (common.Hash, error) { + if err := b.validate(); err != nil { + return common.Hash{}, err + } + version, _ := b.Version() + if version < BatchHeaderVersion2 { + return common.Hash{}, errors.New("BlobHashesHash is only available for V2+; use BlobVersionedHash") + } return common.BytesToHash(b[57:89]), nil } diff --git a/ops/docker/docker-compose-4nodes.yml b/ops/docker/docker-compose-4nodes.yml index 32ea8b79b..8b011c69b 100644 --- a/ops/docker/docker-compose-4nodes.yml +++ b/ops/docker/docker-compose-4nodes.yml @@ -482,7 +482,7 @@ services: - TX_SUBMITTER_MIN_BLOCK=50 - TX_SUBMITTER_FINALIZE=true - TX_SUBMITTER_MAX_FINALIZE_NUM=100 - - TX_SUBMITTER_PRIORITY_ROLLUP=false + - TX_SUBMITTER_PRIORITY_ROLLUP=true - TX_SUBMITTER_METRICS_SERVER_ENABLE=false - TX_SUBMITTER_METRICS_HOSTNAME=0.0.0.0 - TX_SUBMITTER_METRICS_PORT=6060 @@ -493,9 +493,12 @@ services: - TX_SUBMITTER_LOG_COMPRESS=true - TX_SUBMITTER_L1_STAKING_ADDRESS=${MORPH_L1STAKING:-0x5fc8d32690cc91d4c39d9d3abcbd16989f875707} - TX_SUBMITTER_L1_STAKING_DEPLOYED_BLOCKNUM=0 + - TX_SUBMITTER_SEAL_BATCH=true + - TX_SUBMITTER_BATCH_V2_UPGRADE_TIME=1777533291 tx-submitter-1: container_name: tx-submitter-1 + profiles: ["multi-submitter"] depends_on: node-1: condition: service_started @@ -538,6 +541,7 @@ services: tx-submitter-2: container_name: tx-submitter-2 + profiles: ["multi-submitter"] depends_on: node-2: condition: service_started @@ -580,6 +584,7 @@ services: tx-submitter-3: container_name: tx-submitter-3 + profiles: ["multi-submitter"] depends_on: node-3: condition: service_started diff --git a/ops/docker/layer1/configs/values.env.template b/ops/docker/layer1/configs/values.env.template index 52a3ed168..6adca9511 100644 --- a/ops/docker/layer1/configs/values.env.template +++ b/ops/docker/layer1/configs/values.env.template @@ -48,8 +48,13 @@ export VIEW_FREEZE_CUTOFF_BPS=7500 export INCLUSION_LIST_SUBMISSION_DUE_BPS=6667 export PROPOSER_INCLUSION_LIST_CUTOFF_BPS=9167 export DATA_COLUMN_SIDECAR_SUBNET_COUNT=128 -export SAMPLES_PER_SLOT=8 -export CUSTODY_REQUIREMENT=4 +# Single-node devnet: every node IS the entire network, so it must +# custody all 128 columns and sample all 128 each slot. Without this, +# only CUSTODY_REQUIREMENT (default 4) columns are persisted, which is +# never enough to reconstruct blobs (need 64/128) and any historical +# blob retrieval (e.g. validator re-deriving from L1 genesis) fails. +export SAMPLES_PER_SLOT=128 +export CUSTODY_REQUIREMENT=128 export MAX_BLOBS_PER_BLOCK_ELECTRA=9 export TARGET_BLOBS_PER_BLOCK_ELECTRA=6 export MAX_REQUEST_BLOCKS_DENEB=128 @@ -81,5 +86,9 @@ export BPO_5_EPOCH=18446744073709551615 export BPO_5_MAX_BLOBS=0 export BPO_5_TARGET_BLOBS=0 export BPO_5_BASE_FEE_UPDATE_FRACTION=0 -export MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS=4096 +# Bumped from spec default 4096 (~27h on a 3s-slot/8-slot-per-epoch +# minimal preset) to ~30 days, so a freshly reset validator can always +# re-derive from L1 genesis without hitting "0 data columns found" +# pruning errors. 110000 epochs * 24s/epoch ≈ 30.5 days. +export MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS=110000 export MIN_EPOCHS_FOR_BLOCK_REQUESTS=33024 diff --git a/ops/l2-genesis/deploy-config/devnet-deploy-config.json b/ops/l2-genesis/deploy-config/devnet-deploy-config.json index 46923a4b4..7b8200198 100644 --- a/ops/l2-genesis/deploy-config/devnet-deploy-config.json +++ b/ops/l2-genesis/deploy-config/devnet-deploy-config.json @@ -15,7 +15,7 @@ "gasPriceOracleScalar": 1000000000, "gasPriceOracleOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "govVotingDuration": 1000, - "govBatchBlockInterval": 20, + "govBatchBlockInterval": 200, "govBatchTimeout": 600, "govRollupEpoch": 100, "recordOracleAddress": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", diff --git a/oracle/oracle/batch.go b/oracle/oracle/batch.go index 8e1eb9c23..df61159a6 100644 --- a/oracle/oracle/batch.go +++ b/oracle/oracle/batch.go @@ -9,8 +9,8 @@ import ( "time" "morph-l2/bindings/bindings" + commonbatch "morph-l2/common/batch" "morph-l2/node/derivation" - nodetypes "morph-l2/node/types" "morph-l2/oracle/backoff" "github.com/morph-l2/go-ethereum/accounts/abi/bind" @@ -163,7 +163,7 @@ func (o *Oracle) getBatchSubmissionByLogs(rLogs []types.Log, recordBatchSubmissi PostStateRoot: common.BytesToHash(rollupBatchData.PostStateRoot[:]), WithdrawRoot: common.BytesToHash(rollupBatchData.WithdrawalRoot[:]), } - parentBatchHeader := nodetypes.BatchHeaderBytes(batch.ParentBatchHeader) + parentBatchHeader := commonbatch.BatchHeaderBytes(batch.ParentBatchHeader) parentVersion, err := parentBatchHeader.Version() if err != nil { return fmt.Errorf("decode parent batch version error:%v", err) diff --git a/prover/Cargo.toml b/prover/Cargo.toml index 8da6b206f..7881e5442 100644 --- a/prover/Cargo.toml +++ b/prover/Cargo.toml @@ -19,7 +19,7 @@ resolver = "2" [workspace.package] version = "2.0.0" edition = "2021" -rust-version = "1.75" +rust-version = "1.91.0" authors = ["developers"] license = "MIT OR Apache-2.0" homepage = "https://github.com/morph-l2/morph/tree/main/prover" diff --git a/prover/bin/challenge/Cargo.lock b/prover/bin/challenge/Cargo.lock index 21e66f02f..5067ebf7e 100644 --- a/prover/bin/challenge/Cargo.lock +++ b/prover/bin/challenge/Cargo.lock @@ -122,7 +122,7 @@ checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.117", ] [[package]] @@ -688,9 +688,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.9" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -1018,7 +1018,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "syn 2.0.39", + "syn 2.0.117", "toml", "walkdir", ] @@ -1036,7 +1036,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.39", + "syn 2.0.117", ] [[package]] @@ -1062,7 +1062,7 @@ dependencies = [ "serde", "serde_json", "strum", - "syn 2.0.39", + "syn 2.0.117", "tempfile", "thiserror", "tiny-keccak", @@ -1365,7 +1365,7 @@ checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.117", ] [[package]] @@ -2051,6 +2051,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + [[package]] name = "num-integer" version = "0.1.45" @@ -2110,7 +2116,7 @@ dependencies = [ "proc-macro-crate 2.0.0", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.117", ] [[package]] @@ -2321,7 +2327,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.117", ] [[package]] @@ -2359,7 +2365,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.117", ] [[package]] @@ -2426,7 +2432,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", - "syn 2.0.39", + "syn 2.0.117", ] [[package]] @@ -2488,9 +2494,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -2549,9 +2555,9 @@ checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" [[package]] name = "quote" -version = "1.0.33" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2997,22 +3003,32 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" -version = "1.0.192" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.192" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.117", ] [[package]] @@ -3225,7 +3241,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.39", + "syn 2.0.117", ] [[package]] @@ -3267,9 +3283,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -3359,35 +3375,37 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.117", ] [[package]] name = "time" -version = "0.3.30" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", + "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.15" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ + "num-conv", "time-core", ] @@ -3442,7 +3460,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.117", ] [[package]] @@ -3627,7 +3645,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.117", ] [[package]] @@ -3835,7 +3853,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -3869,7 +3887,7 @@ checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.117", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/prover/bin/challenge/rust-toolchain b/prover/bin/challenge/rust-toolchain deleted file mode 100644 index f1d81c421..000000000 --- a/prover/bin/challenge/rust-toolchain +++ /dev/null @@ -1 +0,0 @@ -nightly-2023-12-03 \ No newline at end of file diff --git a/prover/bin/challenge/src/handler.rs b/prover/bin/challenge/src/handler.rs index 44763205f..36eac035f 100644 --- a/prover/bin/challenge/src/handler.rs +++ b/prover/bin/challenge/src/handler.rs @@ -413,7 +413,7 @@ struct BatchInfo { l1_message_popped: u64, total_l1_message_popped: u64, data_hash: [u8; 32], - blob_versioned_hash: [u8; 32], + blob_hashes: Vec<[u8; 32]>, prev_state_root: [u8; 32], post_state_root: [u8; 32], withdrawal_root: [u8; 32], @@ -490,8 +490,11 @@ impl BatchInfo { log::debug!("batch_header_ex len: {:#?}", batch_header_ex.len()); self.data_hash = batch_header_ex.get(0..32).unwrap_or_default().try_into().unwrap_or_default(); - self.blob_versioned_hash = batch_header_ex.get(32..64).unwrap_or_default().try_into().unwrap_or_default(); self.sequencer_set_verify_hash = batch_header_ex.get(64..96).unwrap_or_default().try_into().unwrap_or_default(); + + // All versions: single hash at [32..64] (aggregated for V2, versioned for V0/V1) + let hash: [u8; 32] = batch_header_ex.get(32..64).unwrap_or_default().try_into().unwrap_or_default(); + self.blob_hashes = vec![hash]; self } @@ -502,7 +505,8 @@ impl BatchInfo { batch_header.extend_from_slice(&self.l1_message_popped.to_be_bytes()); batch_header.extend_from_slice(&self.total_l1_message_popped.to_be_bytes()); batch_header.extend_from_slice(&self.data_hash); - batch_header.extend_from_slice(&self.blob_versioned_hash); + // blob hash at offset 57 (same position for all versions; aggregated for V2) + batch_header.extend_from_slice(self.blob_hashes.first().unwrap_or(&[0u8; 32])); batch_header.extend_from_slice(&self.prev_state_root); batch_header.extend_from_slice(&self.post_state_root); batch_header.extend_from_slice(&self.withdrawal_root); diff --git a/prover/bin/client/elf/verifier-client b/prover/bin/client/elf/verifier-client index affbb11e7..960322a30 100755 Binary files a/prover/bin/client/elf/verifier-client and b/prover/bin/client/elf/verifier-client differ diff --git a/prover/bin/host/src/execute.rs b/prover/bin/host/src/execute.rs index ccc3b1692..67ca4c210 100644 --- a/prover/bin/host/src/execute.rs +++ b/prover/bin/host/src/execute.rs @@ -1,7 +1,7 @@ use alloy_provider::DynProvider; use prover_executor_client::{types::input::ExecutorInput, EVMVerifier}; use prover_executor_host::{ - blob::{get_blob_info_from_blocks, get_blob_info_from_traces}, + blob::{get_blob_infos_from_blocks, get_blob_infos_from_traces}, execute::HostExecutor, trace::trace_to_input, utils::{assemble_block_input, query_block, HostExecutorOutput}, @@ -29,6 +29,7 @@ pub async fn execute_batch( end_block: u64, provider: &DynProvider, use_rpc_db: bool, + batch_version: u8, ) -> Result { assert!( end_block >= start_block, @@ -49,15 +50,20 @@ pub async fn execute_batch( } ExecutorInput { block_inputs: block_inputs.clone(), - blob_info: get_blob_info_from_blocks( + blob_infos: get_blob_infos_from_blocks( &block_inputs.iter().map(|input| input.current_block.clone()).collect::>(), )?, + batch_version, } } else { // Use sequencer's trace rpc. let traces = &mut get_block_traces(batch_index, start_block, end_block, provider).await?; let blocks_inputs = traces.iter().map(trace_to_input).collect::>(); - ExecutorInput { block_inputs: blocks_inputs, blob_info: get_blob_info_from_traces(traces)? } + ExecutorInput { + block_inputs: blocks_inputs, + blob_infos: get_blob_infos_from_traces(traces)?, + batch_version, + } }; Ok(executor_input) diff --git a/prover/bin/host/src/main.rs b/prover/bin/host/src/main.rs index e997852af..88455d978 100644 --- a/prover/bin/host/src/main.rs +++ b/prover/bin/host/src/main.rs @@ -4,7 +4,7 @@ use alloy_provider::{Provider, ProviderBuilder}; use clap::Parser; use morph_prove::{execute::execute_batch, utils::command_args::parse_u64_auto_radix, BatchProver}; use prover_executor_client::types::input::ExecutorInput; -use prover_executor_host::{blob::get_blob_info_from_traces, trace::trace_to_input}; +use prover_executor_host::{blob::get_blob_infos_from_traces, trace::trace_to_input}; use prover_primitives::types::BlockTrace; /// The arguments for the command. @@ -32,6 +32,9 @@ struct Args { /// Whether to save input. #[clap(long)] save_input: bool, + /// Batch header version (0/1 = V0/V1, 2 = V2 multi-blob). + #[clap(long = "batch-version", default_value_t = 0)] + batch_version: u8, } #[tokio::main] @@ -45,14 +48,17 @@ async fn main() { let mut input = if args.use_rpc_db { // Use RPC to fetch state. let provider = ProviderBuilder::new().connect_http(args.rpc.parse().unwrap()).erased(); - execute_batch(1, args.start_block, args.end_block, &provider, true).await.unwrap() + execute_batch(1, args.start_block, args.end_block, &provider, true, args.batch_version) + .await + .unwrap() } else { // Use local traces file. let block_traces = &mut load_trace(&args.block_path); let blocks_inputs = block_traces.iter().map(trace_to_input).collect::>(); ExecutorInput { block_inputs: blocks_inputs, - blob_info: get_blob_info_from_traces(block_traces).unwrap(), + blob_infos: get_blob_infos_from_traces(block_traces).unwrap(), + batch_version: args.batch_version, } }; if args.save_input { diff --git a/prover/bin/server/src/queue.rs b/prover/bin/server/src/queue.rs index 0598f7a8e..ee9fcc4a7 100644 --- a/prover/bin/server/src/queue.rs +++ b/prover/bin/server/src/queue.rs @@ -7,6 +7,7 @@ use std::{ }; use crate::{PROVER_L2_RPC, PROVER_PROOF_DIR, PROVER_USE_RPC_DB, PROVE_RESULT, PROVE_TIME}; +use alloy_primitives::Keccak256; use alloy_provider::{DynProvider, Provider, ProviderBuilder}; use morph_prove::{evm::EvmProofFixture, execute::execute_batch, BatchProver, DefaultClient}; use prover_executor_client::{types::input::ExecutorInput, BlobVerifier, EVMVerifier}; @@ -23,6 +24,12 @@ pub struct ProveRequest { pub end_block: u64, pub rpc: String, pub shadow: Option, + #[serde(default = "default_batch_version")] + pub batch_version: u8, +} + +fn default_batch_version() -> u8 { + 2 } /// The prover that processes prove requests from a queue. @@ -50,7 +57,7 @@ impl Prover { tokio::time::sleep(Duration::from_millis(12000)).await; // Step1. Get request from queue - let (batch_index, start_block, end_block, shadow) = match self + let (batch_index, start_block, end_block, shadow, batch_version) = match self .prove_queue .lock() .await @@ -58,17 +65,19 @@ impl Prover { { Some(req) => { log::info!( - "received prove request, batch index = {:#?}, blocks len = {:#?}, start_block = {:#?}, shadow = {:#?}", + "received prove request, batch index = {:#?}, blocks len = {:#?}, start_block = {:#?}, shadow = {:#?}, batch_version = {}", req.batch_index, req.end_block - req.start_block + 1, req.start_block, req.shadow, + req.batch_version, ); ( req.batch_index, req.start_block, req.end_block, req.shadow.unwrap_or_default(), + req.batch_version, ) } None => { @@ -78,19 +87,26 @@ impl Prover { }; // Step2. Generate ExecutorInput - let mut input = - match gen_client_input(batch_index, start_block, end_block, &self.provider).await { - Ok(input) => input, - Err(e) => { - log::error!( - "Generate ExecutorInput error for batch-{:?}, error: {:?}", - batch_index, - e - ); - PROVE_RESULT.set(2); - continue; - } - }; + let mut input = match gen_client_input( + batch_index, + start_block, + end_block, + &self.provider, + batch_version, + ) + .await + { + Ok(input) => input, + Err(e) => { + log::error!( + "Generate ExecutorInput error for batch-{:?}, error: {:?}", + batch_index, + e + ); + PROVE_RESULT.set(2); + continue; + } + }; // Step3. Generate evm proof log::info!("Generate evm proof"); @@ -123,10 +139,18 @@ async fn gen_client_input( start_block: u64, end_block: u64, provider: &DynProvider, + batch_version: u8, ) -> Result { // Step1. Get ExecutorInput - let executor_input = - execute_batch(batch_index, start_block, end_block, provider, *PROVER_USE_RPC_DB).await?; + let executor_input = execute_batch( + batch_index, + start_block, + end_block, + provider, + *PROVER_USE_RPC_DB, + batch_version, + ) + .await?; let proof_dir = PathBuf::from(PROVER_PROOF_DIR.to_string()).join(format!("batch_{batch_index}")); std::fs::create_dir_all(&proof_dir).expect("failed to create proof path"); @@ -136,15 +160,24 @@ async fn gen_client_input( // Step3. Save batch header or error info. if let Ok(batch_info) = verify_result { - let (versioned_hash, _) = BlobVerifier::verify(&executor_input.blob_info)?; - // Save batch_header - // | batch_data_hash | versioned_hash | sequencer_root | - // |-----------------|----------------|----------------| - // | bytes32 | bytes32 | bytes32 | + let (versioned_hashes, _) = BlobVerifier::verify_blobs(&executor_input.blob_infos)?; + // Compute the blob input for the batch header: + // V2: blobHashesHash = keccak256(hash[0] || ... || hash[N-1]) + // V0/V1: just the single versioned hash + let blob_input = if batch_version >= 2 { + let mut blob_hasher = Keccak256::new(); + for h in &versioned_hashes { + blob_hasher.update(h.as_slice()); + } + blob_hasher.finalize() + } else { + versioned_hashes[0] + }; + // Save batch_header_ex (uniform for all versions): + // | data_hash(32) | blob_input(32) | seqSetVerifyHash(32) | (96 bytes) let mut batch_header: Vec = Vec::with_capacity(96); batch_header.extend_from_slice(&batch_info.data_hash().0); - batch_header.extend_from_slice(&versioned_hash.0); - batch_header.extend_from_slice(&batch_info.sequencer_root().0); + batch_header.extend_from_slice(&blob_input.0); batch_header.extend_from_slice(&batch_info.sequencer_root().0); let mut batch_file = File::create(proof_dir.join("batch_header.data"))?; batch_file.write_all(&batch_header[..]).expect("failed to batch_header"); diff --git a/prover/bin/shadow-prove/src/execute.rs b/prover/bin/shadow-prove/src/execute.rs index a35276ee9..1a5142e6e 100644 --- a/prover/bin/shadow-prove/src/execute.rs +++ b/prover/bin/shadow-prove/src/execute.rs @@ -7,7 +7,7 @@ use prover_executor_client::{ verify, }; use prover_executor_host::{ - blob::{get_blob_info_from_blocks, get_blob_info_from_traces}, + blob::{get_blob_infos_from_blocks, get_blob_infos_from_traces}, execute::HostExecutor, trace::trace_to_input, utils::{assemble_block_input, query_block, HostExecutorOutput}, @@ -44,6 +44,7 @@ pub async fn execute( pub async fn try_execute_batch( batch: &BatchInfo, provider: &DynProvider, + batch_version: u8, ) -> Result { let client_input = if *SHADOW_EXECUTE_USE_RPC_DB { let start_block = batch.start_block; @@ -61,9 +62,10 @@ pub async fn try_execute_batch( ExecutorInput { block_inputs: blocks_inputs.clone(), - blob_info: get_blob_info_from_blocks( + blob_infos: get_blob_infos_from_blocks( &blocks_inputs.iter().map(|input| input.current_block.clone()).collect::>(), )?, + batch_version, } } else { // Use sequencer's trace rpc. @@ -71,7 +73,11 @@ pub async fn try_execute_batch( &mut get_block_traces(batch.batch_index, batch.start_block, batch.end_block, provider) .await?; let blocks_inputs = traces.iter().map(trace_to_input).collect::>(); - ExecutorInput { block_inputs: blocks_inputs, blob_info: get_blob_info_from_traces(traces)? } + ExecutorInput { + block_inputs: blocks_inputs, + blob_infos: get_blob_infos_from_traces(traces)?, + batch_version, + } }; verify(client_input.clone()).context("native execution failed") @@ -156,6 +162,7 @@ mod tests { try_execute_batch( &BatchInfo { batch_index: 1, start_block: 53, end_block: 54, total_txn: 1 }, &provider, + 0, ) .await .unwrap(); diff --git a/prover/bin/shadow-prove/src/main.rs b/prover/bin/shadow-prove/src/main.rs index 71487806a..a6cf82946 100644 --- a/prover/bin/shadow-prove/src/main.rs +++ b/prover/bin/shadow-prove/src/main.rs @@ -75,16 +75,18 @@ async fn main() { if *SHADOW_EXECUTE { log::info!(">Start shadow execute batch: {:#?}", batch_info.batch_index); // Execute batch - let offchain_batch_pi = match try_execute_batch(&batch_info, &l2_provider).await { - Ok(pi) => { - // Update the latest processed batch index - pi - } - Err(e) => { - log::error!("execute_batch error: {:?}", e); - continue; - } - }; + let batch_version = batch_header.first().copied().unwrap_or(0); + let offchain_batch_pi = + match try_execute_batch(&batch_info, &l2_provider, batch_version).await { + Ok(pi) => { + // Update the latest processed batch index + pi + } + Err(e) => { + log::error!("execute_batch error: {:?}", e); + continue; + } + }; let onchain_batch_pi = batch_syncer_exec.calc_batch_pi(chain_id, &batch_header).unwrap_or_default(); if offchain_batch_pi != onchain_batch_pi { @@ -317,7 +319,9 @@ async fn test_shadow() { let (batch_info, batch_header) = batch_syncer.get_specified_batch(batch_num).await.unwrap().unwrap(); - let offchain_batch_pi = try_execute_batch(&batch_info, &l2_provider).await.unwrap(); + let batch_version = batch_header.first().copied().unwrap_or(0); + let offchain_batch_pi = + try_execute_batch(&batch_info, &l2_provider, batch_version).await.unwrap(); let onchain_batch_pi = batch_syncer.calc_batch_pi(chain_id, &batch_header).unwrap_or_default(); if offchain_batch_pi != onchain_batch_pi { diff --git a/prover/bin/shadow-prove/src/shadow_prove.rs b/prover/bin/shadow-prove/src/shadow_prove.rs index 4ed15863f..3bd42594a 100644 --- a/prover/bin/shadow-prove/src/shadow_prove.rs +++ b/prover/bin/shadow-prove/src/shadow_prove.rs @@ -20,6 +20,8 @@ pub struct ProveRequest { pub end_block: u64, pub rpc: String, pub shadow: bool, + #[serde(default)] + pub batch_version: u8, } #[derive(Serialize, Deserialize, Debug)] @@ -122,6 +124,7 @@ where break; } } + let batch_version = prove_info.batch_header.first().copied().unwrap_or(0); // Request the proverServer to prove. let request = ProveRequest { @@ -130,6 +133,7 @@ where end_block: prove_info.batch_info.end_block, rpc: l2_rpc.to_owned(), shadow: false, + batch_version, }; let rt = tokio::task::spawn_blocking(move || { util::call_prover(serde_json::to_string(&request).unwrap(), "/prove_batch") diff --git a/prover/bin/shadow-prove/src/shadow_rollup.rs b/prover/bin/shadow-prove/src/shadow_rollup.rs index a862ed49e..742eed522 100644 --- a/prover/bin/shadow-prove/src/shadow_rollup.rs +++ b/prover/bin/shadow-prove/src/shadow_rollup.rs @@ -447,21 +447,26 @@ where chain_id: u64, batch_header: &Bytes, ) -> Result { + let version = batch_header.first().copied().unwrap_or(0); + let prev_state_root: &[u8] = batch_header.get(89..121).unwrap_or_default(); let post_state_root: &[u8] = batch_header.get(121..153).unwrap_or_default(); let withdrawal_root: &[u8] = batch_header.get(153..185).unwrap_or_default(); let data_hash: &[u8] = batch_header.get(25..57).unwrap_or_default(); - let blob_versioned_hash: &[u8] = batch_header.get(57..89).unwrap_or_default(); let sequencer_set_verify_hash: &[u8] = batch_header.get(185..217).unwrap_or_default(); + // All versions: blob input at offset 57 (aggregated hash for V2, versioned hash for V0/V1) + let blob_input: &[u8] = batch_header.get(57..89).unwrap_or_default(); + log::info!( - "calc_batch_pi, prevStateRoot = {:?}, postStateRoot = {:?}, withdrawalRoot = {:?}, - dataHash = {:?}, blobVersionedHash = {:?}, sequencerSetVerifyHash = {:?}", + "calc_batch_pi, version = {}, prevStateRoot = {:?}, postStateRoot = {:?}, withdrawalRoot = {:?}, + dataHash = {:?}, blobInput = {:?}, sequencerSetVerifyHash = {:?}", + version, hex::encode_prefixed(prev_state_root), hex::encode_prefixed(post_state_root), hex::encode_prefixed(withdrawal_root), hex::encode_prefixed(data_hash), - hex::encode_prefixed(blob_versioned_hash), + hex::encode_prefixed(blob_input), hex::encode_prefixed(sequencer_set_verify_hash), ); let mut hasher = Keccak256::new(); @@ -471,7 +476,7 @@ where hasher.update(withdrawal_root); hasher.update(sequencer_set_verify_hash); hasher.update(data_hash); - hasher.update(blob_versioned_hash); + hasher.update(blob_input); Ok(hasher.finalize()) } } diff --git a/prover/bin/shadow-prove/src/util.rs b/prover/bin/shadow-prove/src/util.rs index c84e05480..4858f7e3b 100644 --- a/prover/bin/shadow-prove/src/util.rs +++ b/prover/bin/shadow-prove/src/util.rs @@ -61,6 +61,7 @@ async fn test_call_prover() { end_block: 107, rpc: "http://localhost:3030".to_string(), shadow: true, + batch_version: 0, }; let rt = tokio::task::spawn_blocking(move || { diff --git a/prover/crates/executor/client/src/lib.rs b/prover/crates/executor/client/src/lib.rs index aeb2595db..475f350e8 100644 --- a/prover/crates/executor/client/src/lib.rs +++ b/prover/crates/executor/client/src/lib.rs @@ -10,7 +10,7 @@ pub const EVM_VERIFY: &str = "evm verify"; pub fn verify(input: ExecutorInput) -> Result { // Verify DA - let (versioned_hash, batch_data_from_blob) = BlobVerifier::verify(&input.blob_info)?; + let (versioned_hashes, batch_data_from_blob) = BlobVerifier::verify_blobs(&input.blob_infos)?; let batch_data_from_blocks = get_blob_data_from_blocks( &input.block_inputs.iter().map(|input| input.current_block.clone()).collect::>(), ); @@ -21,19 +21,24 @@ pub fn verify(input: ExecutorInput) -> Result { // Verify EVM exec. let batch_info = profile_report!(EVM_VERIFY, { EVMVerifier::verify(input.block_inputs) })?; - // Calc public input hash. + // Calc public input hash based on version. #[cfg(not(target_os = "zkvm"))] log::info!( "cacl pi hash, prevStateRoot = {:?}, postStateRoot = {:?}, withdrawalRoot = {:?}, - dataHash = {:?}, blobVersionedHash = {:?}, sequencerSetVerifyHash = {:?}", + dataHash = {:?}, blobVersionedHashes = {:?}, sequencerSetVerifyHash = {:?}, batch_version = {}", hex::encode(batch_info.prev_state_root().as_slice()), hex::encode(batch_info.post_state_root().as_slice()), hex::encode(batch_info.withdraw_root().as_slice()), hex::encode(batch_info.data_hash().as_slice()), - hex::encode(versioned_hash.as_slice()), + versioned_hashes.iter().map(|h| hex::encode(h.as_slice())).collect::>(), hex::encode(batch_info.sequencer_root().as_slice()), + input.batch_version, ); - let public_input_hash = batch_info.public_input_hash(&versioned_hash); + let public_input_hash = if input.batch_version >= 2 { + batch_info.public_input_hash_v2(&versioned_hashes) + } else { + batch_info.public_input_hash(&versioned_hashes[0]) + }; #[cfg(not(target_os = "zkvm"))] log::info!("public input hash: {public_input_hash:?}"); Ok(B256::from_slice(public_input_hash.as_slice())) diff --git a/prover/crates/executor/client/src/types/batch.rs b/prover/crates/executor/client/src/types/batch.rs index 1f4f50ce5..5cb7b7c9a 100644 --- a/prover/crates/executor/client/src/types/batch.rs +++ b/prover/crates/executor/client/src/types/batch.rs @@ -77,11 +77,42 @@ impl BatchInfo { hasher.finalize() } + /// V2 public input hash: uses keccak256(hash[0] || ... || hash[N-1]) as blob input + pub fn public_input_hash_v2(&self, blob_hashes: &[B256]) -> B256 { + let mut blob_hasher = Keccak256::new(); + for h in blob_hashes { + blob_hasher.update(h.as_slice()); + } + let blob_hashes_hash: B256 = blob_hasher.finalize(); + + let mut hasher = Keccak256::new(); + hasher.update(self.chain_id.to_be_bytes()); + hasher.update(self.prev_state_root.as_slice()); + hasher.update(self.post_state_root.as_slice()); + hasher.update(self.withdraw_root.unwrap().as_slice()); + hasher.update(self.sequencer_root.unwrap().as_slice()); + hasher.update(self.data_hash.as_slice()); + hasher.update(blob_hashes_hash.as_slice()); + hasher.finalize() + } + /// Chain ID of this chunk pub fn chain_id(&self) -> u64 { self.chain_id } + #[cfg(test)] + fn test_instance(chain_id: u64) -> Self { + BatchInfo { + chain_id, + prev_state_root: B256::ZERO, + post_state_root: B256::ZERO, + withdraw_root: Some(B256::ZERO), + sequencer_root: Some(B256::ZERO), + data_hash: B256::ZERO, + } + } + /// State root before this chunk pub fn prev_state_root(&self) -> B256 { self.prev_state_root @@ -107,3 +138,118 @@ impl BatchInfo { self.data_hash } } + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::keccak256; + + // LAYER_2_CHAIN_ID used in Rollup.sol test environment + const TEST_CHAIN_ID: u64 = 53077; + + fn make_hash(val: u64) -> B256 { + let mut b = [0u8; 32]; + b[24..].copy_from_slice(&val.to_be_bytes()); + B256::from(b) + } + + /// V2 aggregated hash for a single blob: keccak256(h0) != h0 (not backward-compatible with V1). + #[test] + fn test_public_input_hash_v2_single_blob_differs_from_v1() { + let batch = BatchInfo::test_instance(TEST_CHAIN_ID); + let h0 = make_hash(0xBEEF); + + let v1_hash = batch.public_input_hash(&h0); + let v2_hash = batch.public_input_hash_v2(&[h0]); + + assert_ne!(v1_hash, v2_hash, "V2 single-blob must differ from V1"); + } + + /// V2 aggregated hash for two blobs: keccak256(h0 || h1) matches contract formula. + #[test] + fn test_public_input_hash_v2_two_blobs_matches_contract() { + let batch = BatchInfo::test_instance(TEST_CHAIN_ID); + let h0 = make_hash(0xAAAA); + let h1 = make_hash(0xBBBB); + + // Replicate contract formula: aggregatedBlobHash = keccak256(h0 || h1) + let mut concat = [0u8; 64]; + concat[..32].copy_from_slice(h0.as_slice()); + concat[32..].copy_from_slice(h1.as_slice()); + let aggregated = keccak256(&concat); + + // V2 public input uses aggregated as blob input + let mut hasher = Keccak256::new(); + hasher.update(TEST_CHAIN_ID.to_be_bytes()); + hasher.update(B256::ZERO.as_slice()); // prev_state_root + hasher.update(B256::ZERO.as_slice()); // post_state_root + hasher.update(B256::ZERO.as_slice()); // withdraw_root + hasher.update(B256::ZERO.as_slice()); // sequencer_root + hasher.update(B256::ZERO.as_slice()); // data_hash + hasher.update(aggregated.as_slice()); + let expected: B256 = hasher.finalize(); + + let result = batch.public_input_hash_v2(&[h0, h1]); + assert_eq!(result, expected, "V2 two-blob hash must match contract formula"); + } + + /// V2 aggregated hash for three blobs: keccak256(h0 || h1 || h2). + #[test] + fn test_public_input_hash_v2_three_blobs() { + let batch = BatchInfo::test_instance(TEST_CHAIN_ID); + let h0 = make_hash(0xAAAA); + let h1 = make_hash(0xBBBB); + let h2 = make_hash(0xCCCC); + + let mut concat = [0u8; 96]; + concat[..32].copy_from_slice(h0.as_slice()); + concat[32..64].copy_from_slice(h1.as_slice()); + concat[64..].copy_from_slice(h2.as_slice()); + let aggregated = keccak256(&concat); + + let mut hasher = Keccak256::new(); + hasher.update(TEST_CHAIN_ID.to_be_bytes()); + hasher.update(B256::ZERO.as_slice()); + hasher.update(B256::ZERO.as_slice()); + hasher.update(B256::ZERO.as_slice()); + hasher.update(B256::ZERO.as_slice()); + hasher.update(B256::ZERO.as_slice()); + hasher.update(aggregated.as_slice()); + let expected: B256 = hasher.finalize(); + + let result = batch.public_input_hash_v2(&[h0, h1, h2]); + assert_eq!(result, expected, "V2 three-blob hash must match contract formula"); + } + + /// V2 aggregated hash is order-sensitive: (h0,h1) != (h1,h0). + #[test] + fn test_public_input_hash_v2_order_sensitive() { + let batch = BatchInfo::test_instance(TEST_CHAIN_ID); + let h0 = make_hash(0xAAAA); + let h1 = make_hash(0xBBBB); + + let fwd = batch.public_input_hash_v2(&[h0, h1]); + let rev = batch.public_input_hash_v2(&[h1, h0]); + assert_ne!(fwd, rev, "V2 aggregated hash must be order-sensitive"); + } + + /// V2 and V1 produce the same result only when blob_hashes_hash accidentally equals + /// the raw versioned hash — which should never happen in practice. + /// This test confirms the structural difference by construction. + #[test] + fn test_public_input_hash_v2_vs_v1_structural_difference() { + let batch = BatchInfo::test_instance(TEST_CHAIN_ID); + let h0 = make_hash(0x1234); + + // V1: uses h0 directly as blob input + let v1 = batch.public_input_hash(&h0); + // V2: uses keccak256(h0) as blob input — structurally different + let v2 = batch.public_input_hash_v2(&[h0]); + assert_ne!(v1, v2); + + // Confirm: if we manually pass keccak256(h0) into V1, it matches V2 + let agg = keccak256(h0.as_slice()); + let v1_with_agg = batch.public_input_hash(&agg); + assert_eq!(v1_with_agg, v2, "V2 is equivalent to V1 with pre-aggregated hash"); + } +} diff --git a/prover/crates/executor/client/src/types/blob.rs b/prover/crates/executor/client/src/types/blob.rs index 2d0ef80a2..665695fc9 100644 --- a/prover/crates/executor/client/src/types/blob.rs +++ b/prover/crates/executor/client/src/types/blob.rs @@ -9,25 +9,35 @@ pub const MAGIC_NUM: u32 = 0xFD2F_B528; /// evaluationform. pub const BLOB_WIDTH: usize = 4096; -const MAX_BLOB_TX_PAYLOAD_SIZE: usize = 131072; // 131072 = 4096 * 32 = 1024 * 4 * 32 = 128kb - #[derive(Clone, Debug)] pub struct BlobData {} -pub fn get_origin_batch(blob_data: &[u8]) -> Result, anyhow::Error> { - // Decode blob, recovering BLS12-381 scalars. - let mut batch_data = vec![0u8; MAX_BLOB_TX_PAYLOAD_SIZE]; - for i in 0..4096 { +/// Decode a single blob's BLS12-381 field elements into raw bytes (4096 x 31 bytes). +/// Does NOT decompress — call [`decompress_batch`] on the concatenated output of all blobs. +pub fn decode_blob_scalars(blob_data: &[u8]) -> Result, anyhow::Error> { + let mut chunk = vec![0u8; BLOB_WIDTH * 31]; + for i in 0..BLOB_WIDTH { if blob_data[i * 32] != 0 { - return Err(anyhow!(format!( + return Err(anyhow!( "Invalid blob, found non-zero high order byte {:x} of field element {}", blob_data[i * 32], i - ))); + )); } - batch_data[i * 31..i * 31 + 31].copy_from_slice(&blob_data[i * 32 + 1..i * 32 + 32]); + chunk[i * 31..i * 31 + 31].copy_from_slice(&blob_data[i * 32 + 1..i * 32 + 32]); } - decompress_batch(&batch_data) + Ok(chunk) +} + +/// Alias for [`decode_blob_scalars`] — kept for backward compatibility. +pub fn unpack_blob(blob_data: &[u8]) -> Result, anyhow::Error> { + decode_blob_scalars(blob_data) +} + +/// Decode a single blob's scalars and immediately decompress (single-blob / V0/V1 path). +pub fn get_origin_batch(blob_data: &[u8]) -> Result, anyhow::Error> { + let chunk = decode_blob_scalars(blob_data)?; + decompress_batch(&chunk) } pub fn decompress_batch(compressed_batch: &[u8]) -> Result, anyhow::Error> { diff --git a/prover/crates/executor/client/src/types/input.rs b/prover/crates/executor/client/src/types/input.rs index 354563783..4ac2fd13b 100644 --- a/prover/crates/executor/client/src/types/input.rs +++ b/prover/crates/executor/client/src/types/input.rs @@ -69,7 +69,9 @@ impl BlockInput { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExecutorInput { pub block_inputs: Vec, - pub blob_info: BlobInfo, + pub blob_infos: Vec, + #[serde(default)] + pub batch_version: u8, } #[cfg(test)] diff --git a/prover/crates/executor/client/src/verifier/blob_verifier.rs b/prover/crates/executor/client/src/verifier/blob_verifier.rs index d65ef9cb4..729675c8d 100644 --- a/prover/crates/executor/client/src/verifier/blob_verifier.rs +++ b/prover/crates/executor/client/src/verifier/blob_verifier.rs @@ -1,18 +1,49 @@ -use crate::types::{blob::get_origin_batch, input::BlobInfo}; +use crate::types::{ + blob::{decode_blob_scalars, decompress_batch, get_origin_batch}, + input::BlobInfo, +}; use anyhow::anyhow; use kzg_rs::{get_kzg_settings, Blob as KzgRsBlob, Bytes48}; use prover_primitives::B256; use sha2::{Digest as _, Sha256}; -// use Verifier; pub struct BlobVerifier; impl BlobVerifier { + /// Verify multiple blobs: + /// - KZG-verify each blob and decode its BLS scalars (no decompression) + /// - Concatenate all raw scalar bytes, then decompress once + /// + /// Returns `(versioned_hashes, decompressed_batch_data)`. + pub fn verify_blobs(blob_infos: &[BlobInfo]) -> Result<(Vec, Vec), anyhow::Error> { + let mut hashes = Vec::new(); + let mut raw_bytes = Vec::new(); + for info in blob_infos { + let (hash, raw) = Self::verify_raw(info)?; + hashes.push(hash); + raw_bytes.extend(raw); + } + let batch_data = decompress_batch(&raw_bytes)?; + Ok((hashes, batch_data)) + } + + /// KZG-verify a single blob and unpack + decompress its payload (V0/V1 single-blob path). pub fn verify(blob_info: &BlobInfo) -> Result<(B256, Vec), anyhow::Error> { - // decode + let hash = Self::verify_kzg(blob_info)?; let origin_batch = get_origin_batch(&blob_info.blob_data)?; + Ok((hash, origin_batch)) + } - // verify kzg + /// KZG-verify a single blob and decode its BLS scalars without decompression. + /// Returns `(versioned_hash, raw_scalar_bytes)`. + pub fn verify_raw(blob_info: &BlobInfo) -> Result<(B256, Vec), anyhow::Error> { + let hash = Self::verify_kzg(blob_info)?; + let raw = decode_blob_scalars(&blob_info.blob_data)?; + Ok((hash, raw)) + } + + /// KZG-verify a blob's commitment/proof and return its versioned hash. + fn verify_kzg(blob_info: &BlobInfo) -> Result { let versioned_hash = kzg_to_versioned_hash(&blob_info.commitment); let blob = KzgRsBlob::from_slice(&blob_info.blob_data).unwrap(); let commitment = Bytes48::from_slice(&blob_info.commitment).unwrap(); @@ -29,8 +60,7 @@ impl BlobVerifier { "verify_blob_kzg_proof successfully, versioned_hash: {:?}", B256::from_slice(&versioned_hash) ); - - Ok((B256::from_slice(&versioned_hash), origin_batch)) + Ok(B256::from_slice(&versioned_hash)) } } diff --git a/prover/crates/executor/host/src/blob.rs b/prover/crates/executor/host/src/blob.rs index 7ff260220..a7bb38a94 100644 --- a/prover/crates/executor/host/src/blob.rs +++ b/prover/crates/executor/host/src/blob.rs @@ -16,6 +16,9 @@ const BLOB_WIDTH: usize = 4096; /// The bytes len of one blob. const BLOB_DATA_SIZE: usize = BLOB_WIDTH * N_BYTES_U256; +/// Maximum payload bytes that fit in a single blob (4096 field elements × 31 usable bytes each). +const MAX_BLOB_BYTES_SIZE: usize = BLOB_WIDTH * (N_BYTES_U256 - 1); // 4096 * 31 = 126,976 + // Get blob info from L2 blocks pub fn get_blob_info_from_blocks(blocks: &Vec) -> Result { // Assemble batch data from block header and transactions. @@ -42,6 +45,58 @@ pub fn get_blob_info_from_traces( populate_kzg(&blob_data) } +/// Encode batch data from L2 blocks into multiple blob infos (one per 126,976-byte chunk). +pub fn get_blob_infos_from_blocks(blocks: &[L2Block]) -> Result> { + let batch_data = get_blob_data_from_blocks(&blocks.to_vec()); + let compressed = compresse_batch(batch_data.as_slice())?; + encode_multi_blob(compressed) +} + +/// Encode batch data from block traces into multiple blob infos. +pub fn get_blob_infos_from_traces(traces: &[BlockTrace]) -> Result> { + let batch_data = get_blob_data_from_traces(&traces.to_vec()); + let compressed = compresse_batch(batch_data.as_slice())?; + encode_multi_blob(compressed) +} + +/// Split compressed data into N blobs, each holding up to MAX_BLOB_BYTES_SIZE bytes. +fn encode_multi_blob(compressed: Vec) -> Result> { + if compressed.is_empty() { + return Ok(vec![populate_kzg(&[0u8; BLOB_DATA_SIZE])?]); + } + let blob_count = compressed.len().div_ceil(MAX_BLOB_BYTES_SIZE); + let mut infos = Vec::with_capacity(blob_count); + for i in 0..blob_count { + let start = i * MAX_BLOB_BYTES_SIZE; + let end = std::cmp::min(start + MAX_BLOB_BYTES_SIZE, compressed.len()); + let chunk = &compressed[start..end]; + let blob_data = encode_blob_from_bytes(chunk)?; + infos.push(populate_kzg(&blob_data)?); + } + Ok(infos) +} + +/// Encode a byte slice (already compressed) into an EIP-4844 blob payload (131072 bytes). +/// +/// Packs bytes into 4096 BLS12-381 field elements with the MSB forced to 0x00. +pub fn encode_blob_from_bytes(data: &[u8]) -> Result<[u8; BLOB_DATA_SIZE]> { + ensure!( + data.len() <= MAX_BLOB_BYTES_SIZE, + "data size {} exceeds max blob capacity {}", + data.len(), + MAX_BLOB_BYTES_SIZE + ); + let mut coefficients = [[0u8; N_BYTES_U256]; BLOB_WIDTH]; + for (i, byte) in data.iter().enumerate() { + coefficients[i / 31][1 + (i % 31)] = *byte; + } + let mut blob_bytes = [0u8; BLOB_DATA_SIZE]; + for (index, value) in coefficients.iter().enumerate() { + blob_bytes[index * 32..(index + 1) * 32].copy_from_slice(value.as_slice()); + } + Ok(blob_bytes) +} + /// Encode `tx_bytes` into an EIP-4844 blob payload (131072 bytes). /// /// The blob represents `BLOB_WIDTH = 4096` BLS12-381 scalar-field (`Fr`) elements (32 bytes each). @@ -60,13 +115,11 @@ pub fn encode_blob(tx_bytes: Vec) -> Result<[u8; 131072]> { } // zstd compresse let compressed_batch = compresse_batch(tx_bytes.as_slice()).context("compress batch failed")?; - let max = BLOB_WIDTH * (N_BYTES_U256 - 1); - ensure!( - compressed_batch.len() <= max, + compressed_batch.len() <= MAX_BLOB_BYTES_SIZE, "compressed batch size {} exceeds the max capacity {}", compressed_batch.len(), - max + MAX_BLOB_BYTES_SIZE ); let mut coefficients = [[0u8; N_BYTES_U256]; BLOB_WIDTH]; diff --git a/prover/crates/executor/host/src/execute.rs b/prover/crates/executor/host/src/execute.rs index 53ee69c0d..690ebaaae 100644 --- a/prover/crates/executor/host/src/execute.rs +++ b/prover/crates/executor/host/src/execute.rs @@ -1,4 +1,4 @@ -use crate::utils::{beneficiary_by_chain_id, query_block, query_state_root, HostExecutorOutput}; +use crate::utils::{beneficiary_by_chain_id, query_block, HostExecutorOutput}; use alloy_provider::{DynProvider, Provider}; use anyhow::{bail, Context}; use prover_executor_core::MorphExecutor; @@ -29,6 +29,7 @@ impl HostExecutor { let block = query_block(block_number, provider) .await .with_context(|| format!("query_block failed for block {block_number}"))?; + let post_state_root = block.header.state_root; // layer2 chain id let chain_id = @@ -40,30 +41,22 @@ impl HostExecutor { // We use a per-chain hardcoded address as the sequencer/beneficiary. let beneficiary = beneficiary_by_chain_id(chain_id); - // mpt root at this block - let disk_root = query_state_root(block_number, provider) - .await - .with_context(|| format!("query_state_root failed for block {block_number}"))?; - // We need a previous block root to initialize the RPC-backed DB. let prev_block_number = block_number .checked_sub(1) .context("HostExecutor::execute_block requires block_number > 0 (needs prev state)")?; - let prev_disk_root = - query_state_root(prev_block_number, provider).await.with_context(|| { - format!("query_state_root failed for prev block {prev_block_number}") - })?; + + let prev_block = query_block(prev_block_number, provider) + .await + .with_context(|| format!("query_block failed for prev block {prev_block_number}"))?; + let prev_state_root = prev_block.header.state_root; let tx_count = block.transactions.len(); let block_num = block.header.number.to::(); // Init DB (RPC-backed, rooted at previous block). - let rpc_db = BasicRpcDb::new( - provider.clone(), - chain_id, - prev_block_number, - prev_disk_root.disk_root, - ); + let rpc_db = + BasicRpcDb::new(provider.clone(), chain_id, prev_block_number, prev_state_root); // Warm up predeployed contract info. load_predeployed_contracts(&rpc_db).await?; @@ -108,7 +101,7 @@ impl HostExecutor { )); state_for_verification.state_root() }; - let expected_state_root = disk_root.disk_root; + let expected_state_root = block.header.state_root; if computed_state_root != expected_state_root { bail!( "Mismatched state root after executing block {block_number}: expected {expected_state_root:?}, got {computed_state_root:?}" @@ -122,8 +115,8 @@ impl HostExecutor { block, state, codes: rpc_db.bytecodes(), - prev_state_root: prev_disk_root.disk_root, - post_state_root: disk_root.disk_root, + prev_state_root, + post_state_root, }) } } diff --git a/prover/crates/executor/host/src/utils.rs b/prover/crates/executor/host/src/utils.rs index 36dd629e3..66d0d7356 100644 --- a/prover/crates/executor/host/src/utils.rs +++ b/prover/crates/executor/host/src/utils.rs @@ -94,17 +94,6 @@ pub fn assemble_block_input( ClientBlockInput { current_block: l2_block, parent_state: state, bytecodes: codes } } -/// Queries the state root at a given block number. -pub async fn query_state_root( - block_number: u64, - provider: &DynProvider, -) -> Result { - provider - .raw_request::<_, DiskRoot>("morph_diskRoot".into(), [format!("{block_number:#x}")]) - .await - .context("morph_diskRoot error") -} - /// Queries the block at a given block number. pub async fn query_block( block_number: u64, @@ -118,17 +107,3 @@ pub async fn query_block( .await .context("eth_getBlockByNumber error") } - -/// Queries the block at a given block number without transactions. -pub async fn query_chain_d( - block_number: u64, - provider: &DynProvider, -) -> Result { - provider - .raw_request::<_, ProverBlock>( - "eth_getBlockByNumber".into(), - [format!("{block_number:#x}")], - ) - .await - .context("eth_getBlockByNumber error") -} diff --git a/prover/rust-toolchain b/prover/rust-toolchain deleted file mode 100644 index cdeba7a2b..000000000 --- a/prover/rust-toolchain +++ /dev/null @@ -1,3 +0,0 @@ -[toolchain] -channel = "1.91.0" -components = ["rustfmt", "clippy"] diff --git a/tx-submitter/batch/batch_storage_test.go b/tx-submitter/batch/batch_storage_test.go deleted file mode 100644 index 9346379a2..000000000 --- a/tx-submitter/batch/batch_storage_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package batch - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "morph-l2/tx-submitter/iface" -) - -func Test_storageBatch(t *testing.T) { - testDB := setupTestDB(t) - cache := NewBatchCache(nil, l1Client, []iface.L2Client{l2Client}, rollupContract, l2Caller, testDB) - err := cache.InitAndSyncFromRollup() - require.NoError(t, err) - - batches, _, err := cache.batchStorage.LoadAllSealedBatches() - require.NoError(t, err) - require.NotNil(t, batches) - t.Log("loaded batches count", len(batches)) -} diff --git a/tx-submitter/batch/blob.go b/tx-submitter/batch/blob.go deleted file mode 100644 index 399b0c15f..000000000 --- a/tx-submitter/batch/blob.go +++ /dev/null @@ -1,210 +0,0 @@ -package batch - -import ( - "bytes" - "encoding/binary" - "errors" - "fmt" - "io" - "morph-l2/node/zstd" - - eth "github.com/morph-l2/go-ethereum/core/types" - "github.com/morph-l2/go-ethereum/crypto/kzg4844" - "github.com/morph-l2/go-ethereum/rlp" -) - -const MaxBlobBytesSize = 4096 * 31 - -var ( - emptyBlob = new(kzg4844.Blob) - emptyBlobCommit, _ = kzg4844.BlobToCommitment(emptyBlob) - emptyBlobProof, _ = kzg4844.ComputeBlobProof(emptyBlob, emptyBlobCommit) -) - -// MakeBlobCanonical converts the raw blob data into the canonical blob representation of 4096 BLSFieldElements. -func MakeBlobCanonical(blobBytes []byte) (b *kzg4844.Blob, err error) { - if len(blobBytes) > MaxBlobBytesSize { - return nil, fmt.Errorf("data is too large for blob. len=%v", len(blobBytes)) - } - offset := 0 - b = new(kzg4844.Blob) - // encode (up to) 31 bytes of remaining input data at a time into the subsequent field element - for i := 0; i < 4096; i++ { - offset += copy(b[i*32+1:i*32+32], blobBytes[offset:]) - if offset == len(blobBytes) { - break - } - } - if offset < len(blobBytes) { - return nil, fmt.Errorf("failed to fit all data into blob. bytes remaining: %v", len(blobBytes)-offset) - } - return -} - -func RetrieveBlobBytes(blob *kzg4844.Blob) ([]byte, error) { - data := make([]byte, MaxBlobBytesSize) - for i := 0; i < 4096; i++ { - if blob[i*32] != 0 { - return nil, fmt.Errorf("invalid blob, found non-zero high order byte %x of field element %d", data[i*32], i) - } - copy(data[i*31:i*31+31], blob[i*32+1:i*32+32]) - } - return data, nil -} - -func makeBlobCommitment(bz []byte) (b kzg4844.Blob, c kzg4844.Commitment, err error) { - blob, err := MakeBlobCanonical(bz) - if err != nil { - return - } - b = *blob - c, err = kzg4844.BlobToCommitment(&b) - if err != nil { - return - } - return -} - -func MakeBlobTxSidecar(blobBytes []byte) (*eth.BlobTxSidecar, error) { - if len(blobBytes) == 0 { - return ð.BlobTxSidecar{ - Blobs: []kzg4844.Blob{*emptyBlob}, - Commitments: []kzg4844.Commitment{emptyBlobCommit}, - Proofs: []kzg4844.Proof{emptyBlobProof}, - }, nil - } - if len(blobBytes) > 2*MaxBlobBytesSize { - return nil, errors.New("only 2 blobs at most is allowed") - } - blobCount := len(blobBytes)/(MaxBlobBytesSize+1) + 1 - var ( - err error - blobs = make([]kzg4844.Blob, blobCount) - commitments = make([]kzg4844.Commitment, blobCount) - ) - switch blobCount { - case 1: - blobs[0], commitments[0], err = makeBlobCommitment(blobBytes) - if err != nil { - return nil, err - } - case 2: - blobs[0], commitments[0], err = makeBlobCommitment(blobBytes[:MaxBlobBytesSize]) - if err != nil { - return nil, err - } - blobs[1], commitments[1], err = makeBlobCommitment(blobBytes[MaxBlobBytesSize:]) - if err != nil { - return nil, err - } - } - return ð.BlobTxSidecar{ - Blobs: blobs, - Commitments: commitments, - }, nil -} - -func CompressBatchBytes(batchBytes []byte) ([]byte, error) { - if len(batchBytes) == 0 { - return nil, nil - } - compressedBatchBytes, err := zstd.CompressBatchBytes(batchBytes) - if err != nil { - return nil, fmt.Errorf("failed to compress batch bytes, err: %w", err) - } - return compressedBatchBytes, nil -} - -func DecodeTxsFromBytes(txsBytes []byte) (eth.Transactions, error) { - reader := bytes.NewReader(txsBytes) - txs := make(eth.Transactions, 0) - for { - var ( - firstByte byte - fullTxBytes []byte - innerTx eth.TxData - err error - ) - if err = binary.Read(reader, binary.BigEndian, &firstByte); err != nil { - // if the blob byte array is completely consumed, then break the loop - if err == io.EOF { - break - } - return nil, err - } - // zero byte is found after valid tx bytes, break the loop - if firstByte == 0 { - break - } - - switch firstByte { - case eth.AccessListTxType: - if err := binary.Read(reader, binary.BigEndian, &firstByte); err != nil { - return nil, err - } - innerTx = new(eth.AccessListTx) - case eth.DynamicFeeTxType: - if err := binary.Read(reader, binary.BigEndian, &firstByte); err != nil { - return nil, err - } - innerTx = new(eth.DynamicFeeTx) - case eth.SetCodeTxType: - if err := binary.Read(reader, binary.BigEndian, &firstByte); err != nil { - return nil, err - } - innerTx = new(eth.SetCodeTx) - case eth.MorphTxType: - if err := binary.Read(reader, binary.BigEndian, &firstByte); err != nil { - return nil, err - } - innerTx = new(eth.MorphTx) - default: - if firstByte <= 0xf7 { // legacy tx first byte must be greater than 0xf7(247) - return nil, fmt.Errorf("not supported tx type: %d", firstByte) - } - innerTx = new(eth.LegacyTx) - } - - // we support the tx types of LegacyTxType/AccessListTxType/DynamicFeeTxType - //if firstByte == eth.AccessListTxType || firstByte == eth.DynamicFeeTxType { - // // the firstByte here is used to indicate tx type, so skip it - // if err := binary.Read(reader, binary.BigEndian, &firstByte); err != nil { - // return nil, err - // } - //} else if firstByte <= 0xf7 { // legacy tx first byte must be greater than 0xf7(247) - // return nil, fmt.Errorf("not supported tx type: %d", firstByte) - //} - fullTxBytes, err = extractInnerTxFullBytes(firstByte, reader) - if err != nil { - return nil, err - } - if err = rlp.DecodeBytes(fullTxBytes, innerTx); err != nil { - return nil, err - } - txs = append(txs, eth.NewTx(innerTx)) - } - return txs, nil -} - -func extractInnerTxFullBytes(firstByte byte, reader io.Reader) ([]byte, error) { - //the occupied byte length for storing the size of the following rlp encoded bytes - sizeByteLen := firstByte - 0xf7 - - // the size of the following rlp encoded bytes - sizeByte := make([]byte, sizeByteLen) - if err := binary.Read(reader, binary.BigEndian, sizeByte); err != nil { - return nil, err - } - size := binary.BigEndian.Uint32(append(make([]byte, 4-len(sizeByte)), sizeByte...)) - - txRaw := make([]byte, size) - if err := binary.Read(reader, binary.BigEndian, txRaw); err != nil { - return nil, err - } - fullTxBytes := make([]byte, 1+uint32(sizeByteLen)+size) - copy(fullTxBytes[:1], []byte{firstByte}) - copy(fullTxBytes[1:1+sizeByteLen], sizeByte) - copy(fullTxBytes[1+sizeByteLen:], txRaw) - - return fullTxBytes, nil -} diff --git a/tx-submitter/flags/flags.go b/tx-submitter/flags/flags.go index 2dedec0fe..cdbf1405d 100644 --- a/tx-submitter/flags/flags.go +++ b/tx-submitter/flags/flags.go @@ -331,6 +331,22 @@ var ( Usage: "Enable seal batch", EnvVar: prefixEnvVar("SEAL_BATCH"), } + + // max blob count per batch + MaxBlobCountFlag = cli.IntFlag{ + Name: "max_blob_count", + Usage: "Maximum number of blobs per batch submission (1-6)", + Value: 6, + EnvVar: prefixEnvVar("MAX_BLOB_COUNT"), + } + + // batch v2 upgrade time + BatchV2UpgradeTimeFlag = cli.Uint64Flag{ + Name: "batch_v2_upgrade_time", + Usage: "Unix timestamp at which V2 multi-blob batch format is activated (0 = disabled)", + Value: 0, + EnvVar: prefixEnvVar("BATCH_V2_UPGRADE_TIME"), + } ) var requiredFlags = []cli.Flag{ @@ -391,6 +407,8 @@ var optionalFlags = []cli.Flag{ BlockNotIncreasedThreshold, SealBatch, + MaxBlobCountFlag, + BatchV2UpgradeTimeFlag, } // Flags contains the list of configuration options available to the binary. diff --git a/tx-submitter/go.mod b/tx-submitter/go.mod index b428cee23..ccf1825d5 100644 --- a/tx-submitter/go.mod +++ b/tx-submitter/go.mod @@ -33,8 +33,6 @@ require ( github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect github.com/ethereum/go-ethereum v1.10.26 // indirect github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 // indirect - github.com/go-kit/kit v0.12.0 // indirect - github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-resty/resty/v2 v2.17.2 // indirect github.com/go-stack/stack v1.8.1 // indirect diff --git a/tx-submitter/go.sum b/tx-submitter/go.sum index 61b3d092a..792063ca1 100644 --- a/tx-submitter/go.sum +++ b/tx-submitter/go.sum @@ -65,14 +65,11 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays= github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4= -github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= -github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= -github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= -github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= diff --git a/tx-submitter/iface/client.go b/tx-submitter/iface/client.go index 6fafffdac..2081355db 100644 --- a/tx-submitter/iface/client.go +++ b/tx-submitter/iface/client.go @@ -38,6 +38,11 @@ type L2Clients struct { Clients []L2Client } +// Len returns the number of configured L2 clients. +func (c *L2Clients) Len() int { + return len(c.Clients) +} + // getFirstClient returns the first available client, or an error if no clients are available func (c *L2Clients) getFirstClient() (L2Client, error) { if len(c.Clients) == 0 { diff --git a/tx-submitter/mock/rollup.go b/tx-submitter/mock/rollup.go index dbf424a59..a9cf25eb6 100644 --- a/tx-submitter/mock/rollup.go +++ b/tx-submitter/mock/rollup.go @@ -1,6 +1,7 @@ package mock import ( + "errors" "math/big" "github.com/morph-l2/go-ethereum/core/types" @@ -87,7 +88,7 @@ func (m *MockRollup) BatchDataStore(opts *bind.CallOpts, batchIndex *big.Int) (s // FilterCommitBatch implements IRollup func (m *MockRollup) FilterCommitBatch(opts *bind.FilterOpts, batchIndex []*big.Int, batchHash [][32]byte) (*bindings.RollupCommitBatchIterator, error) { - return nil, nil + return nil, errors.New("FilterCommitBatch not implemented in mock") } // FilterFinalizeBatch implements IRollup diff --git a/tx-submitter/services/rollup.go b/tx-submitter/services/rollup.go index 4d92869b4..317cbcae0 100644 --- a/tx-submitter/services/rollup.go +++ b/tx-submitter/services/rollup.go @@ -26,7 +26,8 @@ import ( "github.com/morph-l2/go-ethereum/rpc" "morph-l2/bindings/bindings" - "morph-l2/tx-submitter/batch" + "morph-l2/common/batch" + "morph-l2/common/blob" "morph-l2/tx-submitter/constants" "morph-l2/tx-submitter/db" "morph-l2/tx-submitter/event" @@ -83,7 +84,7 @@ type Rollup struct { eventInfoStorage *event.EventInfoStorage reorgDetector iface.IReorgDetector - ChainConfigMap types.ChainBlobConfigs + ChainConfigMap blob.ChainBlobConfigs } func NewRollup( @@ -108,27 +109,38 @@ func NewRollup( ) *Rollup { reorgDetector := NewReorgDetector(l1, metrics) r := &Rollup{ - ctx: ctx, - metrics: metrics, - l1RpcClient: l1RpcClient, - L1Client: l1, - Rollup: rollup, - Staking: staking, - L2Clients: l2Clients, - privKey: priKey, - chainId: chainId, - rollupAddr: rollupAddr, - abi: abi, - rotator: rotator, - cfg: cfg, - signer: ethtypes.LatestSignerForChainID(chainId), - externalRsaPriv: rsaPriv, - batchCache: batch.NewBatchCache(nil, l1, l2Clients, rollup, l2Caller, ldb), + ctx: ctx, + metrics: metrics, + l1RpcClient: l1RpcClient, + L1Client: l1, + Rollup: rollup, + Staking: staking, + L2Clients: l2Clients, + privKey: priKey, + chainId: chainId, + rollupAddr: rollupAddr, + abi: abi, + rotator: rotator, + cfg: cfg, + signer: ethtypes.LatestSignerForChainID(chainId), + externalRsaPriv: rsaPriv, + batchCache: batch.NewBatchCache( + nil, + func(blockTimestamp uint64) bool { + return cfg.BatchV2UpgradeTime > 0 && blockTimestamp >= cfg.BatchV2UpgradeTime + }, + cfg.MaxBlobCount, + l1, + &iface.L2Clients{Clients: l2Clients}, + rollup, + l2Caller, + ldb, + ), ldb: ldb, bm: bm, eventInfoStorage: eventInfoStorage, reorgDetector: reorgDetector, - ChainConfigMap: types.ChainConfigMap, + ChainConfigMap: blob.ChainConfigMap, } if !cfg.SealBatch { fetcher := NewBatchFetcher(l2Clients) @@ -904,7 +916,7 @@ func (r *Rollup) finalize() error { return fmt.Errorf("get gas tip and cap error:%v", err) } - gas, err := r.EstimateGas(r.WalletAddr(), r.rollupAddr, calldata, feecap, tip) + gas, err := r.EstimateGas(r.WalletAddr(), r.rollupAddr, calldata, feecap, tip, nil, nil) if err != nil { log.Warn("estimate finalize tx gas error", "error", err, @@ -1134,8 +1146,16 @@ func (r *Rollup) rollup() error { if err != nil { return fmt.Errorf("pack calldata error:%v", err) } - // Estimate gas for transaction - gas, err := r.EstimateGas(r.WalletAddr(), r.rollupAddr, calldata, gasFeeCap, tip) + // Estimate gas for transaction. + // For blob batches (e.g. V2), include BlobHashes/BlobGasFeeCap so `blobhash(i)` + // is available during eth_estimateGas simulation. + var estimateBlobHashes []common.Hash + var estimateBlobFeeCap *big.Int + if len(rpcRollupBatch.Sidecar.Blobs) > 0 { + estimateBlobHashes = blob.BlobHashes(rpcRollupBatch.Sidecar.Blobs, rpcRollupBatch.Sidecar.Commitments) + estimateBlobFeeCap = blobFee + } + gas, err := r.EstimateGas(r.WalletAddr(), r.rollupAddr, calldata, gasFeeCap, tip, estimateBlobHashes, estimateBlobFeeCap) if err != nil { log.Warn("Estimate gas failed", "batch_index", batchIndex, "error", err) // Use estimation based on L1 message count @@ -1209,23 +1229,23 @@ func (r *Rollup) createRollupTx(batch *eth.RPCRollupBatch, nonce, gas uint64, ti return r.createDynamicFeeTx(nonce, gas, tip, gasFeeCap, calldata) } -func (r *Rollup) createBlobTx(batch *eth.RPCRollupBatch, nonce, gas uint64, tip, gasFeeCap, blobFee *big.Int, calldata []byte, head *ethtypes.Header) (*ethtypes.Transaction, error) { - versionedHashes := types.BlobHashes(batch.Sidecar.Blobs, batch.Sidecar.Commitments) +func (r *Rollup) createBlobTx(rpcBatch *eth.RPCRollupBatch, nonce, gas uint64, tip, gasFeeCap, blobFee *big.Int, calldata []byte, head *ethtypes.Header) (*ethtypes.Transaction, error) { + versionedHashes := blob.BlobHashes(rpcBatch.Sidecar.Blobs, rpcBatch.Sidecar.Commitments) sidecar := ðtypes.BlobTxSidecar{ - Blobs: batch.Sidecar.Blobs, - Commitments: batch.Sidecar.Commitments, + Blobs: rpcBatch.Sidecar.Blobs, + Commitments: rpcBatch.Sidecar.Commitments, } - switch types.DetermineBlobVersion(head, r.chainId.Uint64()) { + switch blob.DetermineBlobVersion(head, r.chainId.Uint64()) { case ethtypes.BlobSidecarVersion0: sidecar.Version = ethtypes.BlobSidecarVersion0 - proof, err := types.MakeBlobProof(sidecar.Blobs, sidecar.Commitments) + proof, err := blob.MakeBlobProof(sidecar.Blobs, sidecar.Commitments) if err != nil { return nil, fmt.Errorf("gen blob proof failed %v", err) } sidecar.Proofs = proof case ethtypes.BlobSidecarVersion1: sidecar.Version = ethtypes.BlobSidecarVersion1 - proof, err := types.MakeCellProof(sidecar.Blobs) + proof, err := blob.MakeCellProof(sidecar.Blobs) if err != nil { return nil, fmt.Errorf("gen cell proof failed %v", err) } @@ -1325,11 +1345,11 @@ func (r *Rollup) GetGasTipAndCap() (*big.Int, *big.Int, *big.Int, *ethtypes.Head var blobFee *big.Int if head.ExcessBlobGas != nil { log.Info("market blob fee info", "excess blob gas", *head.ExcessBlobGas) - blobConfig, exist := types.ChainConfigMap[r.chainId.Uint64()] + blobConfig, exist := blob.ChainConfigMap[r.chainId.Uint64()] if !exist { - blobConfig = types.DefaultBlobConfig + blobConfig = blob.DefaultBlobConfig } - blobFeeDenominator := types.GetBlobFeeDenominator(blobConfig, head.Time) + blobFeeDenominator := blob.GetBlobFeeDenominator(blobConfig, head.Time) blobFee = eip4844.CalcBlobFee(*head.ExcessBlobGas, blobFeeDenominator.Uint64()) // Set to 3x to handle blob market congestion blobFee = new(big.Int).Mul(blobFee, big.NewInt(3)) @@ -1678,10 +1698,10 @@ func (r *Rollup) ReSubmitTx(resend bool, tx *ethtypes.Transaction) (*ethtypes.Tr }) case ethtypes.BlobTxType: sidecar := tx.BlobTxSidecar() - version := types.DetermineBlobVersion(head, r.chainId.Uint64()) + version := blob.DetermineBlobVersion(head, r.chainId.Uint64()) if sidecar != nil { if sidecar.Version == ethtypes.BlobSidecarVersion0 && version == ethtypes.BlobSidecarVersion1 { - err = types.BlobSidecarVersionToV1(sidecar) + err = blob.BlobSidecarVersionToV1(sidecar) if err != nil { return nil, err } @@ -1748,14 +1768,23 @@ func (r *Rollup) IsStaker() (bool, error) { return isStaker, nil } -func (r *Rollup) EstimateGas(from, to common.Address, data []byte, feecap *big.Int, tip *big.Int) (uint64, error) { +func (r *Rollup) EstimateGas( + from, to common.Address, + data []byte, + feecap *big.Int, + tip *big.Int, + blobHashes []common.Hash, + blobGasFeeCap *big.Int, +) (uint64, error) { gas, err := r.L1Client.EstimateGas(context.Background(), ethereum.CallMsg{ - From: from, - To: &to, - GasFeeCap: feecap, - GasTipCap: tip, - Data: data, + From: from, + To: &to, + GasFeeCap: feecap, + GasTipCap: tip, + Data: data, + BlobHashes: blobHashes, + BlobGasFeeCap: blobGasFeeCap, }) if err != nil { return 0, fmt.Errorf("call estimate gas error:%v", err) diff --git a/tx-submitter/types/blob.go b/tx-submitter/types/blob.go deleted file mode 100644 index d0da2b6bc..000000000 --- a/tx-submitter/types/blob.go +++ /dev/null @@ -1,73 +0,0 @@ -package types - -import ( - "crypto/sha256" - - "github.com/morph-l2/go-ethereum/common" - ethtypes "github.com/morph-l2/go-ethereum/core/types" - "github.com/morph-l2/go-ethereum/crypto/kzg4844" -) - -// BlobHashes computes the blob hashes of the given blobs. -func BlobHashes(blobs []kzg4844.Blob, commitments []kzg4844.Commitment) []common.Hash { - hasher := sha256.New() - h := make([]common.Hash, len(commitments)) - for i := range blobs { - h[i] = kzg4844.CalcBlobHashV1(hasher, &commitments[i]) - } - return h -} - -func MakeBlobProof(blobs []kzg4844.Blob, commitment []kzg4844.Commitment) ([]kzg4844.Proof, error) { - proofs := make([]kzg4844.Proof, len(blobs)) - for i := range blobs { - proof, err := kzg4844.ComputeBlobProof(&blobs[i], commitment[i]) - if err != nil { - return nil, err - } - proofs[i] = proof - } - return proofs, nil -} - -func MakeCellProof(blobs []kzg4844.Blob) ([]kzg4844.Proof, error) { - proofs := make([]kzg4844.Proof, 0, len(blobs)*kzg4844.CellProofsPerBlob) - for _, blob := range blobs { - cellProofs, err := kzg4844.ComputeCellProofs(&blob) - if err != nil { - return nil, err - } - proofs = append(proofs, cellProofs...) - } - return proofs, nil -} - -func DetermineBlobVersion(head *ethtypes.Header, chainID uint64) byte { - if head == nil { - return ethtypes.BlobSidecarVersion0 - } - blobConfig, exist := ChainConfigMap[chainID] - if !exist { - blobConfig = DefaultBlobConfig - } - if blobConfig.OsakaTime != nil && blobConfig.IsOsaka(head.Number, head.Time) { - return ethtypes.BlobSidecarVersion1 - } - return ethtypes.BlobSidecarVersion0 -} - -// BlobSidecarVersionToV1 converts the BlobSidecar to version 1, attaching the cell proofs. -func BlobSidecarVersionToV1(sc *ethtypes.BlobTxSidecar) error { - if sc.Version == ethtypes.BlobSidecarVersion1 { - return nil - } - if sc.Version == ethtypes.BlobSidecarVersion0 { - proofs, err := MakeCellProof(sc.Blobs) - if err != nil { - return err - } - sc.Version = ethtypes.BlobSidecarVersion1 - sc.Proofs = proofs - } - return nil -} diff --git a/tx-submitter/types/blob_compat.go b/tx-submitter/types/blob_compat.go new file mode 100644 index 000000000..01152bea9 --- /dev/null +++ b/tx-submitter/types/blob_compat.go @@ -0,0 +1,54 @@ +package types + +import ( + "math/big" + + "morph-l2/common/blob" + + "github.com/morph-l2/go-ethereum/common" + ethtypes "github.com/morph-l2/go-ethereum/core/types" + "github.com/morph-l2/go-ethereum/crypto/kzg4844" +) + +type BlobFeeConfig = blob.BlobFeeConfig +type BlobConfig = blob.BlobConfig +type ChainBlobConfigs = blob.ChainBlobConfigs + +var ( + ChainConfigMap = blob.ChainConfigMap + DefaultBlobConfig = blob.DefaultBlobConfig + MainnetChainConfig = blob.MainnetChainConfig + HoodiChainConfig = blob.HoodiChainConfig + DevnetChainConfig = blob.DevnetChainConfig + DefaultCancunBlobConfig = blob.DefaultCancunBlobConfig + DefaultPragueBlobConfig = blob.DefaultPragueBlobConfig + DefaultOsakaBlobConfig = blob.DefaultOsakaBlobConfig + DefaultBPO1BlobConfig = blob.DefaultBPO1BlobConfig + DefaultBPO2BlobConfig = blob.DefaultBPO2BlobConfig + DefaultBPO3BlobConfig = blob.DefaultBPO3BlobConfig + DefaultBPO4BlobConfig = blob.DefaultBPO4BlobConfig +) + +func GetBlobFeeDenominator(blobFeeConfig *BlobFeeConfig, blockTime uint64) *big.Int { + return blob.GetBlobFeeDenominator(blobFeeConfig, blockTime) +} + +func BlobHashes(blobs []kzg4844.Blob, commitments []kzg4844.Commitment) []common.Hash { + return blob.BlobHashes(blobs, commitments) +} + +func MakeBlobProof(blobs []kzg4844.Blob, commitment []kzg4844.Commitment) ([]kzg4844.Proof, error) { + return blob.MakeBlobProof(blobs, commitment) +} + +func MakeCellProof(blobs []kzg4844.Blob) ([]kzg4844.Proof, error) { + return blob.MakeCellProof(blobs) +} + +func DetermineBlobVersion(head *ethtypes.Header, chainID uint64) byte { + return blob.DetermineBlobVersion(head, chainID) +} + +func BlobSidecarVersionToV1(sc *ethtypes.BlobTxSidecar) error { + return blob.BlobSidecarVersionToV1(sc) +} diff --git a/tx-submitter/types/blob_params.go b/tx-submitter/types/blob_params.go deleted file mode 100644 index 4ebe43a41..000000000 --- a/tx-submitter/types/blob_params.go +++ /dev/null @@ -1,103 +0,0 @@ -package types - -import ( - "math/big" -) - -var ( - DefaultBlobConfig = HoodiChainConfig - - ChainConfigMap = ChainBlobConfigs{ - 1: MainnetChainConfig, - 560048: HoodiChainConfig, - 900: DevnetChainConfig, - } -) - -func newUint64(val uint64) *uint64 { return &val } - -type ChainBlobConfigs map[uint64]*BlobFeeConfig - -var ( - // MainnetChainConfig is the chain parameters to run a node on the main network. - MainnetChainConfig = &BlobFeeConfig{ - ChainID: big.NewInt(1), - LondonBlock: big.NewInt(12_965_000), - CancunTime: newUint64(1710338135), - PragueTime: newUint64(1746612311), - OsakaTime: newUint64(1764798551), - BPO1Time: newUint64(1765290071), - BPO2Time: newUint64(1767747671), - Cancun: DefaultCancunBlobConfig, - Prague: DefaultPragueBlobConfig, - Osaka: DefaultOsakaBlobConfig, - BPO1: DefaultBPO1BlobConfig, - BPO2: DefaultBPO2BlobConfig, - Default: DefaultOsakaBlobConfig, - } - - // HoodiChainConfig contains the chain parameters to run a node on the Hoodi test network. - HoodiChainConfig = &BlobFeeConfig{ - ChainID: big.NewInt(560048), - LondonBlock: big.NewInt(0), - CancunTime: newUint64(0), - PragueTime: newUint64(1742999832), - OsakaTime: newUint64(1761677592), - BPO1Time: newUint64(1762365720), - BPO2Time: newUint64(1762955544), - Cancun: DefaultCancunBlobConfig, - Prague: DefaultPragueBlobConfig, - Osaka: DefaultOsakaBlobConfig, - BPO1: DefaultBPO1BlobConfig, - BPO2: DefaultBPO2BlobConfig, - Default: DefaultOsakaBlobConfig, - } - - // DevnetChainConfig contains the chain parameters to run a node on the devnet test network. - DevnetChainConfig = &BlobFeeConfig{ - ChainID: big.NewInt(900), - LondonBlock: big.NewInt(0), - CancunTime: newUint64(0), - PragueTime: newUint64(1742999832), - OsakaTime: newUint64(1761677592), - BPO1Time: newUint64(1762365720), - BPO2Time: newUint64(1762955544), - Cancun: DefaultCancunBlobConfig, - Prague: DefaultPragueBlobConfig, - Osaka: DefaultOsakaBlobConfig, - BPO1: DefaultBPO1BlobConfig, - BPO2: DefaultBPO2BlobConfig, - Default: DefaultOsakaBlobConfig, - } -) - -var ( - // DefaultCancunBlobConfig is the default blob configuration for the Cancun fork. - DefaultCancunBlobConfig = &BlobConfig{ - UpdateFraction: 3338477, - } - // DefaultPragueBlobConfig is the default blob configuration for the Prague fork. - DefaultPragueBlobConfig = &BlobConfig{ - UpdateFraction: 5007716, - } - // DefaultOsakaBlobConfig is the default blob configuration for the Osaka fork. - DefaultOsakaBlobConfig = &BlobConfig{ - UpdateFraction: 5007716, - } - // DefaultBPO1BlobConfig is the default blob configuration for the BPO1 fork. - DefaultBPO1BlobConfig = &BlobConfig{ - UpdateFraction: 8346193, - } - // DefaultBPO2BlobConfig is the default blob configuration for the BPO2 fork. - DefaultBPO2BlobConfig = &BlobConfig{ - UpdateFraction: 11684671, - } - // DefaultBPO3BlobConfig is the default blob configuration for the BPO3 fork. - DefaultBPO3BlobConfig = &BlobConfig{ - UpdateFraction: 20609697, - } - // DefaultBPO4BlobConfig is the default blob configuration for the BPO4 fork. - DefaultBPO4BlobConfig = &BlobConfig{ - UpdateFraction: 13739630, - } -) diff --git a/tx-submitter/types/converter.go b/tx-submitter/types/converter.go index d5b16398d..b1b9863fd 100644 --- a/tx-submitter/types/converter.go +++ b/tx-submitter/types/converter.go @@ -1,25 +1,15 @@ package types -import ( - "encoding/binary" - "fmt" -) +import "morph-l2/common/batch" func Uint64ToBigEndianBytes(value uint64) []byte { - valueBytes := make([]byte, 8) - binary.BigEndian.PutUint64(valueBytes, value) - return valueBytes + return batch.Uint64ToBigEndianBytes(value) } func Uint16ToBigEndianBytes(value uint16) []byte { - valueBytes := make([]byte, 2) - binary.BigEndian.PutUint16(valueBytes, value) - return valueBytes + return batch.Uint16ToBigEndianBytes(value) } func HeightFromBlockContextBytes(blockContextBytes []byte) (uint64, error) { - if len(blockContextBytes) != 60 { - return 0, fmt.Errorf("wrong block context bytes length, input: %x", blockContextBytes) - } - return binary.BigEndian.Uint64(blockContextBytes[:8]), nil + return batch.HeightFromBlockContextBytes(blockContextBytes) } diff --git a/tx-submitter/types/l2Caller.go b/tx-submitter/types/l2Caller.go index cef0ab903..e3a9311d2 100644 --- a/tx-submitter/types/l2Caller.go +++ b/tx-submitter/types/l2Caller.go @@ -1,28 +1,17 @@ package types import ( - "bytes" "fmt" - "math/big" - "morph-l2/bindings/bindings" - "morph-l2/bindings/predeploys" + "morph-l2/common/batch" "morph-l2/tx-submitter/iface" - - "github.com/morph-l2/go-ethereum/accounts/abi/bind" - "github.com/morph-l2/go-ethereum/common" - "github.com/morph-l2/go-ethereum/common/hexutil" - "github.com/morph-l2/go-ethereum/crypto" ) -type L2Caller struct { - l2Clients *iface.L2Clients - sequencerContract *bindings.SequencerCaller - l2MessagePasserContract *bindings.L2ToL1MessagePasserCaller - govContract *bindings.GovCaller -} +// L2Caller reads L2 gov / sequencer state for batch assembly (see batch.L2Gov). +type L2Caller = batch.L2Gov -func NewL2Caller(l2Clients []iface.L2Client) (*L2Caller, error) { +// NewL2Caller builds an L2Caller backed by the given L2 RPC clients. +func NewL2Caller(l2Clients []iface.L2Client) (*batch.L2Gov, error) { if len(l2Clients) == 0 { return nil, fmt.Errorf("no l2clients provided") } @@ -31,65 +20,5 @@ func NewL2Caller(l2Clients []iface.L2Client) (*L2Caller, error) { return nil, fmt.Errorf("nil l2client") } } - clients := &iface.L2Clients{Clients: l2Clients} - - // Initialize Sequencer contract - sequencerContract, err := bindings.NewSequencerCaller(predeploys.SequencerAddr, clients) - if err != nil { - return nil, err - } - - // Initialize L2ToL1MessagePasser contract - l2MessagePasserContract, err := bindings.NewL2ToL1MessagePasserCaller(predeploys.L2ToL1MessagePasserAddr, clients) - if err != nil { - return nil, err - } - - // Initialize Gov contract - govContract, err := bindings.NewGovCaller(predeploys.GovAddr, clients) - if err != nil { - return nil, err - } - - return &L2Caller{ - l2Clients: clients, - sequencerContract: sequencerContract, - l2MessagePasserContract: l2MessagePasserContract, - govContract: govContract, - }, nil -} - -// SequencerSetVerifyHash gets the sequencer set verify hash from the Sequencer contract -func (c *L2Caller) SequencerSetVerifyHash(opts *bind.CallOpts) ([32]byte, error) { - return c.sequencerContract.SequencerSetVerifyHash(opts) -} - -// GetTreeRoot gets the tree root from the L2ToL1MessagePasser contract -func (c *L2Caller) GetTreeRoot(opts *bind.CallOpts) ([32]byte, error) { - return c.l2MessagePasserContract.GetTreeRoot(opts) -} - -// BatchBlockInterval gets the batch block interval from the Gov contract -func (c *L2Caller) BatchBlockInterval(opts *bind.CallOpts) (*big.Int, error) { - return c.govContract.BatchBlockInterval(opts) -} - -// BatchTimeout gets the batch timeout from the Gov contract -func (c *L2Caller) BatchTimeout(opts *bind.CallOpts) (*big.Int, error) { - return c.govContract.BatchTimeout(opts) -} - -func (c *L2Caller) GetSequencerSetBytes(opts *bind.CallOpts) ([]byte, common.Hash, error) { - hash, err := c.sequencerContract.SequencerSetVerifyHash(opts) - if err != nil { - return nil, common.Hash{}, err - } - setBytes, err := c.sequencerContract.GetSequencerSetBytes(opts) - if err != nil { - return nil, common.Hash{}, err - } - if bytes.Equal(hash[:], crypto.Keccak256Hash(setBytes).Bytes()) { - return setBytes, hash, nil - } - return nil, common.Hash{}, fmt.Errorf("sequencer set hash verify failed %v: %v", hexutil.Encode(setBytes), common.BytesToHash(hash[:]).String()) + return batch.NewL2Gov(&iface.L2Clients{Clients: l2Clients}) } diff --git a/tx-submitter/utils/config.go b/tx-submitter/utils/config.go index a24d5604b..4e9269f95 100644 --- a/tx-submitter/utils/config.go +++ b/tx-submitter/utils/config.go @@ -112,6 +112,10 @@ type Config struct { BlockNotIncreasedThreshold int64 // enable seal batch SealBatch bool + // max blob count per batch + MaxBlobCount int + // unix timestamp at which V2 multi-blob batch format is activated (0 = disabled) + BatchV2UpgradeTime uint64 } // NewConfig parses the DriverConfig from the provided flags or environment variables. @@ -187,6 +191,10 @@ func NewConfig(ctx *cli.Context) (Config, error) { BlockNotIncreasedThreshold: ctx.GlobalInt64(flags.BlockNotIncreasedThreshold.Name), // SealBatch SealBatch: ctx.GlobalBool(flags.SealBatch.Name), + // MaxBlobCount + MaxBlobCount: ctx.GlobalInt(flags.MaxBlobCountFlag.Name), + // BatchV2UpgradeTime + BatchV2UpgradeTime: ctx.GlobalUint64(flags.BatchV2UpgradeTimeFlag.Name), } return cfg, nil