Skip to content

feat: Validate and prove blocks#1381

Merged
drahnr merged 26 commits intonextfrom
sergerad-validation-endpoints
Nov 28, 2025
Merged

feat: Validate and prove blocks#1381
drahnr merged 26 commits intonextfrom
sergerad-validation-endpoints

Conversation

@sergerad
Copy link
Collaborator

@sergerad sergerad commented Nov 25, 2025

Context

We have updated base types to support deferred block proving. The node needs to be integrated with the new types.

The deferred proving flow is being worked towards. Eventually the store will make the proving request that the block producer is making currently. In the meantime, we can iterate on the current block production flow by adding block validation via the validator component.

Relates to #1316.

Changes

  • Update base dependancies.
  • Add block validation endpoint to validator component.
  • Add validator RPC client.
  • Integrate validator client to block producer.
  • Update block producer to validate blocks through the validator rpc.

@sergerad sergerad marked this pull request as draft November 25, 2025 22:25
};
tracing::debug!(target: COMPONENT, ?message);
let request = tonic::Request::new(message);
let _response = self.validator.clone().validate_block(request).await.unwrap(); // todo: rm unwrap
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@bobbinth would the block producer do this here?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes - though I was imagining the flow a bit differently:

  1. We get the proposed block and send it to the validator [as we do now].
  2. The validator builds the block and returns the block header and body. As a part of this, the validator would sign the blog header (this part still needs to be implemented in miden-base).
  3. The block producer checks them to make sure they are consistent.
  4. Then, we send the block header, transaction batches, and block inputs to the prover (we currently send the block body to the prove as well, but I'm not sure why this is needed).
  5. The prover returns the block proof.
  6. We build the proven block.

Once we move the proving to the store, the last 3 steps will need to happen there. For this, instead of sending a proven block to the store, we'll need to send (block_header, block_body, tx_batches, block_inputs) to the store (probably organized into a struct). We could also skip sending block_inputs because the store should theoretically be able to build them - but it may be easier to just send them.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Once we move the proving to the store, the last 3 steps will need to happen there. For this, instead of sending a proven block to the store, we'll need to send (block_header, block_body, tx_batches, block_inputs) to the store (probably organized into a struct). We could also skip sending block_inputs because the store should theoretically be able to build them - but it may be easier to just send them.

Just to ensure I/we are on the same page. This is intended for a separate PR/future work correct? Because that would also entail changes on gRPC and client etc to allow for validated but as yet unproven blocks.

Copy link
Collaborator

@Mirko-von-Leipzig Mirko-von-Leipzig 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 large strokes are correct modulo the comments from @bobbinth and the step separation. We can also follow these up in a separate PR to unblock other PRs waiting on the protocol changes

@sergerad sergerad requested a review from bobbinth November 27, 2025 20:38
.await
.map_err(BuildBlockError::ValidateBlockFailed)?;

// TODO: Check that the returned header and body match the proposed block.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@bobbinth what checks should we perform here?

Copy link
Contributor

Choose a reason for hiding this comment

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

Not for this PR, but I think here we'd want to check two things:

  • That the block_header returned from the validator is consistent with the proposed_block.
  • That the block_body returned from the validator is consistent with the block_header and proposed_block.

This is protection against the validator becoming malicious/inconsistent and returning an invalid block which will fail to prove later on.

The way to do this is probably by converting proposed_block into block_header and block_body in this function, and checking these against the data we get from the validator (excluding the signature). Since this is a non-negligible operation in terms of performance (would be good to benchmark how long it actually would take at some point), we should do this in parallel with the request to the validator. Meaning:

  • We make the request to the validator but don't await on it.
  • We build the block header and body locally.
  • We await on the validator request - and once it comes back do the comparison.

@sergerad sergerad marked this pull request as ready for review November 27, 2025 22:24
body: BlockBody,
block_proof: BlockProof,
) -> Result<ProvenBlock, BuildBlockError> {
let proven_block = ProvenBlock::new_unchecked(header, body, block_proof);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you add a // SAFETY comment explaining why whatever invariants new_unchecked requires are upheld?

/// passed in the proposed block.
///
/// This expects that transactions from the proposed block and proven block are in the same
/// order, as define by [`OrderedTransactionHeaders`].
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
/// order, as define by [`OrderedTransactionHeaders`].
/// order, as defined by [`OrderedTransactionHeaders`].

Comment on lines +472 to +478
if proposed_txs.as_slice().len() != proven_block.body().transactions().as_slice().len() {
return Err(BuildBlockError::other(format!(
"remote prover returned {} transaction headers but {} transactions were passed as part of the proposed block",
proven_block.body().transactions().as_slice().len(),
proposed_txs.as_slice().len()
)));
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

fyi we could also use itertools::equal to compare the iterators including length.

Up to you; your impl does give more info

let response = response.into_inner();

// Extract header from response (should always be present).
let header_proto = response.header.expect("validator always returns a header");
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wouldn't rely on this expectation, this should be a deserialization error.

In the proto crate we use https://github.com/0xMiden/miden-node/blob/8d76576ef63b1e06402b0c729e836a149507c456/crates/proto/src/errors/mod.rs#L64-L75

which gets uses something like https://github.com/0xMiden/miden-node/blob/8d76576ef63b1e06402b0c729e836a149507c456/crates/proto/src/domain/account.rs#L121-L123

Having the client here is fine for now but I would make these errors instead (and same below).

// VALIDATOR CLIENT
// ================================================================================================

/// Interface to the validator's block-producer gRPC API.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
/// Interface to the validator's block-producer gRPC API.
/// Interface to the validator's gRPC API.

Copy link
Contributor

@bobbinth bobbinth left a comment

Choose a reason for hiding this comment

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

Looks good! Thank you! I left some comments inline, but we can probably merge as is and address the comments in follow-up PRs.

Comment on lines +20 to +21
// Validates a proposed block and returns the block header and body.
rpc ValidateBlock(blockchain.ProposedBlock) returns (ValidateBlockResponse) {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Not for this PR, but I wonder if the naming here can be improved a bit:

  • Technically, we are not so much validating the block as constructing and signing it - so, maybe something like BuildBlock or SignBlock would be better?
  • Also, maybe the response should be SignedBlock? Technically, we are returning a fully constructed (and in the future) signed block from here.

I don't love the suggestions above either - maybe there are some better options.

Comment on lines 23 to 25
/// Url at which to serve the gRPC API.
#[arg(env = ENV_BLOCK_PRODUCER_URL)]
url: Url,
Copy link
Contributor

Choose a reason for hiding this comment

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

Question not related to this PR: are the valid values for this parameter only localhost:[port] and 0.0.0.0:[port] - or could we pass any URL into this?

.try_lock()
.map_err(|_| Status::resource_exhausted("Server is busy handling another request"))?
.prove(proposed_block)
.prove(tx_batches, block_header.clone(), block_inputs)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: we clone the block_header here so that we compute block_id on line 182 below, right? If so, we could compute block_id after line 174 above and avoid the clone.

Comment on lines 11 to +12
pub mod store;
pub mod validator;
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: why do these need to be public?

Comment on lines +14 to +18
pub struct BlockProofRequest {
pub tx_batches: OrderedBatches,
pub block_header: BlockHeader,
pub block_inputs: BlockInputs,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Not for this PR, but it would be great to add doc comments to this struct explaining the purpose of its struct and its fields. For example, I think the block header is included here mostly because of the signature as everything else in the header could be derived from tx_batches and block_inputs.

Also, I wonder if eventually this struct (potentially named differently) should live in miden-base.


// build note tree
let note_tree = block.build_output_note_tree();
let note_tree = block.body().compute_block_note_tree();
Copy link
Contributor

Choose a reason for hiding this comment

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

nit (and not for this PR): build_output_note_tree() was probably a better name than compute_block_note_tree(). Let's remember to update this when we work on follow-up changes in miden-base.

.await
.map_err(BuildBlockError::ValidateBlockFailed)?;

// TODO: Check that the returned header and body match the proposed block.
Copy link
Contributor

Choose a reason for hiding this comment

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

Not for this PR, but I think here we'd want to check two things:

  • That the block_header returned from the validator is consistent with the proposed_block.
  • That the block_body returned from the validator is consistent with the block_header and proposed_block.

This is protection against the validator becoming malicious/inconsistent and returning an invalid block which will fail to prove later on.

The way to do this is probably by converting proposed_block into block_header and block_body in this function, and checking these against the data we get from the validator (excluding the signature). Since this is a non-negligible operation in terms of performance (would be good to benchmark how long it actually would take at some point), we should do this in parallel with the request to the validator. Meaning:

  • We make the request to the validator but don't await on it.
  • We build the block header and body locally.
  • We await on the validator request - and once it comes back do the comparison.

}

self.simulate_proving().await;
validate_tx_headers(&proven_block, &ordered_batches.to_transactions())?;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure this is actually needed here - but maybe for now it is fine. Once we have the actual block proving, this would be replaced with verifying the proof returned from the prover against the block header. Maybe worth adding a comment here about this.

@drahnr drahnr merged commit db22698 into next Nov 28, 2025
6 checks passed
@drahnr drahnr deleted the sergerad-validation-endpoints branch November 28, 2025 10:57
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.

4 participants