Conversation
| }; | ||
| tracing::debug!(target: COMPONENT, ?message); | ||
| let request = tonic::Request::new(message); | ||
| let _response = self.validator.clone().validate_block(request).await.unwrap(); // todo: rm unwrap |
There was a problem hiding this comment.
@bobbinth would the block producer do this here?
There was a problem hiding this comment.
Yes - though I was imagining the flow a bit differently:
- We get the proposed block and send it to the validator [as we do now].
- 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). - The block producer checks them to make sure they are consistent.
- 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).
- The prover returns the block proof.
- 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.
There was a problem hiding this comment.
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.
Mirko-von-Leipzig
left a comment
There was a problem hiding this comment.
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
| .await | ||
| .map_err(BuildBlockError::ValidateBlockFailed)?; | ||
|
|
||
| // TODO: Check that the returned header and body match the proposed block. |
There was a problem hiding this comment.
@bobbinth what checks should we perform here?
There was a problem hiding this comment.
Not for this PR, but I think here we'd want to check two things:
- That the
block_headerreturned from the validator is consistent with theproposed_block. - That the
block_bodyreturned from the validator is consistent with theblock_headerandproposed_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.
…lidation-endpoints
| body: BlockBody, | ||
| block_proof: BlockProof, | ||
| ) -> Result<ProvenBlock, BuildBlockError> { | ||
| let proven_block = ProvenBlock::new_unchecked(header, body, block_proof); |
There was a problem hiding this comment.
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`]. |
There was a problem hiding this comment.
| /// order, as define by [`OrderedTransactionHeaders`]. | |
| /// order, as defined by [`OrderedTransactionHeaders`]. |
| 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() | ||
| ))); | ||
| } |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
| /// Interface to the validator's block-producer gRPC API. | |
| /// Interface to the validator's gRPC API. |
bobbinth
left a comment
There was a problem hiding this comment.
Looks good! Thank you! I left some comments inline, but we can probably merge as is and address the comments in follow-up PRs.
| // Validates a proposed block and returns the block header and body. | ||
| rpc ValidateBlock(blockchain.ProposedBlock) returns (ValidateBlockResponse) {} |
There was a problem hiding this comment.
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
BuildBlockorSignBlockwould 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.
| /// Url at which to serve the gRPC API. | ||
| #[arg(env = ENV_BLOCK_PRODUCER_URL)] | ||
| url: Url, |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
| pub mod store; | ||
| pub mod validator; |
There was a problem hiding this comment.
Question: why do these need to be public?
| pub struct BlockProofRequest { | ||
| pub tx_batches: OrderedBatches, | ||
| pub block_header: BlockHeader, | ||
| pub block_inputs: BlockInputs, | ||
| } |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
Not for this PR, but I think here we'd want to check two things:
- That the
block_headerreturned from the validator is consistent with theproposed_block. - That the
block_bodyreturned from the validator is consistent with theblock_headerandproposed_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())?; |
There was a problem hiding this comment.
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.
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