Support splitting Linera block into multiple EVM transactions.#6295
Open
deuszx wants to merge 29 commits into
Open
Support splitting Linera block into multiple EVM transactions.#6295deuszx wants to merge 29 commits into
deuszx wants to merge 29 commits into
Conversation
74de1cd to
63fefe2
Compare
Empty eventPositionsInTx silently paid for cert verification with no work to do. Reject eagerly so caller bugs surface early.
Mirrors _onBlock's idempotent semantics. Overlap with a prior addBlock or a racing/retrying processBurns no longer reverts the whole chunk — already-released burns are skipped silently and the rest still settle. Removes the all-or-nothing fragility flagged in PR review.
A bare "out of gas" message can fire for reasons unrelated to the block
gas limit — e.g. too-low tx gas cap, or contract state consuming more
than estimated. Matching it as "doesn't fit" sent the relayer down the
chunking path and masked real misconfigurations behind retry churn.
Keep the node-specific signals ("gas required exceeds",
"exceeds block gas limit", anvil's "execution reverted" + data: "0x").
Returns (height, event_indices, by_tx) per height under a single monitor read so process_pending_burns drives chunking and retry/cert persistence off one consistent snapshot. Previously the second view came from another read with a different (no max_retries) filter, risking TOCTOU drift between the two views.
event_index_for_pos maps a (height, tx_index, pos_in_tx) triple back to the event_index that mark_burn_retried/mark_burn_failed need — letting the chunked processBurns path bump retry per-chunk and mark oversized burns failed without a second snapshot read. pending_burns_by_height_and_tx now filters burns whose failed flag is set, so a once-marked-failed burn (next commit's oversized path) drops out of subsequent snapshots instead of poisoning the chunking loop on every pass.
Retry budget previously bumped unconditionally for all burns at a height once per pass — a handful of transient RPC blips (cert fetch timeout, gas-estimate failure) could push burns past max_retries and silently strand them. Now: - Cert fetch / gas-estimate failures: skip the height, bump nothing. These are infrastructure-level, not burn-level. - addBlock submit Ok: no retry bump (completion is async). - addBlock submit Err: bump retry for every burn at the height. - Chunked processBurns submit Err: bump retry for that chunk's burns only; remaining chunks still attempt (each is an independent tx). A single-position chunk that still doesn't fit (oversized burn) used to break out of the per-tx loop, discarding sibling chunks already known to fit and burning every retry pass re-rediscovering it. Now: - Continue draining the stack so other slices still find chunks. - Mark the oversized burn so the next pending_burns_by_height snapshot excludes it — no more poisoning its tx group every pass. - Still submit the chunks that did fit before/around it.
Tuple form (BlockHeight, Vec<u32>, Vec<(u32, Vec<u32>)>) was easy to misuse positionally. Named fields document each role. Ord keyed on height makes BTreeSet height-sorted structurally and rejects a second entry for the same height (which never happens in practice).
process_pending_burns's per-height body collapsed to: cert fetch + persist + dispatch. Each branch of the dispatch now lives in its own helper: - submit_addblock: forward_cert + per-burn retry bump on failure - submit_chunked: per-tx loop, calls split + mark_oversized + submit - split_to_fit: pure binary-search chunking, returns (chunks, oversized) - mark_oversized_failed: looks up event_indices, marks each failed - submit_chunks_with_retry: per-chunk submit + per-chunk retry bump
bcc059b to
ec28611
Compare
afck
reviewed
May 19, 2026
Contributor
afck
left a comment
There was a problem hiding this comment.
(No blockers so far, but I might need a walkthrough in a meeting to approve. 😅)
| if oversized.is_empty() { | ||
| return; | ||
| } | ||
| let to_fail: Vec<u32> = { |
Contributor
There was a problem hiding this comment.
(Type annotation should go on collect below.)
| tracing::error!( | ||
| ?height, | ||
| tx_index, | ||
| event_index = ei, |
Contributor
There was a problem hiding this comment.
(Why not for event_index in to_fail?)
| ?error, | ||
| "processBurns submission failed" | ||
| ); | ||
| let to_bump: Vec<u32> = { |
| alloy::primitives::Address, | ||
| U256, | ||
| > = std::collections::BTreeMap::new(); | ||
| let mut expected_per_recipient: std::collections::BTreeMap<alloy::primitives::Address, U256> = |
Contributor
There was a problem hiding this comment.
(Type annotation could move to next line; avoids BTreeMap duplication.)
| let mut result = Vec::new(); | ||
| for tx_events in events { | ||
| for event in tx_events { | ||
| for (i, tx_events) in events.iter().enumerate() { |
Contributor
There was a problem hiding this comment.
(0u32..).zip(&events) would avoid the type cast below.
|
|
||
| impl PartialOrd for PendingBurnsAtHeight { | ||
| fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { | ||
| Some(self.cmp(other)) |
Contributor
There was a problem hiding this comment.
(That doesn't really satisfy the PartialOrd properties documented in the standard library: Eq should match whether this returns Some(Equal): https://doc.rust-lang.org/std/cmp/trait.PartialOrd.html)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
A Linera block can carry enough burns to exceed any EVM chain's block gas limit (~30M on Ethereum, ~240M on Base), in which case
FungibleBridge.addBlock(cert)does not fit in one EVM transaction. Today therelayer cannot make progress on such a block:
Microchain.verifiedBlocksblocks a retry, and there is no per-burn entry point. This PR makes the bridge multi-tx-friendly so the relayer can split such a blockinto several smaller EVM transactions.
Proposal
Microchain.verifiedBlocksmapping dropped;addBlockis now idempotent on the subclass's per-burn dedup (processedBurnskeyed on(height, Event.index)). Documented requirement that subclasses MUST be idempotent under repeated calls.FungibleBridge.processBurns(bytes cert, uint32 txIndex, uint32[] eventPositionsInTx)entry point: verifies the cert once and releases the subset of burns at the requested positions within a single transaction. Directbody.events[txIndex][pos]access — no nested-loop scan.processBurnsis idempotent like_onBlock: positions already inprocessedBurnsare skipped silently rather than reverted, so overlap with a prioraddBlock(or a racing/retryingprocessBurns) doesn't poison the whole chunk._burnKey,_isMatchingBurn,_releaseBurnextracted from_onBlock+processBurnsto keep the dedup-key invariant in one place and tighten the matching logic._releaseBurnapplies checks-effects-interactions (sets the dedup flag BEFOREtoken.transfer) for both entry points — a reentrancy defense in depth.PendingBurngainstx_indexandevent_pos_in_txfields alongside the existingevent_index(still the on-chain dedup key). The scanner'sfind_burn_eventswidens its return tuple; SQLitepending_burns/finished_burnsadd two columns.process_pending_burnsdoes per-height dispatch (cert fetch + persist + decide addBlock vs chunked) with branches factored into helpers:submit_addblockorsubmit_chunked.addBlocksuccess leaves retry counts alone (completion is observed asynchronously bycheck_burn_completion);addBlocksubmit failure bumps retry for every burn at the height. Chunked submit failure bumps retry only for that chunk's burns; remaining chunks still attempt submission.failedand drop out of subsequent snapshots, instead of poisoning their tx group on every retry pass.Test Plan
New Forge tests on
linera-bridge/src/solidity/:MicrochainIdempotencyTest::test_addBlock_can_be_called_repeatedly_for_same_cert— re-entry on the same cert is now allowed.FungibleBridgeProcessBurnsTestcases —single_position_marks_processed,multi_position_marks_both_processed,already_processed_skips(idempotent no-op + no double-release),tx_index_out_of_range_reverts,event_pos_out_of_range_reverts,non_burn_event_reverts,empty_positions_reverts,partial_overlap_releases_remaining(settle pos 1 then [0,1] → pos 0 settles, pos 1 silently skipped).Rust unit tests under
cargo test -p linera-bridge --features relay:relay::settlement::tests::estimate_fits_*— 8 cases coveringOk,gas required exceeds,exceeds block gas limit, anvil'sOut of gas: gas required exceeds allowance: N(empirically verified againstanvil 1.6 for both calldata-too-large and infinite-loop constructors), real contract revert with non-empty
data(bubbles up), bareout of gas(bubbles up — not a fits signal), unrelated RPC error, andtransport-layer error.
Existing
evm::microchain::tests::test_microchain_*flipped to assert the new idempotent behaviour.New e2e test
linera-bridge/tests/e2e/tests/multi_tx_burn_chunking.rs:evm_setBlockGasLimitafter the bridge deploys.addBlock(cert)estimate exceeds the limit and falls back to chunkedprocessBurnsper tx; all 8 burns settle, balances match1 * 10^18,linera_bridge_burns_completed = 8.Existing
multiple_burns_same_blockandmultiple_burns_same_recipient_across_blockse2e tests still cover the per-burnisBurnProcessedsemantics from Fix the bug in relayer logic responsible for marking LineraPendingBurntxn as finished on EVM #6276 and remain unchanged.Release Plan
mainbranch.Links