Skip to content

Transaction and receipt proofs

Three methods prove a transaction, a receipt, or all of a block’s receipts against the block hash. No SP1 proof is involved, so the response is returned immediately, on every chain.

A block’s header commits to every transaction and receipt in the block. Its transactionsRoot and receiptsRoot are the roots of two Merkle-Patricia tries that contain each transaction’s and each receipt’s consensus bytes, keyed by the item’s position in the block. A proof contains the block’s header, as RLP, and the items from which the response is computed, each with the trie nodes on its path from the root. A proof of a whole block instead contains every transaction and receipt, from which the verifier reconstructs both roots.

MethodParamsProvesContains
willow_getTransactionProofA transaction hash, or a block and an indexThe eth_getTransactionByHash responseThe transaction. For an OP Stack deposit, also its receipt, which contains the deposit’s nonce.
willow_getReceiptProofA transaction hash, or a block and an indexThe eth_getTransactionReceipt responseThe receipt and its transaction, the previous receipt (for gasUsed), and, on the OP Stack chains, the block’s first transaction, its L1 info deposit (for the L1 fee fields).
willow_getBlockReceiptsProofA blockThe eth_getBlockReceipts response, and every transaction object of the blockEvery transaction and receipt of the block.

A block is a number, a tag, a block hash, or {"blockHash": …} / {"blockNumber": …} (EIP-1898), as for eth_getBlockReceipts. The params of the first two methods can therefore be ["0x4560…87c"], ["0x18e8f2a", "0x0"], or [{"blockHash": "0xdb98…53f"}, "0x0"].

These proofs are metered in units by their gzipped size, like any other response, and are not charged per block. They are available on every plan, including Free, and for every block Willow holds, including history.

The verifier computes the standard response from the proven bytes and the header, in the same way Willow computes the data it serves (see How Willow verifies data). Each field is either contained in those bytes or computed from them: for example, a transaction’s hash and from, the gasPrice of a fee-market transaction, a receipt’s gasUsed, contractAddress, and effectiveGasPrice, the blob gas fields, the OP Stack L1 fee and deposit fields, and Nitro’s l1BlockNumber. blockHash, blockNumber, and blockTimestamp are taken from the proven header. This covers every field of the response except the following:

  • Log indexes of a single receipt. A log’s logIndex is its position among all the logs of the block, so it depends on every earlier receipt. A receipt proof contains only the previous receipt, so it proves logIndex for a block’s first two transactions and omits it from the third onward. willow_getBlockReceiptsProof proves it for every log.
  • gasUsedForL1 and timeboosted on Arbitrum One and Robinhood Chain receipts. No block data determines these fields, so they are served as the provider reported them and are not covered by any proof.
  • One block hash. The proof establishes that the items are in the block whose header it contains. That block’s hash is confirmed with a node or light client of the chain selected by the verifying party.
  • Keccak-256 and the chain’s signature scheme, as relied on by the chain itself: the hashes bind the items to the header, and a signed transaction’s from is recovered from its signature.
  • The verifier’s code, which computes the derived fields from the proven bytes. For Ethereum’s blobGasPrice, this includes the verifier’s table of blob fee forks, so a verifier that predates a new blob fork reports a discrepancy in that field until it is updated.

The block and the transaction are verified against the original request (the requested hash or position), never against the server’s response, so no other dependency on Willow’s server exists.

Request:

{"jsonrpc":"2.0","id":2,"method":"willow_getReceiptProof","params":["0x4560d377ceb62f875b054d83a86caabe34e7f681271f2d204ac692a472cf987c"]}

Response (truncated; this transaction is the first in its block, so there is no previous receipt):

{
"id": 2,
"jsonrpc": "2.0",
"result": {
"blockHash": "0xdb98a28f352acb72751ec8012c5a617acfb2d7469aadb9bc4690db6b6dd8d53f",
"blockNumber": "0x18e8f2a",
"chainId": "0x1",
"header": "0xf90286a0834f5b002df089d24ecbce7da6966b10fdaf01d953aaa55a6783f07b…",
"receiptProofs": [
{
"index": "0x0",
"proof": [
"0xf90131a07d5015253d479db7c33d3f289ff5be07268c6991e2422acb15df3a6c…",
"0xf851a0890e88aba4e1656ab2e6573c7ce99aa9d48d6b77548fd01b039f7fe085…",
"0xf9063d20b9063902f90635018303b309b9010000000000000000000400000000…"
],
"value": "0x02f90635018303b309b901000000000000000000040000000000000000000000…"
}
],
"transactionIndex": "0x0",
"transactionProofs": [
{
"index": "0x0",
"proof": [
"0xf90131a05eb1fdbdc9e0bda5e2ccd6d30af827e48965362aa3b7568b0d343e82…",
"0xf851a08ade0b72943ea4d0b80dcfec145b5f991e82ef8ebffe6d6ef6c996a2bc…",
"0xf9035820b9035402f90350018302260e808413f1d6838306d871940000000aa2…"
],
"value": "0x02f90350018302260e808413f1d6838306d871940000000aa232009084bd71a5…"
}
]
}
}
  • chainId, blockNumber, blockHash: hexadecimal. The verifier recomputes the block hash from header and reads the number from it, and both must match.
  • header: the block header’s RLP encoding. Its keccak256 is the block hash.
  • transactionIndex (transaction and receipt proofs only): the transaction’s position in the block.
  • transactionProofs, receiptProofs (transaction and receipt proofs only): one entry per item. value is the item’s consensus bytes as stored in the trie: a transaction’s EIP-2718 encoding, and, for a receipt, its type byte (none for a legacy receipt) followed by the RLP of its status, cumulative gas used, logs bloom, and logs (and, on the OP Stack, a deposit’s nonce and receipt version). proof is the list of trie nodes from the root down to the item, root first. Item i is stored under the key rlp(i).
  • transactions, receipts (block proofs only): every transaction’s and receipt’s consensus bytes, in block order, instead of item proofs.

In testing, a transaction proof was 2 to 9 KB of JSON and a receipt proof 3 to 94 KB, since it contains the receipt’s logs. A block’s receipts proof was 650 KB to 1.1 MB on Ethereum (229 and 355 transactions), 170 to 820 KB on OP Mainnet and Base, and 5 to 12 KB on the other chains.

A request for a transaction Willow does not hold is refused with -32000, as with the standard methods. An index beyond the block’s last transaction is refused with -32602:

{"error":{"code":-32602,"message":"invalid params: block 26119978 has 229 transactions; there is no transaction 4096"},"id":6,"jsonrpc":"2.0"}

Verifying proofs describes verification of these proofs in a browser or from the command line.