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

  1. Submit the exact receipt bytes from eth_getTransactionReceipt without re-encoding
  2. If you build receipts yourself, round-trip them through the same alloy version the contract uses and assert decode_receipt_logs succeeds locally
  3. For typed receipts, ensure the type byte is preserved as the first RLP list element
  4. 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

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

Related errors


AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22). Data as JSON: /api/errors/49b17b862e7306eb. Report an issue: GitHub.