diff --git a/CHANGELOG.md b/CHANGELOG.md index d2a6f40f1..8b02c4f11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 0.3.0 (TBD) +* Added block authentication data to the `GetBlockHeaderByNumber` RPC (#345). * Added option to mint pulic notes in the faucet (#339). * Renamed `note_hash` into `note_id` in the database (#336) * Changed `version` and `timestamp` fields in `Block` message to `u32` (#337). diff --git a/crates/proto/proto/requests.proto b/crates/proto/proto/requests.proto index 667a10378..50c850775 100644 --- a/crates/proto/proto/requests.proto +++ b/crates/proto/proto/requests.proto @@ -14,11 +14,17 @@ message CheckNullifiersRequest { repeated digest.Digest nullifiers = 1; } +// Returns the block header corresponding to the requested block number, as well as the merkle +// path and current forest which validate the block's inclusion in the chain. +// +// The Merkle path is an MMR proof for the block's leaf, based on the current chain length. message GetBlockHeaderByNumberRequest { // The block number of the target block. // // If not provided, means latest know block. optional uint32 block_num = 1; + // Whether or not to return authentication data for the block header. + optional bool include_mmr_proof = 2; } // State synchronization request. diff --git a/crates/proto/proto/responses.proto b/crates/proto/proto/responses.proto index 76ca3abf4..1294a664a 100644 --- a/crates/proto/proto/responses.proto +++ b/crates/proto/proto/responses.proto @@ -17,7 +17,14 @@ message CheckNullifiersResponse { } message GetBlockHeaderByNumberResponse { + // The requested block header block_header.BlockHeader block_header = 1; + + // Merkle path to verify the block's inclusion in the MMR at the returned `chain_length` + optional merkle.MerklePath mmr_path = 2; + + // Current chain length + optional fixed32 chain_length = 3; } message NullifierUpdate { @@ -26,22 +33,22 @@ message NullifierUpdate { } message SyncStateResponse { - // number of the latest block in the chain + // Number of the latest block in the chain fixed32 chain_tip = 1; - // block header of the block with the first note matching the specified criteria + // Block header of the block with the first note matching the specified criteria block_header.BlockHeader block_header = 2; - // data needed to update the partial MMR from `block_num + 1` to `block_header.block_num` + // Data needed to update the partial MMR from `block_num + 1` to `block_header.block_num` mmr.MmrDelta mmr_delta = 3; - // a list of account hashes updated after `block_num + 1` but not after `block_header.block_num` + // List of account hashes updated after `block_num + 1` but not after `block_header.block_num` repeated account.AccountSummary accounts = 5; - // a list of all notes together with the Merkle paths from `block_header.note_root` + // List of all notes together with the Merkle paths from `block_header.note_root` repeated note.NoteSyncRecord notes = 6; - // a list of nullifiers created between `block_num + 1` and `block_header.block_num` + // List of nullifiers created between `block_num + 1` and `block_header.block_num` repeated NullifierUpdate nullifiers = 7; } diff --git a/crates/proto/src/generated/requests.rs b/crates/proto/src/generated/requests.rs index f448e59a8..fef941919 100644 --- a/crates/proto/src/generated/requests.rs +++ b/crates/proto/src/generated/requests.rs @@ -11,6 +11,10 @@ pub struct CheckNullifiersRequest { #[prost(message, repeated, tag = "1")] pub nullifiers: ::prost::alloc::vec::Vec, } +/// Returns the block header corresponding to the requested block number, as well as the merkle +/// path and current forest which validate the block's inclusion in the chain. +/// +/// The merkle path is an MMR proof for the block's leaf, based on the current forest. #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetBlockHeaderByNumberRequest { @@ -19,6 +23,9 @@ pub struct GetBlockHeaderByNumberRequest { /// If not provided, means latest know block. #[prost(uint32, optional, tag = "1")] pub block_num: ::core::option::Option, + /// Whether or not to return authentication data for the block header. + #[prost(bool, optional, tag = "2")] + pub include_mmr_proof: ::core::option::Option, } /// State synchronization request. /// diff --git a/crates/proto/src/generated/responses.rs b/crates/proto/src/generated/responses.rs index fd3d9f4f0..5f5797fe4 100644 --- a/crates/proto/src/generated/responses.rs +++ b/crates/proto/src/generated/responses.rs @@ -12,8 +12,15 @@ pub struct CheckNullifiersResponse { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetBlockHeaderByNumberResponse { + /// The requested block header #[prost(message, optional, tag = "1")] pub block_header: ::core::option::Option, + /// Merkle path to verify the block's inclusion in the MMR at the returned `forest` + #[prost(message, optional, tag = "2")] + pub mmr_path: ::core::option::Option, + /// Current value of the MMR forest + #[prost(fixed32, optional, tag = "3")] + pub chain_length: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -26,22 +33,22 @@ pub struct NullifierUpdate { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SyncStateResponse { - /// number of the latest block in the chain + /// Number of the latest block in the chain #[prost(fixed32, tag = "1")] pub chain_tip: u32, - /// block header of the block with the first note matching the specified criteria + /// Block header of the block with the first note matching the specified criteria #[prost(message, optional, tag = "2")] pub block_header: ::core::option::Option, - /// data needed to update the partial MMR from `block_num + 1` to `block_header.block_num` + /// Data needed to update the partial MMR from `block_num + 1` to `block_header.block_num` #[prost(message, optional, tag = "3")] pub mmr_delta: ::core::option::Option, - /// a list of account hashes updated after `block_num + 1` but not after `block_header.block_num` + /// List of account hashes updated after `block_num + 1` but not after `block_header.block_num` #[prost(message, repeated, tag = "5")] pub accounts: ::prost::alloc::vec::Vec, - /// a list of all notes together with the Merkle paths from `block_header.note_root` + /// List of all notes together with the Merkle paths from `block_header.note_root` #[prost(message, repeated, tag = "6")] pub notes: ::prost::alloc::vec::Vec, - /// a list of nullifiers created between `block_num + 1` and `block_header.block_num` + /// List of nullifiers created between `block_num + 1` and `block_header.block_num` #[prost(message, repeated, tag = "7")] pub nullifiers: ::prost::alloc::vec::Vec, } diff --git a/crates/rpc/README.md b/crates/rpc/README.md index 36edc43c5..1355eae8c 100644 --- a/crates/rpc/README.md +++ b/crates/rpc/README.md @@ -34,7 +34,7 @@ Gets a list of proofs for given nullifier hashes, each proof as a sparse Merkle ### GetBlockHeaderByNumber -Retrieves block header by given block number. +Retrieves block header by given block number, optionally alongside a Merkle path and the current chain length to validate its inclusion. **Parameters** diff --git a/crates/store/README.md b/crates/store/README.md index 6facb2636..f746edb28 100644 --- a/crates/store/README.md +++ b/crates/store/README.md @@ -48,7 +48,7 @@ Get a list of proofs for given nullifier hashes, each proof as a sparse Merkle T ### GetBlockHeaderByNumber -Retrieves block header by given block number. +Retrieves block header by given block number. Optionally, it also returns the MMR path and current chain length to authenticate the block's inclusion. **Parameters** diff --git a/crates/store/src/errors.rs b/crates/store/src/errors.rs index a73330766..02923ad5e 100644 --- a/crates/store/src/errors.rs +++ b/crates/store/src/errors.rs @@ -172,6 +172,14 @@ pub enum ApplyBlockError { FailedToUpdateNullifierTree(NullifierTreeError), } +#[derive(Error, Debug)] +pub enum GetBlockHeaderError { + #[error("Database error: {0}")] + DatabaseError(#[from] DatabaseError), + #[error("Error retrieving the merkle proof for the block: {0}")] + MmrError(#[from] MmrError), +} + #[derive(Error, Debug)] pub enum GetBlockInputsError { #[error("Database error: {0}")] diff --git a/crates/store/src/server/api.rs b/crates/store/src/server/api.rs index f3e937dc0..abc6d5e51 100644 --- a/crates/store/src/server/api.rs +++ b/crates/store/src/server/api.rs @@ -67,16 +67,20 @@ impl api_server::Api for StoreApi { request: tonic::Request, ) -> Result, Status> { info!(target: COMPONENT, ?request); + let request = request.into_inner(); - let block_num = request.into_inner().block_num; - let block_header = self + let block_num = request.block_num; + let (block_header, mmr_proof) = self .state - .get_block_header(block_num) + .get_block_header(block_num, request.include_mmr_proof.unwrap_or(false)) .await - .map_err(internal_error)? - .map(Into::into); + .map_err(internal_error)?; - Ok(Response::new(GetBlockHeaderByNumberResponse { block_header })) + Ok(Response::new(GetBlockHeaderByNumberResponse { + block_header: block_header.map(Into::into), + chain_length: mmr_proof.as_ref().map(|p| p.forest as u32), + mmr_path: mmr_proof.map(|p| Into::into(p.merkle_path)), + })) } /// Returns info on whether the specified nullifiers have been consumed. diff --git a/crates/store/src/state.rs b/crates/store/src/state.rs index 51d54666c..0121faa1d 100644 --- a/crates/store/src/state.rs +++ b/crates/store/src/state.rs @@ -10,7 +10,7 @@ use miden_objects::{ block::{Block, BlockNoteIndex, BlockNoteTree}, crypto::{ hash::rpo::RpoDigest, - merkle::{LeafIndex, Mmr, MmrDelta, MmrPeaks, SimpleSmt, SmtProof, ValuePath}, + merkle::{LeafIndex, Mmr, MmrDelta, MmrPeaks, MmrProof, SimpleSmt, SmtProof, ValuePath}, }, notes::{NoteId, Nullifier}, transaction::OutputNote, @@ -27,8 +27,8 @@ use crate::{ blocks::BlockStore, db::{Db, NoteRecord, NullifierInfo, StateSyncUpdate}, errors::{ - ApplyBlockError, DatabaseError, GetBlockInputsError, StateInitializationError, - StateSyncError, + ApplyBlockError, DatabaseError, GetBlockHeaderError, GetBlockInputsError, + StateInitializationError, StateSyncError, }, nullifier_tree::NullifierTree, types::{AccountId, BlockNumber}, @@ -291,15 +291,30 @@ impl State { Ok(()) } - /// Queries a [BlockHeader] from the database. + /// Queries a [BlockHeader] from the database, and returns it alongside its inclusion proof. /// - /// If [None] is given as the value of `block_num`, the latest [BlockHeader] is returned. + /// If [None] is given as the value of `block_num`, the data for the latest [BlockHeader] is + /// returned. #[instrument(target = "miden-store", skip_all, ret(level = "debug"), err)] pub async fn get_block_header( &self, block_num: Option, - ) -> Result, DatabaseError> { - self.db.select_block_header_by_block_num(block_num).await + include_mmr_proof: bool, + ) -> Result<(Option, Option), GetBlockHeaderError> { + let block_header = self.db.select_block_header_by_block_num(block_num).await?; + if let Some(header) = block_header { + let mmr_proof = if include_mmr_proof { + let inner = self.inner.read().await; + let mmr_proof = + inner.chain_mmr.open(header.block_num() as usize, inner.chain_mmr.forest())?; + Some(mmr_proof) + } else { + None + }; + Ok((Some(header), mmr_proof)) + } else { + Ok((None, None)) + } } /// Generates membership proofs for each one of the `nullifiers` against the latest nullifier