feat(settlement): persist block → proof-request mapping in the DB + RPC - #631
Merged
Merged
Conversation
…lter
Pin sp1_sdk::network::prover=info in DEFAULT_LOG_FILTER so the prover-network
submit log ("Created request <id> in transaction <tx>") is captured for every
SP1 settlement proof, independent of the trailing global default. Adds a test
that the default filter parses (a malformed directive otherwise only fails at
node startup).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
⚠️ Performance Alert ⚠️
Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.30.
| Benchmark suite | Current: 8d93675 | Previous: 89628fa | Ratio |
|---|---|---|---|
FinalityStatus/compress |
1 ns/iter (± 0) |
0 ns/iter (± 0) |
+∞ |
This comment was automatically generated by workflow using github-action-benchmark.
CC: @kariy
Codec benchmark diff vs
|
| Benchmark | Baseline (ns) | Current (ns) | Δ |
|---|---|---|---|
CompiledClass(fixture)/compress |
2626367 | 2665983 | +1.51% |
CompiledClass(fixture)/decompress |
2773110 | 2815640 | +1.53% |
ExecutionCheckpoint/compress |
33 | 32 | -3.03% |
ExecutionCheckpoint/decompress |
26 | 23 | -11.54% |
PruningCheckpoint/compress |
33 | 32 | -3.03% |
PruningCheckpoint/decompress |
25 | 23 | -8.00% |
VersionedHeader/compress |
635 | 654 | +2.99% |
VersionedHeader/decompress |
903 | 840 | -6.98% |
StoredBlockBodyIndices/compress |
78 | 76 | -2.56% |
StoredBlockBodyIndices/decompress |
39 | 34 | -12.82% |
StorageEntry/compress |
172 | 143 | -16.86% |
StorageEntry/decompress |
160 | 139 | -13.13% |
ContractNonceChange/compress |
173 | 146 | -15.61% |
ContractNonceChange/decompress |
304 | 238 | -21.71% |
ContractClassChange/compress |
216 | 188 | -12.96% |
ContractClassChange/decompress |
312 | 259 | -16.99% |
ContractStorageEntry/compress |
169 | 153 | -9.47% |
ContractStorageEntry/decompress |
375 | 312 | -16.80% |
GenericContractInfo/compress |
129 | 132 | +2.33% |
GenericContractInfo/decompress |
106 | 101 | -4.72% |
Felt/compress |
90 | 80 | -11.11% |
Felt/decompress |
63 | 52 | -17.46% |
BlockHash/compress |
89 | 80 | -10.11% |
BlockHash/decompress |
62 | 51 | -17.74% |
TxHash/compress |
91 | 80 | -12.09% |
TxHash/decompress |
62 | 53 | -14.52% |
ClassHash/compress |
89 | 81 | -8.99% |
ClassHash/decompress |
63 | 52 | -17.46% |
CompiledClassHash/compress |
90 | 80 | -11.11% |
CompiledClassHash/decompress |
63 | 51 | -19.05% |
BlockNumber/compress |
51 | 46 | -9.80% |
BlockNumber/decompress |
24 | 24 | +0.00% |
TxNumber/compress |
51 | 46 | -9.80% |
TxNumber/decompress |
25 | 24 | -4.00% |
FinalityStatus/compress |
0 | 1 | +Infinity% |
FinalityStatus/decompress |
11 | 11 | +0.00% |
TypedTransactionExecutionInfo/compress |
15126 | 17735 | +17.25% |
TypedTransactionExecutionInfo/decompress |
3738 | 3635 | -2.76% |
VersionedContractClass/compress |
372 | 382 | +2.69% |
VersionedContractClass/decompress |
803 | 785 | -2.24% |
MigratedCompiledClassHash/compress |
171 | 145 | -15.20% |
MigratedCompiledClassHash/decompress |
157 | 138 | -12.10% |
ContractInfoChangeList/compress |
1402 | 1388 | -1.00% |
ContractInfoChangeList/decompress |
2250 | 2135 | -5.11% |
BlockChangeList/compress |
606 | 608 | +0.33% |
BlockChangeList/decompress |
911 | 857 | -5.93% |
ReceiptEnvelope/compress |
26478 | 29861 | +12.78% |
ReceiptEnvelope/decompress |
6478 | 6088 | -6.02% |
TrieDatabaseValue/compress |
148 | 165 | +11.49% |
TrieDatabaseValue/decompress |
249 | 211 | -15.26% |
TrieHistoryEntry/compress |
270 | 288 | +6.67% |
TrieHistoryEntry/decompress |
286 | 246 | -13.99% |
|
Record which proof settled each block so a settled block can be traced back to
its proof (e.g. on the Succinct explorer), and expose it over JSON-RPC.
- SDK (amd-sev-snp-attestation-sdk 4ce5017): surface the network request_id on
RawProof instead of discarding it after the cost lookup.
- ProofId (katana-primitives): a prover-agnostic, opaque proof identifier. The
TEE/SP1 backend fills it with the Succinct request_id; the storage, provider,
and RPC layers stay agnostic. The prover type is a node-level config constant,
so it is not stored per block. Threaded through ProvingBackend::prove ->
settle_batch.
- New per-block SettlementProofs table (block -> ProofId); every block in a
settled batch maps to that batch's single proof. Best-effort write beside the
settled-block checkpoint, so a DB failure never stalls settlement.
- SettlementProof{Provider,Writer} provider traits + impls over the db and
forked providers.
- katana_getBlockProof(block) RPC returning { block, proofId } (proofId as
0x-hex), or null if the block was not settled with a proof.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kariy
force-pushed
the
feat/settlement-block-proof-mapping
branch
from
July 6, 2026 14:00
8bacb7d to
8d93675
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #631 +/- ##
==========================================
- Coverage 73.32% 68.07% -5.25%
==========================================
Files 209 334 +125
Lines 23132 46717 +23585
==========================================
+ Hits 16961 31802 +14841
- Misses 6171 14915 +8744 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
kariy
added a commit
that referenced
this pull request
Jul 20, 2026
…ter restart A crash or restart between proving and settling discarded the prepared payload, forcing a fresh (paid) SP1 proving round for the identical range. The proof itself still exists on the prover network though — so persist just its network reference and recover it instead. - The moment proving yields a ProofId, record it as a PendingBatchProof (range + id) in a new singleton PendingSettlementProofs table — before update_state submission. Cleared when the batch settles. - On an in-memory miss, prepare_batch resolves a matching persisted reference via the new ProvingBackend::recover, which fetches the fulfilled proof from the network (SDK-side: the new Program::recover_proof — sp1_sdk wait_proof by request id, same plumbing as gen_raw_proof minus the paid request; SDK rev bump) and rebuilds the payload from a freshly built attestation. Only range-derived fields enter the payload, so a rebuilt attestation + recovered proof is byte-equivalent in everything on-chain validation checks. Mock proving reports recovery as unsupported (it is free anyway); any recovery failure falls back to fresh proving. - Escape hatch for externally invalidated proofs: an update_state that REVERTS in execution (e.g. a TEE-registry trust-root rotation between proving and submission) drops the payload and its persisted reference, so the retry proves fresh. Pre-execution failures (fees, nonce, transport) keep both — that's the incident case where reuse is the whole point. Table addition is version-neutral (same as SettlementProofs in #631). New proof_reuse tests cover restart recovery, unsupported/failed recovery fallback, stale-range references, and reference lifetime across failed submissions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
kariy
added a commit
that referenced
this pull request
Jul 20, 2026
…it (#639) ## Why When `update_state` submission fails after proving succeeded (fees, nonce, transient RPC), the settle loop retried the **whole pipeline** — including a fresh, paid SP1 proving round — every ~60s for the identical block range. Production incident (mainnet enclave, 2026-07-17): the settlement account was short on STRK for the submission fee, and the node burned ~0.4 PROVE per retry (~24 PROVE/hour) re-proving range 96–105 in a loop that could never succeed. A node restart had the same cost: the in-flight proof died with the process even though the prover network still held it. ## What **Prove once, retry the submission** (`Worker::settle_batch`): the payload is obtained at most once per attempt and lives as a local scoped to it; an inner loop retries only the `update_state` submission (own backoff, failure metrics, cursor-idempotency re-read, shutdown-responsive via the run loop's oneshot receiver). Reuse-while-unchanged, spend-on-success, drop-on-cursor-move, and drop-on-execution-revert are all control flow rather than cache state. Terminal conditions (prove failure, execution revert, on-chain cursor moved) return `Err` to the run loop, which backs off and recomputes the range. `PiltoverClient::update_state` takes `&PiltoverInput` (the generated bindings don't derive `Clone`). **Recovery across restarts (persisted network reference):** the moment proving yields a `ProofId`, it is persisted as a `PendingBatchProof` (range + id) in a new singleton `PendingSettlementProofs` table — *before* submission — and cleared when the batch settles. On startup of an attempt, a matching reference is resolved through the new `ProvingBackend::recover`, which fetches the fulfilled proof from the prover network (SDK-side: new `Program::recover_proof` — `sp1_sdk` `wait_proof` by request id, the same plumbing as `gen_raw_proof` minus the paid request; SDK rev bump to cartridge-gg/amd-sev-snp-attestation-sdk@2685854) and rebuilds the payload from a freshly built attestation. Only range-derived fields enter the payload, so a rebuilt attestation + recovered proof is equivalent in everything on-chain validation checks. Mock proving reports recovery as unsupported (it's free anyway); recovery failure (expired/unknown id) falls back to fresh proving. **Escape hatch:** an `update_state` that *reverts in execution* (e.g. a TEE-registry trust-root rotation invalidated the proof between proving and submission) drops the payload **and** its persisted reference, so the next attempt proves fresh — retry-with-same-proof only applies to pre-execution failures (fees, nonce, transport), the incident class where it's wanted. Reuse is sound because the payload is validity-stable for a fixed historical range — `report_data` is a pure function of the settled range, and Piltover validates the proof against the range-derived commitment, not any timestamp embedded at proving time. The `PendingSettlementProofs` table addition is version-neutral (same as `SettlementProofs` in #631). `proof_generation_seconds` now records only actual proving rounds. **Operator-visible log change:** transient submission failures emit `Failed to submit state update; will retry with the same proof.`; `Failed to settle block range; will retry.` now marks terminal attempt failures only. Docs updated accordingly. ## Tests The `proof_reuse` suite drives `settle_batch`'s real submission-retry loop under a paused tokio clock against an unreachable Piltover endpoint (the incident shape), shut down after bounded virtual time: one prove across many submission retries with the persisted reference surviving, restart recovery from the reference, unsupported/failed recovery fallback to fresh proving, stale-range reference skip, prove-failure early return, and reference clearing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Records which proof settled each block, so a settled block can be traced back to its proof (e.g. on the Succinct explorer), and exposes it over JSON-RPC. Builds on the settlement observability work (proof-cost metrics, request/tx-hash logging).
Settlement proves a whole block range per batch (one proof per batch), so every block in a settled batch maps to that batch's single proof (per-block keying → O(1) lookup by any block number).
Prover-agnostic by design
The stored value is a
ProofId(katana-primitives): an opaque, prover-defined identifier. The TEE/SP1 backend fills it with the Succinct prover-network request ID, but the storage / provider / RPC layers stay agnostic. The prover type is a node-level config constant, so it is not stored per block — it can be inferred from config. A future proving backend supplies its own opaque id with zero changes below the backend.Changes
amd-sev-snp-attestation-sdk, merged as PR #2, rev4ce5017):RawProofretains the networkrequest_idinstead of discarding it after the cost lookup.Option<ProofId>throughProvingBackend::prove→settle_batch(mock / off-network →None).SettlementProofstable (block → ProofId); best-effort write beside the settled-block checkpoint, so a DB failure never stalls settlement.SettlementProof{Provider,Writer}provider traits + impls over the db and forked providers.katana_getBlockProof(block)→{ block, proofId }(proofIdas0x-hex), ornullif the block was not settled with a proof.sp1_sdk::network::prover=infoin the default log filter so the prover-network submit log is captured for every SP1 proof.Testing
ProofIdcodec round-trip, table registration, provider write/read round-trip, andpersist_block_proofs. All green.katanabinary compiles; clippy clean on the changed crates.🤖 Generated with Claude Code