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
- 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
- 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
- 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
- Reject submissions exceeding the node cap before they reach the verifier
- Verify proofs are built against receiptsRoot (not stateRoot) so trie depth stays bounded
- Monitor rejection rates per relayer to detect adversarial sources
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
- proof too large: {total_bytes} bytes (max {MAX_PROOF_BYTES})
- block header must be an RLP list
- block header payload extends past available data
- empty receipt RLP
- receipt must be an RLP list
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/3107473cb3185408.
Report an issue: GitHub.