linera-io/linera-protocol · error

log_index out of range

Error message

log_index out of range

What it means

process_deposit converts the u64 log_index to usize with usize::try_from and expects success. On wasm32 (where Linera contracts run), usize is 32-bit, so any log_index >= 2^32 fails here; the following assert! then separately rejects indices >= logs.len(). The checked cast is a documented replay-guard: an unchecked cast would alias log_index and log_index + 2^32 to the same log while hashing them to different DepositKeys, enabling a double mint.

Source

Thrown at linera-bridge/contracts/evm-bridge/src/contract.rs:235

        }

        // 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);
        let deposit = proof::parse_deposit_event(&logs[log_index_usize], bridge_contract)
            .expect("failed to parse DepositInitiated event");

        // 4. Validate deposit fields against bridge parameters
        assert_eq!(
            deposit.source_chain_id.as_limbs()[0],
            params.source_chain_id,

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. As the submitter: validate 0 <= log_index < receipt.logs.len() and log_index <= u32::MAX before submitting, since the contract runs on wasm32
  2. Fix relayer arithmetic that packs or shifts log indices into high bits
  3. If you maintain the contract and want a gentler failure, keep the checked cast but convert the expect into an assert! with the offending value in the message
  4. Treat any occurrence in production as a red flag: inspect who submitted the operation, since values >= 2^32 indicate malice rather than accident

Example fix

// before (submitter skips validation)
let log_index = compute_log_index(); // may exceed u32::MAX on wasm32
submit(ProcessDeposit { log_index, .. }); // aborts: log_index out of range

// after (preflight in the relayer)
let log_index = compute_log_index();
assert!(log_index <= u32::MAX as u64, "log_index {log_index} not representable on wasm32");
assert!((log_index as usize) < receipt.logs.len(), "log_index beyond receipt logs");
submit(ProcessDeposit { log_index, .. });
Defensive patterns

Strategy: validation

Validate before calling

// Preflight both guards before submitting:
assert!(log_index <= u32::MAX as u64, "wasm32 usize overflow");
assert!((log_index as usize) < logs.len(), "index beyond receipt logs");
submit(ProcessDeposit { log_index, .. });

Type guard

fn log_index_submittable(log_index: u64, logs_len: usize) -> bool {
    log_index <= u32::MAX as u64 && (log_index as usize) < logs_len
}

Try / catch

// A failing ProcessDeposit aborts atomically; alert on it — indices >= 2^32
// indicate a relayer integer bug or deliberate probing, both worth paging on.

Prevention

When it happens

Trigger: A relayer bug or a malicious submitter crafts log_index with the high 32 bits set (e.g. 0x1_0000_0001) targeting a real log; log_index is computed with an arithmetic overflow that produces a huge value; fuzzing the ProcessDeposit operation with extreme integers.

Common situations: Adversarial probing of the bridge by third parties; relayer integer bugs when log_index is derived from packed bitfields; test suites exercising the wasm32 truncation edge case described in the source comment.

Related errors


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