Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 bin/ntx-builder/src/actor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ impl AccountActorContext {
clients: GrpcClients {
rpc: RpcClient::new(
url.clone(),
miden_protocol::Word::default(),
Duration::from_millis(100),
Duration::from_secs(30),
),
Expand Down
223 changes: 199 additions & 24 deletions bin/ntx-builder/src/clients/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,26 @@ use backon::{ExponentialBuilder, Retryable};
use futures::Stream;
use futures::stream::TryStreamExt;
use miden_node_proto::clients::{Builder, RpcClient as InnerRpcClient};
use miden_node_proto::domain::account::{
AccountDetails, AccountResponse, AccountVaultDetails, StorageMapEntries
};
use miden_node_proto::errors::ConversionError;
use miden_node_proto::generated::rpc::account_request::account_detail_request::{StorageMapDetailRequest, storage_map_detail_request};
use miden_node_proto::generated::rpc::account_request::account_detail_request::storage_map_detail_request::MapKeys;
use miden_node_proto::generated::rpc::{BlockSubscriptionRequest, BlockSubscriptionResponse};
use miden_node_proto::generated::{self as proto};
use miden_node_utils::ErrorReport;
use miden_protocol::Word;
use miden_protocol::account::{AccountId, StorageMapKey, StorageMapWitness, StorageSlotName};
use miden_protocol::asset::{AssetVaultKey, AssetWitness};
use miden_protocol::account::{
AccountCode,
AccountId,
PartialAccount,
PartialStorage,
StorageMapKey,
StorageMapWitness,
StorageSlotName,
};
use miden_protocol::asset::{Asset, AssetVault, AssetVaultKey, AssetWitness, PartialVault};
use miden_protocol::block::{BlockNumber, SignedBlock};
use miden_protocol::note::NoteScript;
use miden_protocol::transaction::{AccountInputs, ProvenTransaction, TransactionInputs};
Expand Down Expand Up @@ -41,14 +55,23 @@ impl RpcClient {
///
/// `backoff_initial` / `backoff_max` configure the exponential backoff schedule applied to
/// `block_subscription` retries (the only operation that retries today).
pub fn new(rpc_url: Url, backoff_initial: Duration, backoff_max: Duration) -> Self {
Self::new_with_auth(rpc_url, None, backoff_initial, backoff_max)
pub fn new(
rpc_url: Url,
genesis_commitment: Word,
backoff_initial: Duration,
backoff_max: Duration,
) -> Self {
Self::new_with_auth(rpc_url, None, genesis_commitment, backoff_initial, backoff_max)
}

/// Creates a new client with an optional metadata header for internal RPC authentication.
///
/// `genesis_commitment` is sent as the `genesis` parameter of the `Accept` header so that the
/// node accepts write RPCs such as `SubmitProvenTx`, which require a matching genesis.
pub fn new_with_auth(
rpc_url: Url,
rpc_auth_header_value: Option<AsciiMetadataValue>,
genesis_commitment: Word,
backoff_initial: Duration,
backoff_max: Duration,
) -> Self {
Expand All @@ -58,7 +81,7 @@ impl RpcClient {
.without_tls()
.without_timeout()
.without_metadata_version()
.without_metadata_genesis();
.with_metadata_genesis(genesis_commitment.to_hex());
let builder = match rpc_auth_header_value {
Some(value) => builder.with_auth_header_value(value),
None => builder.without_auth_header(),
Expand Down Expand Up @@ -151,45 +174,193 @@ fn decode_block_subscription_response(
// ACTOR-PATH METHODS
// ================================================================================================
//
// The actor module still references these methods. PR 1 keeps the actor code in tree as dead
// code (it is not spawned), so the methods exist as stubs to preserve compilation. PR 2 wires
// them through the appropriate RPC gRPC service.

#[expect(clippy::unused_async)]
// Required endpoint implementations for the NTX `DataStore` implementation
impl RpcClient {
/// Fetches the transaction inputs for a specific account.
///
/// These inputs reference a specific `block_num`, and include a minimal partial account,
/// plus its witness.
pub async fn get_account_inputs(
&self,
_account_id: AccountId,
_block_num: BlockNumber,
account_id: AccountId,
block_num: BlockNumber,
) -> Result<AccountInputs, RpcError> {
unimplemented!("get_account_inputs is rewired in PR 2 of the ntx-builder refactor")
// Only request account code
let request = proto::rpc::AccountRequest {
account_id: Some(proto::account::AccountId { id: account_id.to_bytes() }),
block_num: Some(block_num.into()),
// TODO: should these commitments be cached on the NTX builder?
details: Some(proto::rpc::account_request::AccountDetailRequest {
code_commitment: Some(Word::default().into()),
asset_vault_commitment: None, //
storage_maps: vec![],
}),
};

let response = self.get_account(request).await?;
let details = response.details.as_ref().ok_or_else(|| {
RpcError::InvalidResponse("response did not include account details".into())
})?;
let partial_account = build_minimal_partial_account(details)?;

Ok(AccountInputs::new(partial_account, response.witness))
}

/// Fetches asset vault witnesses for the given keys at the reference block.
pub async fn get_vault_asset_witnesses(
&self,
_account_id: AccountId,
_vault_keys: BTreeSet<AssetVaultKey>,
_block_num: Option<BlockNumber>,
account_id: AccountId,
vault_keys: BTreeSet<AssetVaultKey>,
block_num: Option<BlockNumber>,
) -> Result<Vec<AssetWitness>, RpcError> {
unimplemented!("get_vault_asset_witnesses is rewired in PR 2 of the ntx-builder refactor")
if vault_keys.is_empty() {
return Ok(Vec::new());
}

let request = proto::rpc::AccountRequest {
account_id: Some(proto::account::AccountId { id: account_id.to_bytes() }),
block_num: block_num.map(Into::into),
details: Some(proto::rpc::account_request::AccountDetailRequest {
code_commitment: None,
asset_vault_commitment: Some(Word::default().into()),
storage_maps: vec![],
}),
};

let response = self.get_account(request).await?;
let assets: Vec<Asset> = match response.details.map(|details| details.vault_details) {
Some(AccountVaultDetails::Assets(assets)) => assets,
Some(AccountVaultDetails::LimitExceeded) => {
// NOTE: in the tx kernel, `get_vault_asset_witnesses` is called either for single
// asset keys, or when pre-loading all the assets related to input notes involved in
// the transaction. This should never exceed the maximum amount of keys you can
// request to RPC, but this needs double-checking. If it able to exceed them,
// batching needs to be implemented as a workaround.
panic!("should never exceed maximum number of requested keys")
},
None => Vec::new(),
};

let vault =
AssetVault::new(&assets).map_err(|err| RpcError::InvalidResponse(err.as_report()))?;

Ok(vault_keys.into_iter().map(|key| vault.open(key)).collect())
}

/// Fetches a storage map witness for a single key at the reference block.
pub async fn get_storage_map_witness(
&self,
_account_id: AccountId,
_slot_name: StorageSlotName,
_map_key: StorageMapKey,
_block_num: Option<BlockNumber>,
account_id: AccountId,
slot_name: StorageSlotName,
map_key: StorageMapKey,
block_num: Option<BlockNumber>,
) -> Result<StorageMapWitness, RpcError> {
unimplemented!("get_storage_map_witness is rewired in PR 2 of the ntx-builder refactor")
let request = proto::rpc::AccountRequest {
account_id: Some(proto::account::AccountId { id: account_id.to_bytes() }),
block_num: block_num.map(Into::into),
details: Some(proto::rpc::account_request::AccountDetailRequest {
code_commitment: None,
asset_vault_commitment: None,
storage_maps: vec![StorageMapDetailRequest {
slot_name: slot_name.to_string(),
slot_data: Some(storage_map_detail_request::SlotData::MapKeys(MapKeys {
map_keys: vec![map_key.into()],
})),
}],
}),
};

let response = self.get_account(request).await?;
let details = response.details.as_ref().ok_or_else(|| {
RpcError::InvalidResponse("response did not include account details".into())
})?;

let map_details = details
.storage_details
.map_details
.iter()
.find(|detail| detail.slot_name == slot_name)
.ok_or_else(|| {
RpcError::InvalidResponse(format!(
"response is missing storage map details for slot {slot_name}"
))
})?;

let StorageMapEntries::EntriesWithProofs(proofs) = &map_details.entries else {
return Err(RpcError::InvalidResponse(
"response did not include storage map entry proofs".into(),
));
};

let proof = proofs.first().cloned().ok_or_else(|| {
RpcError::InvalidResponse(
"response did not include a proof for the requested key".into(),
)
})?;

StorageMapWitness::new(proof, [map_key])
.map_err(|err| RpcError::InvalidResponse(err.as_report()))
}

/// Fetches a note script by its root, returning `None` if the node does not know it.
#[instrument(target = COMPONENT, name = "ntx.rpc.client.get_note_script_by_root", skip_all, err)]
pub async fn get_note_script_by_root(
&self,
_script_root: Word,
script_root: Word,
) -> Result<Option<NoteScript>, RpcError> {
unimplemented!("get_note_script_by_root is rewired in PR 2 of the ntx-builder refactor")
let request = proto::note::NoteScriptRoot { root: Some(script_root.into()) };

let script = self
.inner
.clone()
.get_note_script_by_root(request)
.await
.map_err(RpcError::GrpcClientError)?
.into_inner()
.script;

script.map(NoteScript::try_from).transpose().map_err(RpcError::Conversion)
}

/// Issues a `GetAccount` request and decodes the response into the domain [`AccountResponse`].
async fn get_account(
&self,
request: proto::rpc::AccountRequest,
) -> Result<AccountResponse, RpcError> {
let response = self
.inner
.clone()
.get_account(request)
.await
.map_err(RpcError::GrpcClientError)?
.into_inner();

AccountResponse::try_from(response).map_err(RpcError::Conversion)
}
}

/// Builds a minimal partial account from account details.
fn build_minimal_partial_account(details: &AccountDetails) -> Result<PartialAccount, RpcError> {
let code_bytes = details
.account_code
.as_ref()
.ok_or_else(|| RpcError::InvalidResponse("response did not include account code".into()))?;
let account_code = AccountCode::read_from_bytes(code_bytes).map_err(RpcError::Deserialize)?;

let partial_storage = PartialStorage::new(details.storage_details.header.clone(), [])
.map_err(|err| RpcError::InvalidResponse(err.as_report()))?;

let partial_vault = PartialVault::new(details.account_header.vault_root());

PartialAccount::new(
details.account_header.id(),
details.account_header.nonce(),
account_code,
partial_storage,
partial_vault,
None,
)
.map_err(|err| RpcError::InvalidResponse(err.as_report()))
}

// RPC ERROR
Expand All @@ -201,4 +372,8 @@ pub enum RpcError {
GrpcClientError(#[source] tonic::Status),
#[error("failed to deserialize RPC payload")]
Deserialize(#[source] miden_protocol::utils::serde::DeserializationError),
#[error("failed to convert RPC response")]
Conversion(#[source] ConversionError),
#[error("invalid RPC response: {0}")]
InvalidResponse(String),
}
2 changes: 1 addition & 1 deletion bin/ntx-builder/src/db/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ mod tests {
use super::*;

const EXPECTED_SCHEMA_HASHES: [SchemaHash; 1] = [SchemaHash::from_hex(
"e7383731af6f594a2f84ea8c3863325f0219899cff13e1396630c4ea8fed8157",
"8f580504230fb5ebc91bdf3e99f316bd919ec7e7312a45cbc8a52682edf8e68c",
)];

#[test]
Expand Down
3 changes: 3 additions & 0 deletions bin/ntx-builder/src/db/migrations/001_initial.sql
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ CREATE TABLE chain_state (
block_header BLOB NOT NULL,
-- Serialized PartialMmr corresponding to `block_header`.
chain_mmr BLOB NOT NULL,
-- Serialized genesis block commitment (Word). Set once at bootstrap and retained across tip
-- updates; used for the `genesis` Accept-header param required by write RPCs.
genesis_commitment BLOB,

CONSTRAINT chain_state_block_num_is_u32 CHECK (block_num BETWEEN 0 AND 0xFFFFFFFF)
);
Expand Down
16 changes: 16 additions & 0 deletions bin/ntx-builder/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,30 @@ impl Db {
"ntx-builder database is already bootstrapped",
);

let genesis_commitment = genesis.header().commitment();

let effects = CommittedBlockEffects::from_signed_block(genesis);
db.apply_committed_block(effects, PartialMmr::default())
.await
.context("failed to insert genesis block")?;

db.inner
.transact("set_genesis_commitment", move |conn| {
queries::set_genesis_commitment(conn, &genesis_commitment)
})
.await
.context("failed to persist genesis commitment")?;

Ok(())
}

/// Reads the genesis block commitment persisted at bootstrap.
pub async fn get_genesis_commitment(&self) -> Result<Word> {
self.inner
.query("get_genesis_commitment", queries::select_genesis_commitment)
.await
}

// BLOCK APPLICATION
// ============================================================================================

Expand Down
Loading
Loading