linera-io/linera-protocol · error
failed to decode receipt logs
Error message
failed to decode receipt logs
What it means
Step 3 of process_deposit decodes the submitted receipt_rlp into its list of logs with proof::decode_receipt_logs and expects success. The panic means the bytes passed the earlier inclusion proof as opaque data but do not deserialize as a receipt whose logs field is a well-formed list — usually non-canonical or mismatched RLP structure for the log entries. The transaction aborts without minting.
Source
Thrown at linera-bridge/contracts/evm-bridge/src/contract.rs:228
.state
.verified_block_hashes
.contains(&block_hash.0)
.await
.expect("failed to check verified block hashes")
{
self.verify_block_hash(block_hash.0).await;
}
// 2. Verify receipt inclusion via MPT proof
let proof_bytes: Vec<Bytes> = proof_nodes
.iter()
.map(|n| Bytes::copy_from_slice(n))
.collect();
proof::verify_receipt_inclusion(receipts_root, tx_index, receipt_rlp, &proof_bytes)
.expect("receipt inclusion proof failed");
// 3. Decode receipt logs and parse the deposit event
let logs = proof::decode_receipt_logs(receipt_rlp).expect("failed to decode receipt logs");
// `log_index` is a u64 but indexes a Vec (usize). On wasm32 `usize` is
// 32-bit, so an unchecked `as usize` cast would truncate — letting
// `log_index` and `log_index + 2^32` select the same log while hashing
// to different `DepositKey`s (replay-guard bypass → double mint). A
// checked cast rejects any value that does not fit `usize`; the full
// u64 is preserved for the `DepositKey` below.
let log_index_usize = usize::try_from(log_index).expect("log_index out of range");
assert!(
log_index_usize < logs.len(),
"log_index {} out of range (receipt has {} logs)",
log_index,
logs.len()
);
let bridge_contract_bytes =
self.state.bridge_contract_address.get().expect(
"bridge contract address not registered — call RegisterFungibleBridge first",
);
let bridge_contract = alloy_primitives::Address::from(bridge_contract_bytes);View on GitHub (pinned to 6c226ddcb3)
Solutions
- Submit the exact receipt bytes from eth_getTransactionReceipt without re-encoding
- If you build receipts yourself, round-trip them through the same alloy version the contract uses and assert decode_receipt_logs succeeds locally
- For typed receipts, ensure the type byte is preserved as the first RLP list element
- Pin alloy versions between relayer and evm-bridge contract and add a local decode check to CI fixtures
Example fix
// before
let receipt_rlp = my_db_reencoded_receipt(); // hashes match proof but decodes wrong
submit(ProcessDeposit { receipt_rlp, .. });
// after
let receipt_hex = rpc.get_transaction_receipt(tx_hash).await?.unwrap().raw; // exact bytes
let receipt_rlp = hex::decode(receipt_hex.trim_start_matches("0x"))?;
debug_assert!(proof::decode_receipt_logs(&receipt_rlp).is_ok()); // preflight
submit(ProcessDeposit { receipt_rlp, .. }); Defensive patterns
Strategy: validation
Validate before calling
// Confirm the receipt decodes to logs before paying for the transaction:
if proof::decode_receipt_logs(&receipt_rlp).is_err() {
tracing::warn!("receipt RLP will fail contract decode — skipping");
return;
}
submit(ProcessDeposit { receipt_rlp, .. }); Type guard
fn receipt_has_decodable_logs(receipt_rlp: &[u8]) -> bool {
proof::decode_receipt_logs(receipt_rlp).is_ok()
} Try / catch
// Local pre-decode with the same alloy version the contract uses:
let logs = proof::decode_receipt_logs(&receipt_rlp)
.unwrap_or_else(|e| panic!("relayer produced undecodable receipt: {e}")); // fail the pipeline, not the chain Prevention
- Pass through RPC receipt bytes unmodified — no database re-serialization
- Pin alloy versions between relayer and contract
- Cover legacy vs typed receipt encodings in relayer unit tests
When it happens
Trigger: The relayer submits bytes that hash correctly under the MPT proof but were re-encoded with a different receipt schema (wrong field count/types after an EIP changed the receipt format); manual construction of receipt_rlp instead of using RPC-returned bytes; alloy version skew between the relayer's encoder and the contract's decoder.
Common situations: Relayers that store receipts in a database and re-serialize them; chain-fork or EIP-transition windows where receipt types differ (legacy vs typed receipts, EIP-1559/4844); test harnesses with handcrafted receipt fixtures.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- invalid block header RLP
- invalid log RLP: {e}
- empty receipt RLP
- receipt must be an RLP list
- receipt payload extends past available data
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/49b17b862e7306eb.
Report an issue: GitHub.