Skip to content

feat: Deferred block proving#1725

Open
sergerad wants to merge 82 commits intonextfrom
sergerad-deferred-block-proving
Open

feat: Deferred block proving#1725
sergerad wants to merge 82 commits intonextfrom
sergerad-deferred-block-proving

Conversation

@sergerad
Copy link
Collaborator

@sergerad sergerad commented Mar 2, 2026

Context

We are adding deferred (asynchronous) block proving for the node, as described #1592. Currently, block proving happens synchronously during apply_block, which means block commitment is blocked until the proof is generated.

Blocks will now exhibit committed (not yet proven) and proven states. A committed block is already part of the canonical chain and fully usable. Clients that require proof-level finality can opt into it via the new finality parameter on SyncChainMmr.

Changes

  • Schema: Added proving_inputs BLOB to the block_headers table, with partial index for querying proven (proving_inputs = NULL) blocks.
  • Block proof file storage: Block proofs are stored as files via BlockStore (following the existing block file pattern) rather than as BLOBs in SQLite.
  • DB queries: Added mark_block_proven, select_block_proving_inputs (returns deserialized BlockProofRequest), and select_latest_proven_block_num.
  • Decoupled proving from apply_block: The BlockProofRequest is now serialized and persisted alongside the block during apply_block.
  • Proof scheduler: Added a background task (proof_scheduler.rs) that drives deferred proving. It queries unproven blocks on startup (restart recovery), listens for new block commits via Notify, and proves blocks concurrently using FuturesOrdered for FIFO completion ordering. Proofs are saved to files, then the block is marked proven in the DB.
  • Finality on SyncChainMmr: Added a Finality enum (COMMITTED, PROVEN) to the protobuf and a finality field on SyncChainMmrRequest.
  • Refactored apply_block query: Introduced ApplyBlockData struct to replace the 7-parameter function signature.

@sergerad sergerad changed the title Sergerad deferred block proving feat: Deferred block proving Mar 2, 2026
@sergerad sergerad marked this pull request as ready for review March 2, 2026 23:42
@sergerad sergerad requested review from Mirko-von-Leipzig, bobbinth and drahnr and removed request for Mirko-von-Leipzig March 2, 2026 23:43
Comment on lines +625 to +628
/// Returns the highest block number that has been proven, or `None` if no blocks have been
/// proven yet.
#[instrument(level = "debug", target = COMPONENT, skip_all, ret(level = "debug"), err)]
pub async fn select_latest_proven_block_num(&self) -> Result<Option<BlockNumber>> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we treat the genesis block? Should it not always be considered proven?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the comments to reflect - we treat the genesis block as proven

// Mark all sequentially proven blocks as completed.
while latest_complete.child().as_u32() < lowest_in_flight.as_u32() {
latest_complete = latest_complete.child();
db.mark_block_proven(latest_complete)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: this now breaks the concept of having a signle write-only-connection since we now might end up here if we did proof all instances while apply_block is still running. Am I missing something?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apply_block will only ever affect a single row one time right? and this query (col delete) will only ever happen after that

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine as far as I can tell, we just need to find a better model than the single writer connection approach CC @Mirko-von-Leipzig

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have an alternative which we can discuss and pursue in a follow-up PR.

We can add another Watch channel which is the inverse of the apply_block::chain_tip --> proof_scheduler one. The proof_scheduler never updates the DB itself, it just sets the latest proven block in the watch channel. On every apply_block, we update the proven block as well.

This adds a bit of latency to the marking, but considering the latency itself is 30s+, adding another 3s (worst case, avg 1.5s) doesn't seem too bad.

Whether this is worth it 🤷 I do like isolating database writes so we know for sure there are no problems.

Copy link
Contributor

@drahnr drahnr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the logic is sound, I don't particularly like the logic inversion when it comes marking blocks as proven, since it's error rather error prone.

block_header BLOB NOT NULL,
signature BLOB NOT NULL,
commitment BLOB NOT NULL,
proving_inputs BLOB, -- Serialized BlockProofRequest needed for deferred proving. NULL if it has been proven or never proven (genesis block).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: leave a TODO that the size might become a problem in the future

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants