Summary
engine_newSafeL2Block is expected to deterministically reconstruct the L1-committed L2 block from SafeL2Data. In morph-reth v1.2.0 it reuses the normal payload builder, which always appends best transactions from the local txpool.
A follower can therefore insert transactions from future L2 blocks into an earlier derived block. Its chain then diverges from the sequencer and derivation eventually stalls when the same transactions appear at their committed heights.
Environment
- Image:
ghcr.io/morph-l2/morph-reth:latest
- Version:
v1.2.0
- Revision:
ce119b59b99190d640599e622e2a162cb47cf8ed
- Devnet command:
make devnet-up-reth in morph-l2/morph
- Sequencer EL:
morph-el-0 / host port 8545
- Layer-1 verification EL:
morph-el-1 / host port 8645
The current main branch is also affected; it is only one CI dependency commit ahead of v1.2.0.
Observed behavior
Batch 2 committed L2 blocks 200 through 398. On the sequencer:
- block 200 was empty;
- transaction
0x29c0e6a805748fa240e314cc2cc8dead97f7afdd9b7b390df834e45af228e2f4 (sender nonce 8) was in block 314;
- transaction
0x819fadd2e852dd3e0771d151dd2924f6b37f31eb7affdd150a752194637cadc7 (sender nonce 9) was in block 318.
The derivation follower instead produced block 200 containing both transactions:
sequencer block 200:
hash=0x39c4fa982dffefba7d7ab49364264575b349b730a6399107ebcc796cd4ef5d36
txs=0
follower block 200:
hash=0xdb5b9ecc4c6b6e061c1196dc0045cbfc372af09442a10c36c2568d6dc2eb1d93
txs=[nonce 8, nonce 9]
The chains agree through block 199 and diverge exactly at block 200. The follower then reaches block 313 and fails while deriving block 314:
invalid L1 message transaction in payload attributes
error="nonce 8 too low, expected 10"
failed to NewSafeL2Block
error="failed to build block: failed to get built payload: missing payload"
After retries, the Engine API calls time out. The follower remains at block 313 while the sequencer continues advancing.
Root cause
RealMorphL2EngineApi::new_safe_l2_block moves SafeL2Data.transactions into AssembleL2BlockParams and calls the shared build_l2_payload path.
That path puts the supplied transactions into MorphPayloadAttributes.transactions. The payload builder defines those attributes as L1 messages, executes them first, and then unconditionally executes transactions from the local pool:
// Always execute pool transactions (L2 transactions from mempool)
let best_txs = best(ctx.best_transaction_attributes(base_fee));
ctx.execute_pool_transactions(...)?;
On a derivation follower, future sequencer transactions are already present in the local pool through transaction gossip. An empty committed block therefore absorbs those future transactions. When the committed transaction is supplied again at its real block height, its nonce is already consumed.
There is also an interface mismatch: SafeL2Data.transactions is documented as the complete ordered transaction list needed to reconstruct the block, while MorphPayloadAttributes.transactions is documented and processed as L1 messages only.
Expected behavior
engine_newSafeL2Block must be deterministic:
- execute exactly the ordered transactions supplied in
SafeL2Data;
- never read or append transactions from the local txpool;
- produce the same block hash as the sequencer for the same parent and committed block inputs.
Normal sequencer assembly can continue using the txpool.
Requested change
- Add an explicit payload-building policy that disables txpool selection for
new_safe_l2_block.
- Treat
SafeL2Data.transactions as the complete ordered block transaction list rather than as an L1-message-only list.
- Keep the normal sequencer assembly behavior unchanged.
- Add a regression test that:
- preloads the follower pool with transactions committed to later blocks;
- derives an earlier empty block and asserts that it remains empty;
- derives the transactions at their committed heights without a nonce error;
- asserts follower and sequencer block hashes match.
Impact
The layer-1 verification/full-history follower diverges and stalls, so its safe and finalized tags stop advancing. Services that require both full history and L2 finality cannot safely use the follower RPC.
Summary
engine_newSafeL2Blockis expected to deterministically reconstruct the L1-committed L2 block fromSafeL2Data. In morph-reth v1.2.0 it reuses the normal payload builder, which always appends best transactions from the local txpool.A follower can therefore insert transactions from future L2 blocks into an earlier derived block. Its chain then diverges from the sequencer and derivation eventually stalls when the same transactions appear at their committed heights.
Environment
ghcr.io/morph-l2/morph-reth:latestv1.2.0ce119b59b99190d640599e622e2a162cb47cf8edmake devnet-up-rethinmorph-l2/morphmorph-el-0/ host port 8545morph-el-1/ host port 8645The current
mainbranch is also affected; it is only one CI dependency commit ahead of v1.2.0.Observed behavior
Batch 2 committed L2 blocks 200 through 398. On the sequencer:
0x29c0e6a805748fa240e314cc2cc8dead97f7afdd9b7b390df834e45af228e2f4(sender nonce 8) was in block 314;0x819fadd2e852dd3e0771d151dd2924f6b37f31eb7affdd150a752194637cadc7(sender nonce 9) was in block 318.The derivation follower instead produced block 200 containing both transactions:
The chains agree through block 199 and diverge exactly at block 200. The follower then reaches block 313 and fails while deriving block 314:
After retries, the Engine API calls time out. The follower remains at block 313 while the sequencer continues advancing.
Root cause
RealMorphL2EngineApi::new_safe_l2_blockmovesSafeL2Data.transactionsintoAssembleL2BlockParamsand calls the sharedbuild_l2_payloadpath.That path puts the supplied transactions into
MorphPayloadAttributes.transactions. The payload builder defines those attributes as L1 messages, executes them first, and then unconditionally executes transactions from the local pool:On a derivation follower, future sequencer transactions are already present in the local pool through transaction gossip. An empty committed block therefore absorbs those future transactions. When the committed transaction is supplied again at its real block height, its nonce is already consumed.
There is also an interface mismatch:
SafeL2Data.transactionsis documented as the complete ordered transaction list needed to reconstruct the block, whileMorphPayloadAttributes.transactionsis documented and processed as L1 messages only.Expected behavior
engine_newSafeL2Blockmust be deterministic:SafeL2Data;Normal sequencer assembly can continue using the txpool.
Requested change
new_safe_l2_block.SafeL2Data.transactionsas the complete ordered block transaction list rather than as an L1-message-only list.Impact
The layer-1 verification/full-history follower diverges and stalls, so its
safeandfinalizedtags stop advancing. Services that require both full history and L2 finality cannot safely use the follower RPC.