Skip to content

Log proof methods

Log proofs are provided by four methods. Proofs describes what a log proof establishes, how proving on request operates, and how proofs are priced; this page is the method reference.

MethodDescription
willow_quoteLogsProofThe cost of a proof over a filter and the time required to produce it. Not charged.
willow_getLogsProofThe proof for an eth_getLogs filter, or a queued job for blocks without one.
willow_getProofA stored SP1 proof, by the id a part specifies.
willow_getProofJobThe status of a proof job.

Takes the same filter object as eth_getLogs and returns a quote. Nothing is charged or queued. The fields are described under Quotes and acceptance.

{"jsonrpc":"2.0","id":1,"method":"willow_quoteLogsProof","params":[{
"fromBlock":"0x18e6448",
"toBlock":"0x18e6449",
"address":"0x000000000004444c5dc75cb358380d2e3de08a90"
}]}

Takes the same filter object as eth_getLogs, with any topics, and returns the proof for it. Use explicit block numbers. Blocks in the range without a proof are proven on request. latest resolves to the newest block Willow holds, which usually has no proof yet, so a request that uses it queues a job. A request whose new blocks cost more than a set amount requires "acceptQuote": true (see Quotes and acceptance).

Request:

{"jsonrpc":"2.0","id":1,"method":"willow_getLogsProof","params":[{
"fromBlock":"0x18e6448",
"toBlock":"0x18e6449",
"address":"0x000000000004444c5dc75cb358380d2e3de08a90",
"topics":["0x40e9cecb9f5f1f1c5b9c97dec2917b7ee92e57ba5563708daca94dd84ad7112f"]
}]}

Response (truncated):

{
"id": 1,
"jsonrpc": "2.0",
"result": {
"chainId": "0x1",
"fromBlock": "0x18e6448",
"parts": [
{
"proof": "l1/26108992-26109055",
"publicValues": "0x0000000000000000000000000000000000000000000000000000000000000020…",
"ranges": [
{
"lo": "0x15f",
"records": [
{
"block": "0x18e6447",
"record": "0x000000a30000022a000000000004444c5dc75cb358380d2e3de08a900340e9cecb…"
},
… 38 more
],
"siblings": [
"0x29b647b92c8382f2fb16bf6890fd7f4e054f14c08f5350161deb4cf64aed0693",
"0xc2c1f20d27e7299e1d5629550126dde5935791396b21603365edba01ce47d46d",
… 18 more
]
}
],
"type": "segment"
}
],
"toBlock": "0x18e6449"
}
}

A block part, for an Ethereum block that is not in a segment, has this form:

{
"proof": "leaf/26117437-59c607c93905a791e6b01a73edd8df25405f8f2b39d5c1bb0e4c253dc44cac6c",
"publicValues": "0x0000000000000000000000000000000000000000000000000000000000000020…",
"records": "0x0000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc203ddf252ad1b…",
"type": "block"
}

A leaf part, in this example a 64-block Base leaf, contains one string per block:

{
"proof": "leaf/52140160-52140223-4713e20111d7506544cdb821e7f878abde0ebb9bf49b6c83ff68c4ef1cd0d7f1",
"publicValues": "0x0000000000000000000000000000000000000000000000000000000000000020…",
"records": [
"0x00000002000000002df380544b88adb3ad0a94100dcc45fd705aae2d040c396cd989a39f…",
… 63 more
],
"type": "leaf"
}
  • chainId, fromBlock, toBlock: hexadecimal quantities. The block range is the filter’s, with tags resolved.
  • parts: contiguous, in block order. type is "segment", "block", or "leaf".
  • publicValues: the statement the SP1 proof attests to. It contains the chain, the block range, the parent hash of the first block, the hash of the last block, and a commitment to the logs. A segment’s public values also identify the block program from which it was built.
  • proof: the id of the SP1 proof, retrieved with willow_getProof. Ids are l1/<start>-<end> for a segment, leaf/<number>-<block hash> for a one-block leaf, and leaf/<start>-<end>-<hash of the end block> for a leaf of many blocks.
  • ranges (segment parts only): one Merkle range proof per index range the filter requires. lo is the position of the first entry in the segment’s index, records contains the entries in that range with their block numbers, and siblings contains the Merkle siblings. A range can include entries immediately outside the requested blocks (in the example, one from block 0x18e6447). The verifier applies the filter to proven entries, so such entries do not appear in the result.
  • records (block and leaf parts): every log in the block, as encoded entries concatenated in log order. A leaf part contains a list of these, one per block of the leaf, in block order.

Each entry in records encodes one log, big-endian:

transactionIndex 4 bytes
logIndex 4 bytes
address 20 bytes
topic count 1 byte (0 to 4)
topics 32 bytes each
data length 4 bytes
data (data length) bytes

The first entry of the segment example decodes as transaction index 0xa3, log index 0x22a, address 0x000000000004444c5dc75cb358380d2e3de08a90, and 3 topics, the first being the Uniswap v4 Swap signature.

Returns a stored SP1 proof by the id a part specifies. The proof is the serialized SP1 proof with its public values, hex-encoded. Sampled proofs were about 1.27 MB (about 2.5 million hex characters). One segment proof serves every filter over that segment, so proofs should be cached by id.

Retrieving the parts of a proof already paid for is not charged. Retrieving any other proof by id charges for its blocks, unless the account has already paid for them.

Request:

{"jsonrpc":"2.0","id":3,"method":"willow_getProof","params":["l1/26108992-26109055"]}

Response (truncated):

{
"id": 3,
"jsonrpc": "2.0",
"result": {
"id": "l1/26108992-26109055",
"proof": "0x0100000000000000000000000000000013424101303762058048500924876801…"
}
}

Errors:

{"error":{"code":-32602,"message":"bad proof id \"l1/../../etc\""},"id":4,"jsonrpc":"2.0"}
{"error":{"code":-32000,"message":"stored proof l1/1-2 not found"},"id":5,"jsonrpc":"2.0"}

Returns the status of a proof job, identified by the jobId from a queued response, on the chain on which it was queued. A job can be read only with the key that created it. A request with any other key returns proof job <id> not found.

Request:

{"jsonrpc":"2.0","id":1,"method":"willow_getProofJob","params":["j-1791025283-51e16463"]}

Response:

{
"id": 1,
"jsonrpc": "2.0",
"result": {
"chain": 1,
"createdAt": 1791025283,
"error": null,
"finishedAt": 1791025601,
"firstProofAt": 1791025580,
"from": 26111374,
"id": "j-1791025283-51e16463",
"provedBlocks": 4,
"startedAt": 1791025288,
"status": "done",
"to": 26111377,
"totalBlocks": 4
}
}
  • status: queued, running, done, or failed. A failed job’s charge is refunded.
  • from, to, totalBlocks, provedBlocks: the job’s blocks, and the number proven so far.
  • createdAt, startedAt, firstProofAt, finishedAt: Unix seconds; null until reached.
  • error: the reason the job failed, or null.

Blocks are proven with SP1 in leaves: one block per leaf on Ethereum, and on the other chains a run of blocks starting at a multiple of the leaf size. A leaf’s proof commits every log of its blocks, linked to the parent hash of its first block and the hash of its last. Complete windows of leaves (starting at a multiple of the segment size) are then combined into a final segment, whose SP1 proof commits a sorted index of those blocks’ logs.

ChainBlocks per leafBlocks per segment
Ethereum164
Base64128
Arbitrum One2,0484,096
Robinhood Chain1,0242,048
OP Mainnet128256
Unichain1,0242,048
Ink1,0242,048

A proof response consists of parts that cover the requested blocks in order.

  • A segment part covers blocks in a final segment. It contains the segment’s public values, the id of its SP1 proof, and Merkle range proofs for the portions of the index the filter can match. No new SP1 proof is generated per query.
  • A block part covers one Ethereum block that is not in a final segment. It contains the block’s public values, the id of its SP1 proof, and every log in the block, which the verifier filters. These parts are therefore large: one recent block’s part was 276 KB.
  • A leaf part covers a leaf of many blocks that is not in a final segment. It contains every log of each of its blocks, so these parts are considerably larger: one 64-block Base leaf was 19 MB of JSON, 2 MB with gzip. Send Accept-Encoding: gzip.

A job proves whole leaves, so on chains with leaves of many blocks its from and to can extend beyond the requested blocks (only the requested blocks are charged). When the chain has produced every block of a leaf’s segment window, the request also queues the window’s other unproven leaves, so that they can be combined into a segment and later proofs over it remain compact.

A proof response that would be too large, or that takes more than 20 seconds to build, is refused with -32002 (HTTP 200), never truncated.

LimitValue
Entries in segment range proofs10,000 (every log of each segment it spans with the filter’s address and topic0; the client applies the other topics)
Segments in one proof48 (3,072 blocks on Ethereum; 48 times the segment size on the other chains)
Parts outside a segmentEthereum: 256 blocks. Other chains: leaves totaling up to 256 blocks, and always at least one (4 leaves on Base, 2 on OP Mainnet, 1 on the others). With more than one part, 16 MiB of their logs.
Time to build the response20 seconds
Index ranges per segment1,024 (one per address and topic0 pair; beyond it, -32602)

Each message states which limit was reached and how to narrow the range. One recent Ethereum block contained about 138 KB of log entries, so the 16 MiB budget allows roughly 120 comparable blocks outside a segment.

Proof jobs are limited per key and per chain. A request over a limit is refused with -32002, and the message identifies the limit. A lower max blocks per job and max open jobs can be set on a key.

ChainBlocks without a proof in one request
Ethereum256
Base512
OP Mainnet1,024
Robinhood Chain16,384
Arbitrum One, Unichain, Ink32,768

A key can have 2 proof jobs open on each chain on Pay as you go, and 4 on Builder and Scale. No daily limit applies, because every block is paid for before it is queued.

These limits count blocks queued for proving. On chains with leaves of many blocks, a request queues whole leaves, plus the other leaves of any window it completes, and every block of them counts: one block without a proof on Base counts as 64 or 128.

The corresponding messages are:

  • range too large: N blocks in this range have no proof yet; one request can queue at most M; narrow the range
  • proof jobs not available for this key right now: it has N open and this range needs M more; at most K can be open at once; retry when one finishes

When proving is unavailable, the request is refused with -32000 on-demand proving is not available right now, and /v1/rpc/health/proofs reports "available": false. Blocks beyond the newest block cannot be queued.