linera-io/linera-protocol · error

too many proof nodes: {} (max {})

Error message

too many proof nodes: {} (max {})

What it means

verify_receipt_inclusion enforces MAX_PROOF_NODES = 32 on the Merkle-Patricia-Trie proof node list before running verification. The constant is sized ~3x the theoretical maximum receipts-trie depth (an Ethereum block fits ~1,400 txs, giving depth ~11), so more than 32 nodes is either malicious (DoS) or not a receipts proof at all.

Source

Thrown at linera-bridge/src/proof/mod.rs:349

const MAX_PROOF_NODES: usize = 32;

/// Maximum total bytes across all proof nodes (32 KiB).
///
/// Each MPT branch node has 17 children (16 nibbles + value), each a 32-byte hash
/// plus RLP overhead, for a worst-case size of ~600 bytes per node.
/// With 32 nodes that's ~19,200 bytes; 32 KiB (32,768) is ~1.7× the theoretical max.
const MAX_PROOF_BYTES: usize = 32 * 1024;

/// Verifies that a receipt is included in the receipts trie via MPT proof.
///
/// Enforces DoS limits on the proof size before forwarding to the trie verifier.
pub fn verify_receipt_inclusion(
    receipts_root: B256,
    tx_index: u64,
    receipt_rlp: &[u8],
    proof_nodes: &[Bytes],
) -> Result<()> {
    ensure!(
        proof_nodes.len() <= MAX_PROOF_NODES,
        "too many proof nodes: {} (max {})",
        proof_nodes.len(),
        MAX_PROOF_NODES
    );
    let total_bytes: usize = proof_nodes.iter().map(|n| n.len()).sum();
    ensure!(
        total_bytes <= MAX_PROOF_BYTES,
        "proof too large: {total_bytes} bytes (max {MAX_PROOF_BYTES})",
    );

    let key = receipt_trie_key(tx_index);
    alloy_trie::proof::verify_proof(receipts_root, key, Some(receipt_rlp.to_vec()), proof_nodes)
        .map_err(|e| anyhow!("MPT proof verification failed: {e}"))
}

/// Decodes a receipt's RLP and extracts its logs.
///

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Confirm the proof comes from eth_getProof-style receipts data for the right block, and that the trie root is the block header's receiptsRoot
  2. If legitimately deeper trees are expected on your chain, re-derive MAX_PROOF_NODES from its gas limit/tx throughput rather than removing the check
  3. Treat repeated occurrences from one source as adversarial and reject the relayer's messages
Defensive patterns

Strategy: validation

Validate before calling

// Cheap pre-check mirroring the library's DoS bound (MAX_PROOF_NODES = 32)
const MAX_PROOF_NODES: usize = 32;
if proof_nodes.len() > MAX_PROOF_NODES {
    return Err(anyhow::anyhow!("rejecting proof: {} nodes", proof_nodes.len()));
}
verify_receipt_inclusion(root, tx_index, receipt, proof_nodes)?;

Try / catch

match verify_receipt_inclusion(root, idx, rlp, nodes) {
    Ok(()) => Ok(()),
    Err(e) if e.to_string().contains("too many proof nodes") => {
        // not a legitimate receipts proof: drop message, do not retry
        Err(anyhow::anyhow!("malicious or wrong-trie proof rejected"))
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: Submitting a receipt inclusion proof whose node array exceeds 32 entries: hand-crafted adversarial proofs in fuzzing, or mistakenly passing an account/state-trie proof (deeper tree) where a receipts proof belongs.

Common situations: Bridge/relayer tests generating proofs from the state trie instead of the receipts trie; a malicious relayer padding the node list; upstream proof builder emitting every trie level including leaf+extension for a pathological key.

Related errors


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