Support blocking certain Burn s from releasing tokens on EVM. Refund the sender.#6400
Draft
deuszx wants to merge 28 commits into
Draft
Support blocking certain Burn s from releasing tokens on EVM. Refund the sender.#6400deuszx wants to merge 28 commits into
Burn s from releasing tokens on EVM. Refund the sender.#6400deuszx wants to merge 28 commits into
Conversation
667fc82 to
5ecb50c
Compare
Also corrects pre-existing burnEvt.amount.value access in _releaseBurn — codegen exposes BurnEvent.amount as a bare uint128, not a wrapped struct.
Add a new proof variant that proves a burn was blocked by validators, enabling the refund path in the fungible bridge. The proof carries the burn parameters and a validator-signed attestation that the burn could not be executed on the destination chain. - New BurnBlocked proof module under linera-bridge/src/proof/ - Wire the variant into the proof enum and verification dispatch - Update fungible_bridge call sites to handle the new variant - Add bcs to linera-bridge deps for proof serialization
EvmClient gains get_chain_id and get_burn_blocked_logs. MonitorState holds the EVM source chain id, plus a refunds map mirroring the burn state machine (track / complete / retry / fail) with matching metrics. The EVM scan iteration now picks up BurnBlocked logs alongside deposits and feeds them into track_refund using RefundKey built from the cached chain id. SQLite persistence and the retry-loop branch land in Tasks 10 and 11.
Revert the variant rename — Process prefix is intentional and mirrors BridgeOperation::Process*. Use #[allow(clippy::enum_variant_names)] instead.
Replay protection is a cheap SetView containment check; running it before the RPC finality call and MPT receipt-inclusion proof lets duplicate refund proofs fail fast without paying for the expensive crypto.
Mirror the reorder applied to process_refund — cheap SetView containment check runs before the expensive finality + MPT proof so duplicate proofs fail fast.
StatusSummary gains refunds_pending / refunds_completed / refunds_failed. The EVM scan loop already drives the refund completion check; extend its periodic trace! summary to report the refund counters so operators can see refund flow in the bridge logs and dashboards.
The startup path queried evm_client.get_chain_id() once and bailed on failure; a brief RPC outage at boot would kill the relayer. Bounded exponential backoff (1s, 2s, 4s, 8s, 16s — ~31s total) lets a flaky RPC recover before we give up while still surfacing a hard outage as a clean startup error.
…ked emission Add MockLightClientWithOwner with owner fields as immutable constructor params so Reserved/CHAIN, Address32, and Address20 can each be exercised. test_blockBurn_emits_BurnBlocked_with_decoded_fields verifies the emitted source_owner_bcs matches bcs_serialize_AccountOwner for every variant.
Add KeyDomain enum (Deposit=0x01, Refund=0x02) whose variants are the single source of truth for hash domain tags. DepositKey and RefundKey each carry a private _domain field set in their new() constructors — struct-literal construction is now a compile error, so a future key type cannot forget to assign a tag or accidentally reuse one. hash() reads self._domain instead of a local constant.
Silent no-ops masked caller-intent mismatches: a blockBurn against an already-processed burn used to succeed quietly, and processBurns across a blocked position used to skip silently. Both now revert.
56190bf to
44070ae
Compare
Field is already non-pub; the leading underscore added nothing.
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
Support blocking certain Linera
Burn-s from unlocking tokens on the EVM side (legal purposes for example). What's important, we can't take over those tokens – they need to be returned to the original account (that issued the transfer) on Linera side.Proposal
End-to-end flow:
WrappedFungibleemitsBurnEvent { source: Account, target: Address20, amount }on theburnsstream. The newsourcefield carries the burner's chain + owner so arefund can be routed back.
FungibleBridge.blockBurn(bytes cert, uint32 txIndex, uint32 eventPosInTx). Signature is now cert-verified — pre-emptive blocking is no longer supported. On success the contract setsblockedBurns[key] = trueand emits an enrichedBurnBlocked(uint64 indexed height, uint32 indexed eventIndex, address indexed blocked_by, bytes32 source_chain_id, bytes source_owner_bcs, uint128 amount).source_owner_bcsis the BCS encoding ofAccountOwner, produced via the codegen helper.BurnBlockedlogs alongsideDepositInitiated, persists each as aPendingRefundin SQLite (pending_refunds+finished_refundstables), generates an MPT receipt proof, and submitsBridgeOperation::RefundBurn { block_header_rlp, receipt_rlp, proof_nodes, tx_index, log_index }.evm-bridgecontract verifies the proof (same scaffolding asProcessDeposit), dedups via a newprocessed_refunds: SetView<[u8; 32]>keyed byRefundKey { source_chain_id, block_hash, tx_index, log_index }.hash(), and callsWrappedFungibleOperation::Mint { target_account: Account { chain_id: source_chain_id, owner: source_owner }, amount }.execute_mintcascades through the existing flow: local credit if the bridge runs on the burner's chain, otherwise a trackedMessage::Creditto the burner's chain — same path the deposit-mint already uses.Caller-intent mismatches now revert instead of silently no-op:
blockBurnreverts with"already processed"when the burn has already been settled — the caller intended to block, not accept settlement.processBurnsreverts with"burn blocked"when any position in the batch is inblockedBurns— atomic, no partial release._onBlock(addBlock path) still silently skips both — broader-scopehandler that processes everything in a block.
Replay-protection keys are domain-separated by construction:
KeyDomainenum (Deposit = 0x01,Refund = 0x02) — the single source of truth for hash-input tags. Adding a future key type requires adding a variant, so a tag cannot be forgotten or accidentallyreused.
DepositKeyandRefundKeycarry a private_domain: KeyDomainfield set byDepositKey::new(...)/RefundKey::new(...). Struct-literal construction is now a compile error, so callers can't bypass thetag.
hash()readsself._domaininstead of a hardcoded constant.Implementation notes:
bcsdependency moved fromoffchain-only to unconditional inlinera-bridge/Cargo.toml. The newparse_burn_blocked_eventdecodesAccountOwnerunder thechainfeature so the Linera-side Wasmcontract can verify refund proofs.
isRefundProcessed(refundHash: ...)GraphQL field that mirrorsisDepositProcessedand the relayer polls for completion.refund_completedIntCounter metric (mirrorsburn_completed/deposit_completed).BridgeParameters.rpc_endpointfields inprocess_deposit.rs+set_rpc_endpoint.rs, and aburnEvt.amount.valueaccess in_releaseBurnthat didn't compile against the regenerated codegen (uint128 amountis bare, not wrapped). These ship as separate commits.Test Plan
forge testonlinera-bridge/src/solidity— 22 cases pass (5 suites). Notable cases for this PR:test_blockBurn_emits_BurnBlocked_with_decoded_fields— exercises all threeAccountOwnervariants (Reserved/CHAIN, Address32, Address20) through a new parametricMockLightClientWithOwner; eachvariant verifies
source_owner_bcsround-trips throughBridgeTypes.bcs_serialize_AccountOwner.test_blockBurn_reverts_on_already_processed—blockBurnafter a successfulprocessBurnsreverts with"already processed"; blocked flag does not flip.test_processBurns_reverts_on_blocked_position—processBurns([0, 1])over a position that was blocked reverts with"burn blocked"; both positions stay unprocessed, no tokens released (atomic revert).Same-intent idempotency:
test_blockBurn_idempotent_emits_once,test_processBurns_already_processed_skips,test_blockBurn_prevents_addBlock_release(addBlock path still silently skips blocked).test_blockBurn_chain_id_mismatch_reverts,_tx_index_out_of_range_reverts,_event_pos_out_of_range_reverts,_non_burn_event_reverts.cargo test -p linera-bridge— 88 passing.cargo test -p linera-bridge --features relay— 145 passing. New tests: 5 cases inproof::burn_blocked(happy path, wrong address / topic / arity / malformedowner),
RefundKey::hashdeterminism, andtest_refund_and_deposit_key_hashes_are_domain_separatedprovingKeyDomainkeeps the two key spaces disjoint even with identical field values.cargo test -p evm-bridge— 17 passing including 4 newrefund_burn.rscases:refund_burn_success_mints_to_source— happy path;processed_refundsflag set,Mintissued with decoded source + amount.refund_burn_replay_rejected— second submission of the same proof panics with"refund already processed".refund_burn_wrong_bridge_address— log address differs from registered.refund_burn_wrong_topic— proof points at aDepositInitiatedlog.cargo clippy --locked --all-targets --all-features -- -D warnings— clean.e2e:
linera-bridge/tests/e2e/tests/refund_after_block.rs(#[ignore]d like sibling tests). Sets up chain A as bridge host + mint chain, chain B as the burner. Owner_b burns wrapped tokens to an EVM address.Test driver calls
blockBurn(cert, txIndex, eventPosInTx)before the relayer'saddBlocklands. AssertsBurnBlockeddecoded fields match the BurnEvent payload, then pollsquery_refund_processeduntil therelayer submits the refund. Final assertions:
isBurnProcessed == false,isBurnBlocked == true, ERC-20 recipient balance unchanged, owner_b's chain-B balance restored to the pre-burn level,refunds_completedmetric incremented.
Release Plan
mainbranch.Links