Skip to content
Open
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
f50435e
add block_proof column and DB queries for storing block proofs
sergerad Mar 1, 2026
9101df8
decouple proving from apply_block and persist proving inputs
sergerad Mar 1, 2026
0ef989a
add concurrent proof scheduler with FuturesOrdered for FIFO completion
sergerad Mar 1, 2026
34f0cdc
Simplify prove fns
sergerad Mar 1, 2026
7a5022a
Simplify retry fn
sergerad Mar 2, 2026
5062cd0
Fix data dir issue
sergerad Mar 2, 2026
ecc067e
add finality parameter to SyncChainMmr endpoint
sergerad Mar 2, 2026
f116fa3
RM flake
sergerad Mar 2, 2026
866b324
Fix lint
sergerad Mar 2, 2026
1f2919b
Undo fmt
sergerad Mar 2, 2026
14e16f4
Wrap up signed block todo
sergerad Mar 2, 2026
1b21962
Pass blockproofrequest down
sergerad Mar 2, 2026
671630a
Lint
sergerad Mar 2, 2026
b6f3d3c
Fix stress tests
sergerad Mar 2, 2026
6856585
Changelog
sergerad Mar 2, 2026
3b0ba10
Fix proving inputs
sergerad Mar 2, 2026
98c0aa6
Handle docstring
sergerad Mar 2, 2026
1189d46
Update genesis comment
sergerad Mar 2, 2026
a2b5952
RM arc clone
sergerad Mar 2, 2026
8a65851
load_proving_inputs comments
sergerad Mar 2, 2026
d355dc5
Comments
sergerad Mar 2, 2026
769d2bf
refactor errors and retries
sergerad Mar 2, 2026
633f761
Merge branch 'next' of github.com:0xMiden/miden-node into sergerad-de…
sergerad Mar 2, 2026
549d808
Tidy up future results
sergerad Mar 2, 2026
dab79e4
Comments
sergerad Mar 2, 2026
98d56fa
Merge branch 'next' of github.com:0xMiden/miden-node into sergerad-de…
sergerad Mar 3, 2026
c08b657
Fix compile
sergerad Mar 3, 2026
258bafc
Rm dead code fields
sergerad Mar 3, 2026
bd8dad7
impl conv::SqlTypeConv for BlockProof
sergerad Mar 3, 2026
2b44192
Add index update query
sergerad Mar 3, 2026
92878d8
Bump timeout
sergerad Mar 3, 2026
7d5ed4c
Update notify
sergerad Mar 3, 2026
2c5a7d7
Specify proving block batch size
sergerad Mar 3, 2026
8b803f7
static lifetime
sergerad Mar 3, 2026
2ca641f
backticks
sergerad Mar 3, 2026
6e52650
replace match
sergerad Mar 3, 2026
295f9d8
Store proofs to file
sergerad Mar 3, 2026
5bfba05
update select proving inputs return value
sergerad Mar 3, 2026
37cb516
RM pub crate
sergerad Mar 3, 2026
ec89137
Fix changelog
sergerad Mar 3, 2026
bc0bd54
Changelog
sergerad Mar 4, 2026
c8a76d4
finality unspecified
sergerad Mar 4, 2026
506ea80
unspecified comment
sergerad Mar 4, 2026
97b320b
More comments
sergerad Mar 4, 2026
820261d
source not from
sergerad Mar 4, 2026
b3f0238
arc clone
sergerad Mar 4, 2026
228d38b
rename proof scheduler handle
sergerad Mar 4, 2026
d272347
refactor proof concurrency
sergerad Mar 4, 2026
4c216c2
Merge branch 'next' of github.com:0xMiden/miden-node into sergerad-de…
sergerad Mar 4, 2026
9b2f3c8
lint
sergerad Mar 4, 2026
03b0415
Merge branch 'next' of github.com:0xMiden/miden-node into sergerad-de…
sergerad Mar 4, 2026
da22438
RM unused query and index
sergerad Mar 4, 2026
359a056
Merge branch 'next' of github.com:0xMiden/miden-node into sergerad-de…
sergerad Mar 8, 2026
0e3145f
Move retry logic to prove_and_save
sergerad Mar 8, 2026
75cca6e
PendingJoinSet
sergerad Mar 8, 2026
20c1720
Simplify scheduling logic
sergerad Mar 8, 2026
3bbb943
Rename var
sergerad Mar 8, 2026
433085b
instrument field name
sergerad Mar 9, 2026
aa4f32a
parameterize max concurrent proofs
sergerad Mar 9, 2026
ee6b74a
Merge branch 'next' of github.com:0xMiden/miden-node into sergerad-de…
sergerad Mar 10, 2026
8a70c04
joinset type specific
sergerad Mar 10, 2026
4238cf1
flatten error
sergerad Mar 10, 2026
2720950
unwrap or
sergerad Mar 10, 2026
e0d9b55
retry loop
sergerad Mar 10, 2026
4e71764
instrument refactor
sergerad Mar 10, 2026
1cb0546
Fix info block num fields
sergerad Mar 10, 2026
3bfce8c
Merge branch 'next' of github.com:0xMiden/miden-node into sergerad-de…
sergerad Mar 12, 2026
3e1e3a9
rm is_proven and wipe proving_inputs on proven
sergerad Mar 12, 2026
28f359f
Merge branch 'next' of github.com:0xMiden/miden-node into sergerad-de…
sergerad Mar 12, 2026
1b0712d
nonzerousize
sergerad Mar 12, 2026
8292df7
Update select block num comment
sergerad Mar 12, 2026
d8bb994
anyhow context
sergerad Mar 12, 2026
3044cea
another anyhow context
sergerad Mar 12, 2026
b8aa91c
Fix fields
sergerad Mar 12, 2026
905a298
log loop errs
sergerad Mar 12, 2026
ca54ff2
assert_matches
sergerad Mar 12, 2026
a605383
std io err
sergerad Mar 12, 2026
7c8fba7
Simplify schedule logic
sergerad Mar 12, 2026
1dd667a
mark proven in prove_and_save
sergerad Mar 12, 2026
e539861
saturating sub
sergerad Mar 16, 2026
3acd808
Merge branch 'next' of github.com:0xMiden/miden-node into sergerad-de…
sergerad Mar 16, 2026
346bf15
rm too many lines
sergerad Mar 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- Introduce `SyncChainMmr` RPC endpoint to sync chain MMR deltas within specified block ranges ([#1591](https://github.com/0xMiden/node/issues/1591)).
- Fixed `TransactionHeader` serialization for row insertion on database & fixed transaction cursor on retrievals ([#1701](https://github.com/0xMiden/node/issues/1701)).
- Added KMS signing support in validator ([#1677](https://github.com/0xMiden/node/pull/1677)).
- Restructured block proving to be asynchronous and added finality field for `SyncChainMmr` requests ([#1725](https://github.com/0xMiden/miden-node/pull/1725)).

### Changes

Expand Down
3 changes: 1 addition & 2 deletions bin/remote-prover/src/server/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use std::num::NonZeroUsize;
use std::sync::Arc;
use std::time::Duration;

use assert_matches::assert_matches;
use miden_protocol::MIN_PROOF_SECURITY_LEVEL;
use miden_protocol::account::auth::AuthScheme;
use miden_protocol::asset::{Asset, FungibleAsset};
Expand Down Expand Up @@ -239,7 +238,7 @@ async fn capacity_is_respected() {
result.sort_unstable();
assert_eq!(expected, result);

assert_matches!(first.err().or(second.err()).or(third.err()), Some(err) => {
assert_matches::assert_matches!(first.err().or(second.err()).or(third.err()), Some(err) => {
assert_eq!(err.code(), tonic::Code::ResourceExhausted);
});

Expand Down
11 changes: 8 additions & 3 deletions bin/stress-test/src/seeding/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ pub async fn seed_store(
let faucet = create_faucet();
let fee_params = FeeParameters::new(faucet.id(), 0).unwrap();
let signer = EcdsaSecretKey::new();
let genesis_state = GenesisState::new(vec![faucet.clone()], fee_params, 1, 1, signer);
let genesis_state = GenesisState::new(vec![faucet.clone()], fee_params, 1, 1, signer.clone());
Store::bootstrap(genesis_state.clone(), &data_directory)
.await
.expect("store should bootstrap");
Expand All @@ -114,6 +114,7 @@ pub async fn seed_store(
&store_client,
data_directory,
accounts_filepath,
&signer,
)
.await;

Expand All @@ -125,6 +126,7 @@ pub async fn seed_store(
///
/// The first transaction in each batch sends assets from the faucet to 255 accounts.
/// The rest of the transactions consume the notes created by the faucet in the previous block.
#[expect(clippy::too_many_arguments)]
async fn generate_blocks(
num_accounts: usize,
public_accounts_percentage: u8,
Expand All @@ -133,6 +135,7 @@ async fn generate_blocks(
store_client: &StoreClient,
data_directory: DataDirectory,
accounts_filepath: PathBuf,
signer: &EcdsaSecretKey,
Copy link
Contributor

Choose a reason for hiding this comment

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

is this always an Ecdsa key type?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes. As opposed to what?

) -> SeedingMetrics {
// Each block is composed of [`BATCHES_PER_BLOCK`] batches, and each batch is composed of
// [`TRANSACTIONS_PER_BATCH`] txs. The first note of the block is always a send assets tx
Expand Down Expand Up @@ -211,7 +214,8 @@ async fn generate_blocks(
let block_inputs = get_block_inputs(store_client, &batches, &mut metrics).await;

// update blocks
prev_block_header = apply_block(batches, block_inputs, store_client, &mut metrics).await;
prev_block_header =
apply_block(batches, block_inputs, store_client, &mut metrics, signer).await;
if current_anchor_header.block_epoch() != prev_block_header.block_epoch() {
current_anchor_header = prev_block_header.clone();
}
Expand Down Expand Up @@ -246,11 +250,12 @@ async fn apply_block(
block_inputs: BlockInputs,
store_client: &StoreClient,
metrics: &mut SeedingMetrics,
signer: &EcdsaSecretKey,
) -> BlockHeader {
let proposed_block = ProposedBlock::new(block_inputs, batches).unwrap();
let (header, body) = proposed_block.clone().into_header_and_body().unwrap();
let block_size: usize = header.to_bytes().len() + body.to_bytes().len();
let signature = EcdsaSecretKey::new().sign(header.commitment());
let signature = signer.sign(header.commitment());
// SAFETY: The header, body, and signature are known to correspond to each other.
let signed_block = SignedBlock::new_unchecked(header, body, signature);
let ordered_batches = proposed_block.batches().clone();
Expand Down
1 change: 1 addition & 0 deletions bin/stress-test/src/store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,7 @@ async fn sync_chain_mmr(
) -> SyncChainMmrRun {
let sync_request = proto::rpc::SyncChainMmrRequest {
block_range: Some(proto::rpc::BlockRange { block_from, block_to: Some(block_to) }),
finality: proto::rpc::Finality::Committed.into(),
};

let start = Instant::now();
Expand Down
1 change: 1 addition & 0 deletions crates/rpc/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,7 @@ async fn sync_chain_mmr_returns_delta() {

let request = proto::rpc::SyncChainMmrRequest {
block_range: Some(proto::rpc::BlockRange { block_from: 0, block_to: None }),
finality: proto::rpc::Finality::Committed.into(),
};
let response = rpc_client.sync_chain_mmr(request).await.expect("sync_chain_mmr should succeed");
let response = response.into_inner();
Expand Down
52 changes: 52 additions & 0 deletions crates/store/src/blocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,41 @@ impl BlockStore {
fs_err::write(block_path, data)
}

// PROOF STORAGE
// --------------------------------------------------------------------------------------------

#[expect(dead_code)]
pub async fn load_proof(
&self,
block_num: BlockNumber,
) -> Result<Option<Vec<u8>>, std::io::Error> {
match tokio::fs::read(self.proof_path(block_num)).await {
Ok(data) => Ok(Some(data)),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(err) => Err(err),
}
}

#[instrument(
target = COMPONENT,
name = "store.block_store.save_proof",
skip(self, data),
err,
fields(proof_size = data.len())
)]
pub async fn save_proof(
&self,
block_num: BlockNumber,
data: &[u8],
) -> Result<(), std::io::Error> {
let (epoch_path, proof_path) = self.epoch_proof_path(block_num)?;
if !epoch_path.exists() {
tokio::fs::create_dir_all(epoch_path).await?;
}

tokio::fs::write(proof_path, data).await
}

// HELPER FUNCTIONS
// --------------------------------------------------------------------------------------------

Expand All @@ -117,6 +152,13 @@ impl BlockStore {
epoch_dir.join(format!("block_{block_num:08x}.dat"))
}

fn proof_path(&self, block_num: BlockNumber) -> PathBuf {
let block_num = block_num.as_u32();
let epoch = block_num >> 16;
let epoch_dir = self.store_dir.join(format!("{epoch:04x}"));
epoch_dir.join(format!("proof_{block_num:08x}.dat"))
}

fn epoch_block_path(
&self,
block_num: BlockNumber,
Expand All @@ -127,6 +169,16 @@ impl BlockStore {
Ok((epoch_path.to_path_buf(), block_path))
}

fn epoch_proof_path(
&self,
block_num: BlockNumber,
) -> Result<(PathBuf, PathBuf), std::io::Error> {
let proof_path = self.proof_path(block_num);
let epoch_path = proof_path.parent().ok_or(std::io::Error::from(ErrorKind::NotFound))?;

Ok((epoch_path.to_path_buf(), proof_path))
}

pub fn display(&self) -> std::path::Display<'_> {
self.store_dir.display()
}
Expand Down
13 changes: 9 additions & 4 deletions crates/store/src/db/migrations/2025062000000_setup/up.sql
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
CREATE TABLE block_headers (
block_num INTEGER NOT NULL,
block_header BLOB NOT NULL,
signature BLOB NOT NULL,
commitment BLOB NOT NULL,
block_num INTEGER NOT NULL,
block_header BLOB NOT NULL,
signature BLOB NOT NULL,
commitment BLOB NOT NULL,
proving_inputs BLOB, -- Serialized BlockProofRequest needed for deferred proving. NULL for 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.

What's the reason for storing proving inputs in the database?

Copy link
Contributor

Choose a reason for hiding this comment

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

How big are these?

Copy link
Contributor

Choose a reason for hiding this comment

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

Iiuc conceptually, we retrieve all un-proven blocks from DB, and to request a proof we require the inputs. The column becomes uniform source for inputs when it comes to proofing for both in-flight and proving on-startup. It also ensure we persist the inputs/don't lose any information on crash / host reboot / etc.

If retrieving/creating a proof fails repeatedly, we might want a mitigation to get stuck in trying to proof a failing one. (I'll get back to this).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes the proof scheduler needs to gather proving inputs on startup / in loop when proving committed blocks.

Do we want to factor this differently so that the proof scheduler has to construct the proving inputs itself? @bobbinth

Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we want to factor this differently so that the proof scheduler has to construct the proving inputs itself? @bobbinth

I'm still reviewing, but I think that's the thrust of it. If the block-to-commit doesn't arrive with proof inputs already present, then there is no need to generate and cache them in the database. Each proof job can generate these on-the-fly as part of its process.

Counter-arguments to this are if the inputs are expensive to generate and come pre-generated as part of the committed block data.

Copy link
Contributor

Choose a reason for hiding this comment

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

How big are these?

These could get quite big - though, highly depends on the number of batches and the nature of underlying transactions. I'd expect that these could between 1MB and dozens of MB per block.

Yes the proof scheduler needs to gather proving inputs on startup / in loop when proving committed blocks.

Ah - so we need these only for blocks that haven't been proven yet, right? Initially I thought these could live just in memory - but in case of a crush of shutdown, this info won't be available (and I don't think all of it can be reconstructed from the data in the store).

I think there are two things to consider here:

  1. How long do we need to save this data for. I think one option is to just remove the data as soon as the block has been proven.
  2. What data to save. We could go with only the data that cannot be reconstructed from the store (I think that would be the tx_batches) and reconstruct the rest when required, but I'm not sure if extra complexity is worth it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think as long as we don't SELECT * on the table unnecessarily then it should be fine to put the entire BlockProofRequest in there and delete it once proven.

Unless we have a sense that reconstructing it is definitely a meaningful performance improvement, I'm not sure its worth adding the complexity. Best approach might be to keep it in the db and monitor performance. Also not sure though.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, agreed. Let's save the full request and delete once the block is proven.

is_proven BOOLEAN NOT NULL DEFAULT 0, -- Whether the block has been proven

PRIMARY KEY (block_num),
CONSTRAINT block_header_block_num_is_u32 CHECK (block_num BETWEEN 0 AND 0xFFFFFFFF)
);

CREATE INDEX block_headers_to_be_proven ON block_headers(block_num ASC) WHERE is_proven = 0;
CREATE INDEX block_headers_proven_desc ON block_headers(block_num DESC) WHERE is_proven = 1;

CREATE TABLE account_codes (
code_commitment BLOB NOT NULL,
code BLOB NOT NULL,
Expand Down
76 changes: 63 additions & 13 deletions crates/store/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::path::PathBuf;
use anyhow::Context;
use diesel::{Connection, QueryableByName, RunQueryDsl, SqliteConnection};
use miden_node_proto::domain::account::AccountInfo;
use miden_node_proto::generated as proto;
use miden_node_proto::{BlockProofRequest, generated as proto};
use miden_node_utils::tracing::OpenTelemetrySpanExt;
use miden_protocol::Word;
use miden_protocol::account::{AccountHeader, AccountId, AccountStorageHeader};
Expand Down Expand Up @@ -270,12 +270,15 @@ impl Db {
conn.transaction(move |conn| {
models::queries::apply_block(
conn,
genesis.header(),
genesis.signature(),
&[],
&[],
genesis.body().updated_accounts(),
genesis.body().transactions(),
models::queries::ApplyBlockData {
block_header: genesis.header(),
signature: genesis.signature(),
notes: &[],
nullifiers: &[],
accounts: genesis.body().updated_accounts(),
transactions: genesis.body().transactions(),
proving_inputs: None, // Genesis block is never proven.
Copy link
Collaborator

Choose a reason for hiding this comment

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

If only genesis uses None, then I would just have genesis use an "empty" value instead of supporting Option in the function itself.

Its somewhat confusing in the normal code -- because why would it ever be None.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Given the relevant type is not a container, then wouldn't None be the "empty" value?

},
)
})
.context("failed to insert genesis block")?;
Expand Down Expand Up @@ -555,16 +558,20 @@ impl Db {
acquire_done: oneshot::Receiver<()>,
signed_block: SignedBlock,
notes: Vec<(NoteRecord, Option<Nullifier>)>,
proving_inputs: Option<BlockProofRequest>,
) -> Result<()> {
self.transact("apply block", move |conn| -> Result<()> {
models::queries::apply_block(
conn,
signed_block.header(),
signed_block.signature(),
&notes,
signed_block.body().created_nullifiers(),
signed_block.body().updated_accounts(),
signed_block.body().transactions(),
models::queries::ApplyBlockData {
block_header: signed_block.header(),
signature: signed_block.signature(),
notes: &notes,
nullifiers: signed_block.body().created_nullifiers(),
accounts: signed_block.body().updated_accounts(),
transactions: signed_block.body().transactions(),
proving_inputs,
},
)?;

// XXX FIXME TODO free floating mutex MUST NOT exist
Expand All @@ -582,6 +589,49 @@ impl Db {
.await
}

/// Marks a previously committed block as proven.
///
/// Sets the `is_proven` flag to `true` for the given block number.
#[instrument(target = COMPONENT, skip_all, err)]
pub async fn mark_block_proven(&self, block_num: BlockNumber) -> Result<()> {
self.transact("mark block proven", move |conn| {
models::queries::mark_block_proven(conn, block_num)
})
.await?;
Ok(())
}

/// Returns block numbers for all blocks that have not yet been proven, ordered ascending.
#[instrument(level = "debug", target = COMPONENT, skip_all, ret(level = "debug"), err)]
pub async fn select_unproven_blocks(&self, limit: i64) -> Result<Vec<BlockNumber>> {
self.transact("select unproven blocks", move |conn| {
models::queries::select_unproven_blocks(conn, limit)
})
.await
}

/// Returns the proving inputs for a given block number, if stored.
#[instrument(level = "debug", target = COMPONENT, skip_all, err)]
pub async fn select_block_proving_inputs(
&self,
block_num: BlockNumber,
) -> Result<Option<BlockProofRequest>> {
self.transact("select block proving inputs", move |conn| {
models::queries::select_block_proving_inputs(conn, block_num)
})
.await
}

/// 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>> {
Comment on lines +638 to +643
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

self.transact("select latest proven block num", |conn| {
models::queries::select_latest_proven_block_num(conn)
})
.await
}

/// Selects storage map values for syncing storage maps for a specific account ID.
///
/// The returned values are the latest known values up to `block_range.end()`, and no values
Expand Down
Loading
Loading