Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
1dcb46e
contract for blob
Kukoomomo Apr 15, 2026
9b4d976
submitter multi batch
Kukoomomo Apr 15, 2026
a6958e1
fix: align V2 header generation and remove MAX_BLOB_PER_BLOCK constant
chengwenxi Apr 16, 2026
2711f07
feat(prover): add multi-blob V2 support for prover, shadow-prove, and…
chengwenxi Apr 16, 2026
9894b04
fix(prover): wire batch_version and fix V2 blob validation across all…
chengwenxi Apr 16, 2026
8c419a7
refactor(challenge): simplify BatchInfo to use blob_hashes vec, drop …
chengwenxi Apr 17, 2026
f8a7f2e
style(prover): cargo fmt
chengwenxi Apr 17, 2026
edf823f
fix(prover): correct multi-blob decode — unpack all blobs first, deco…
chengwenxi Apr 17, 2026
d78251c
refactor: simplify V2 batch header — store aggregated blob hash at of…
chengwenxi Apr 17, 2026
56bd83f
fix(rollup): store aggregatedBlobHash in batchBlobVersionedHashes for V2
chengwenxi Apr 17, 2026
2ee96ae
refactor(rollup): extract _computeBlobVersionedHash, unify header con…
chengwenxi Apr 17, 2026
9ae6e51
refactor(rollup): allow V2 batches to use commitState
chengwenxi Apr 17, 2026
50f96d1
gas-price-oracle support multi blob
anylots Apr 22, 2026
c355467
fix(rollup): require blob count check before keccak in _computeBlobVe…
chengwenxi Apr 23, 2026
10a7d3b
feat(node): multi-blob derivation support (V2 batch) (#937)
curryxbo Apr 24, 2026
482f31c
update submitter config for multi blob
Kukoomomo Apr 28, 2026
44e37a4
update devnet gov config
Kukoomomo Apr 28, 2026
31b7bb7
update docker config
Kukoomomo Apr 28, 2026
2bc40ff
update common
Kukoomomo Apr 29, 2026
86b1764
gas-oracle batch data check
anylots Apr 30, 2026
1bc53e3
fix(derivation): harden blob verification for PeerDAS sidecars (#944)
curryxbo May 6, 2026
47f4b67
Merge branch 'main' into feat/multi_batch
curryxbo May 6, 2026
7b7be9a
Merge branch 'main' into feat/multi_batch
May 6, 2026
8fe8bde
refactor: dedupe batch header / blob helpers, single-source on common…
curryxbo May 7, 2026
82190b3
add common for batch
Kukoomomo May 7, 2026
53aee9b
Merge branch 'feat/multi_batch' of github.com:morph-l2/morph into fea…
Kukoomomo May 7, 2026
a3568e0
add common for batch
Kukoomomo May 7, 2026
da9c301
fix node test
Kukoomomo May 7, 2026
cb17a22
fix node test
Kukoomomo May 7, 2026
7535858
add logs
Kukoomomo May 7, 2026
245656f
num_blobs check
anylots May 8, 2026
d61284e
update prover elf and programVkey
chengwenxi May 9, 2026
2843031
fix(derivation): guard BlockContexts length before reading block-coun…
May 11, 2026
aae2213
fix(derivation): guard against blockCount underflow on malformed batches
May 11, 2026
f5f5656
add prover qa&testnet deploy cmd
anylots May 11, 2026
82e7062
Revert "fix(derivation): guard against blockCount underflow on malfor…
May 11, 2026
e0756a4
use block header's state-root
anylots May 11, 2026
5df8bf3
add default_batch_version
anylots May 11, 2026
44ab6aa
update vkey
chengwenxi May 12, 2026
fbd1bee
chore: align go-ethereum submodule with origin/main
Kukoomomo May 12, 2026
1fe0d74
add rustc version desc
anylots May 13, 2026
fa9f4e8
fix submitter replay batch with config max blob count
Kukoomomo May 13, 2026
7ec11cf
Merge branch 'feat/multi_batch' of github.com:morph-l2/morph into fea…
Kukoomomo May 13, 2026
c6f7cc2
add v0/v1 blob check
Kukoomomo May 13, 2026
4be9cda
update challenge handler dep
anylots May 13, 2026
84ae08d
fix(derivation): guard against blockCount underflow on malformed batc…
curryxbo May 18, 2026
daf6937
fix(prover): replace .unwrap() with map_err in blob KZG verification …
chengwenxi May 18, 2026
af9e631
docs(prover): explain why header.state_root replaces morph_diskRoot (…
chengwenxi May 18, 2026
ab5c916
docs(shadow-prove): clarify version is read for logging only in calc_…
chengwenxi May 18, 2026
6718710
Multi batch commit state (#954)
Kukoomomo May 19, 2026
c3243f2
Merge remote-tracking branch 'origin/main' into feat/multi_batch
chengwenxi May 21, 2026
dacd340
fix: address CodeRabbit issues from PR #935
chengwenxi May 21, 2026
6ae662f
chore(prover): rebuild verifier-client ELF for SP1 v6 + multi_batch
chengwenxi May 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
408 changes: 352 additions & 56 deletions tx-submitter/batch/batch_cache.go → common/batch/batch_cache.go

Large diffs are not rendered by default.

129 changes: 129 additions & 0 deletions common/batch/batch_cache_genesis_header_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package batch

import (
"context"
"fmt"
"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)

batchCacheSyncMu.Lock()
err = cache.AssembleCurrentBatchHeader()
batchCacheSyncMu.Unlock()
require.NoError(t, err)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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

Expand All @@ -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()
Expand All @@ -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)
}
31 changes: 14 additions & 17 deletions tx-submitter/batch/batch_data.go → common/batch/batch_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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 {
Expand All @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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
}
Expand Down Expand Up @@ -94,13 +104,48 @@ 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
}

// BlobCommitHash returns the blob-related 32-byte field at offset [57:89]:
// V0/V1 use BlobVersionedHash; V2+ use BlobHashesHash (same wire offset, different semantics).
func (b BatchHeaderBytes) BlobCommitHash() (common.Hash, error) {
version, err := b.Version()
if err != nil {
return common.Hash{}, err
}
if version >= BatchHeaderVersion2 {
return b.BlobHashesHash()
}
return b.BlobVersionedHash()
}

func (b BatchHeaderBytes) PrevStateRoot() (common.Hash, error) {
if err := b.validate(); err != nil {
return common.Hash{}, err
Expand Down
Loading
Loading