Skip to content

Commit f9005b3

Browse files
Merge branch 'next' into mirko/readmes
2 parents f603afe + ae56724 commit f9005b3

16 files changed

Lines changed: 334 additions & 59 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
- Added a `miden-ntx-builder bootstrap` command that initializes the ntx-builder database with the genesis block fetched from the node RPC. The `start` command now requires a bootstrapped database instead of fetching the genesis block from the committed-block subscription on first run ([#2149](https://github.com/0xMiden/node/pull/2149)).
5555
- Added `--tx-expiration-delta` (env `MIDEN_NODE_NTX_BUILDER_TX_EXPIRATION_DELTA`, default `30`) to the network transaction builder: submitted network transactions now expire on-chain after this many blocks, and the builder reuses the same delta as the local window before resubmitting a transaction that has not landed ([#2148](https://github.com/0xMiden/node/pull/2148)).
5656
- Added a `miden-ntx-builder bootstrap` command that initializes the ntx-builder database from a trusted genesis block file. The `start` command now requires a bootstrapped database instead of fetching the genesis block from the committed-block subscription on first run ([#2149](https://github.com/0xMiden/node/pull/2149)).
57+
- Persisted the genesis commitment in the ntx-builder at bootstrap and sent it in the RPC `Accept` header so the node accepts its write transactions, and added RPC client implementations ([#2162](https://github.com/0xMiden/node/pull/2162)).
58+
- Persisted attachments of private output notes when applying a block, so they are now returned by `GetNotesById` ([#2172](https://github.com/0xMiden/node/pull/2172)).
5759

5860
## v0.14.11 (TBD)
5961

bin/node/src/commands/rpc.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ use std::num::{NonZeroU32, NonZeroU64};
33
use std::time::Duration;
44

55
use anyhow::Context;
6-
use miden_node_rpc::NetworkTxAuth;
76
use miden_node_utils::clap::{GrpcOptionsExternal, duration_to_human_readable_string};
87
use tonic::metadata::AsciiMetadataValue;
98
use url::Url;
@@ -44,13 +43,12 @@ impl RpcOptions {
4443
}
4544
}
4645

47-
pub(super) fn network_tx_auth(&self) -> anyhow::Result<Option<NetworkTxAuth>> {
46+
pub(super) fn network_tx_auth(&self) -> anyhow::Result<Option<AsciiMetadataValue>> {
4847
self.network_tx_auth_header_value
4948
.as_deref()
5049
.map(|value| {
5150
value
5251
.parse::<AsciiMetadataValue>()
53-
.map(NetworkTxAuth)
5452
.context("invalid rpc.network-tx-auth-header-value")
5553
})
5654
.transpose()

bin/ntx-builder/src/actor/execute.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,9 @@ pub struct NtxContext {
156156

157157
/// Pre-compiled transaction script that sets the network tx's on-chain expiration delta. Cloned
158158
/// into the [`TransactionArgs`] of the executed transaction.
159+
///
160+
/// TEMP: disabled until the resolution of <https://github.com/0xMiden/protocol/issues/3027>
161+
#[expect(dead_code)]
159162
expiration_script: TransactionScript,
160163

161164
/// [`ExponentialBuilder`] used to back off retries on transient request failures.
@@ -382,7 +385,11 @@ impl NtxContext {
382385

383386
// Attach the pre-compiled expiration script so the submitted tx is rejected on-chain if it
384387
// does not land within the configured block delta.
385-
let tx_args = TransactionArgs::default().with_tx_script(self.expiration_script.clone());
388+
//
389+
// TEMP: disabled until the resolution of https://github.com/0xMiden/protocol/issues/3027
390+
// let tx_args = TransactionArgs::default().with_tx_script(self.expiration_script.clone());
391+
392+
let tx_args = TransactionArgs::default();
386393

387394
Box::pin(executor.execute_transaction(
388395
data_store.account.id(),

bin/ntx-builder/src/actor/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ impl AccountActorContext {
153153
clients: GrpcClients {
154154
rpc: RpcClient::new(
155155
url.clone(),
156+
miden_protocol::Word::default(),
156157
Duration::from_millis(100),
157158
Duration::from_secs(30),
158159
),

bin/ntx-builder/src/clients/rpc.rs

Lines changed: 201 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,26 @@ use backon::{ExponentialBuilder, Retryable};
55
use futures::Stream;
66
use futures::stream::TryStreamExt;
77
use miden_node_proto::clients::{Builder, RpcClient as InnerRpcClient};
8+
use miden_node_proto::domain::account::{
9+
AccountDetails, AccountResponse, AccountVaultDetails, StorageMapEntries
10+
};
11+
use miden_node_proto::errors::ConversionError;
12+
use miden_node_proto::generated::rpc::account_request::account_detail_request::{StorageMapDetailRequest, StorageMapDetailRequests, StorageRequest, storage_map_detail_request};
13+
use miden_node_proto::generated::rpc::account_request::account_detail_request::storage_map_detail_request::MapKeys;
814
use miden_node_proto::generated::rpc::{BlockSubscriptionRequest, BlockSubscriptionResponse};
915
use miden_node_proto::generated::{self as proto};
1016
use miden_node_utils::ErrorReport;
1117
use miden_protocol::Word;
12-
use miden_protocol::account::{AccountId, StorageMapKey, StorageMapWitness, StorageSlotName};
13-
use miden_protocol::asset::{AssetVaultKey, AssetWitness};
18+
use miden_protocol::account::{
19+
AccountCode,
20+
AccountId,
21+
PartialAccount,
22+
PartialStorage,
23+
StorageMapKey,
24+
StorageMapWitness,
25+
StorageSlotName,
26+
};
27+
use miden_protocol::asset::{Asset, AssetVault, AssetVaultKey, AssetWitness, PartialVault};
1428
use miden_protocol::block::{BlockNumber, SignedBlock};
1529
use miden_protocol::note::NoteScript;
1630
use miden_protocol::transaction::{AccountInputs, ProvenTransaction, TransactionInputs};
@@ -41,14 +55,23 @@ impl RpcClient {
4155
///
4256
/// `backoff_initial` / `backoff_max` configure the exponential backoff schedule applied to
4357
/// `block_subscription` retries (the only operation that retries today).
44-
pub fn new(rpc_url: Url, backoff_initial: Duration, backoff_max: Duration) -> Self {
45-
Self::new_with_auth(rpc_url, None, backoff_initial, backoff_max)
58+
pub fn new(
59+
rpc_url: Url,
60+
genesis_commitment: Word,
61+
backoff_initial: Duration,
62+
backoff_max: Duration,
63+
) -> Self {
64+
Self::new_with_auth(rpc_url, None, genesis_commitment, backoff_initial, backoff_max)
4665
}
4766

4867
/// Creates a new client with an optional metadata header for internal RPC authentication.
68+
///
69+
/// `genesis_commitment` is sent as the `genesis` parameter of the `Accept` header so that the
70+
/// node accepts write RPCs such as `SubmitProvenTx`, which require a matching genesis.
4971
pub fn new_with_auth(
5072
rpc_url: Url,
5173
rpc_auth_header_value: Option<AsciiMetadataValue>,
74+
genesis_commitment: Word,
5275
backoff_initial: Duration,
5376
backoff_max: Duration,
5477
) -> Self {
@@ -58,7 +81,7 @@ impl RpcClient {
5881
.without_tls()
5982
.without_timeout()
6083
.without_metadata_version()
61-
.without_metadata_genesis();
84+
.with_metadata_genesis(genesis_commitment.to_hex());
6285
let builder = match rpc_auth_header_value {
6386
Some(value) => builder.with_auth_header_value(value),
6487
None => builder.without_auth_header(),
@@ -151,45 +174,195 @@ fn decode_block_subscription_response(
151174
// ACTOR-PATH METHODS
152175
// ================================================================================================
153176
//
154-
// The actor module still references these methods. PR 1 keeps the actor code in tree as dead
155-
// code (it is not spawned), so the methods exist as stubs to preserve compilation. PR 2 wires
156-
// them through the appropriate RPC gRPC service.
157-
158-
#[expect(clippy::unused_async)]
177+
// Required endpoint implementations for the NTX `DataStore` implementation
159178
impl RpcClient {
179+
/// Fetches the transaction inputs for a specific account.
180+
///
181+
/// These inputs reference a specific `block_num`, and include a minimal partial account,
182+
/// plus its witness.
160183
pub async fn get_account_inputs(
161184
&self,
162-
_account_id: AccountId,
163-
_block_num: BlockNumber,
185+
account_id: AccountId,
186+
block_num: BlockNumber,
164187
) -> Result<AccountInputs, RpcError> {
165-
unimplemented!("get_account_inputs is rewired in PR 2 of the ntx-builder refactor")
188+
// Only request account code
189+
let request = proto::rpc::AccountRequest {
190+
account_id: Some(proto::account::AccountId { id: account_id.to_bytes() }),
191+
block_num: Some(block_num.into()),
192+
// TODO: should these commitments be cached on the NTX builder?
193+
details: Some(proto::rpc::account_request::AccountDetailRequest {
194+
code_commitment: Some(Word::default().into()),
195+
asset_vault_commitment: None, //
196+
storage_request: None,
197+
}),
198+
};
199+
200+
let response = self.get_account(request).await?;
201+
let details = response.details.as_ref().ok_or_else(|| {
202+
RpcError::InvalidResponse("response did not include account details".into())
203+
})?;
204+
let partial_account = build_minimal_partial_account(details)?;
205+
206+
Ok(AccountInputs::new(partial_account, response.witness))
166207
}
167208

209+
/// Fetches asset vault witnesses for the given keys at the reference block.
168210
pub async fn get_vault_asset_witnesses(
169211
&self,
170-
_account_id: AccountId,
171-
_vault_keys: BTreeSet<AssetVaultKey>,
172-
_block_num: Option<BlockNumber>,
212+
account_id: AccountId,
213+
vault_keys: BTreeSet<AssetVaultKey>,
214+
block_num: Option<BlockNumber>,
173215
) -> Result<Vec<AssetWitness>, RpcError> {
174-
unimplemented!("get_vault_asset_witnesses is rewired in PR 2 of the ntx-builder refactor")
216+
if vault_keys.is_empty() {
217+
return Ok(Vec::new());
218+
}
219+
220+
let request = proto::rpc::AccountRequest {
221+
account_id: Some(proto::account::AccountId { id: account_id.to_bytes() }),
222+
block_num: block_num.map(Into::into),
223+
details: Some(proto::rpc::account_request::AccountDetailRequest {
224+
code_commitment: None,
225+
asset_vault_commitment: Some(Word::default().into()),
226+
storage_request: None,
227+
}),
228+
};
229+
230+
let response = self.get_account(request).await?;
231+
let assets: Vec<Asset> = match response.details.map(|details| details.vault_details) {
232+
Some(AccountVaultDetails::Assets(assets)) => assets,
233+
Some(AccountVaultDetails::LimitExceeded) => {
234+
// NOTE: in the tx kernel, `get_vault_asset_witnesses` is called either for single
235+
// asset keys, or when pre-loading all the assets related to input notes involved in
236+
// the transaction. This should never exceed the maximum amount of keys you can
237+
// request to RPC, but this needs double-checking. If it able to exceed them,
238+
// batching needs to be implemented as a workaround.
239+
panic!("should never exceed maximum number of requested keys")
240+
},
241+
None => Vec::new(),
242+
};
243+
244+
let vault =
245+
AssetVault::new(&assets).map_err(|err| RpcError::InvalidResponse(err.as_report()))?;
246+
247+
Ok(vault_keys.into_iter().map(|key| vault.open(key)).collect())
175248
}
176249

250+
/// Fetches a storage map witness for a single key at the reference block.
177251
pub async fn get_storage_map_witness(
178252
&self,
179-
_account_id: AccountId,
180-
_slot_name: StorageSlotName,
181-
_map_key: StorageMapKey,
182-
_block_num: Option<BlockNumber>,
253+
account_id: AccountId,
254+
slot_name: StorageSlotName,
255+
map_key: StorageMapKey,
256+
block_num: Option<BlockNumber>,
183257
) -> Result<StorageMapWitness, RpcError> {
184-
unimplemented!("get_storage_map_witness is rewired in PR 2 of the ntx-builder refactor")
258+
let request = proto::rpc::AccountRequest {
259+
account_id: Some(proto::account::AccountId { id: account_id.to_bytes() }),
260+
block_num: block_num.map(Into::into),
261+
details: Some(proto::rpc::account_request::AccountDetailRequest {
262+
code_commitment: None,
263+
asset_vault_commitment: None,
264+
storage_request: Some(StorageRequest::StorageMaps(StorageMapDetailRequests {
265+
storage_maps: vec![StorageMapDetailRequest {
266+
slot_name: slot_name.to_string(),
267+
slot_data: Some(storage_map_detail_request::SlotData::MapKeys(MapKeys {
268+
map_keys: vec![map_key.into()],
269+
})),
270+
}],
271+
})),
272+
}),
273+
};
274+
275+
let response = self.get_account(request).await?;
276+
let details = response.details.as_ref().ok_or_else(|| {
277+
RpcError::InvalidResponse("response did not include account details".into())
278+
})?;
279+
280+
let map_details = details
281+
.storage_details
282+
.map_details
283+
.iter()
284+
.find(|detail| detail.slot_name == slot_name)
285+
.ok_or_else(|| {
286+
RpcError::InvalidResponse(format!(
287+
"response is missing storage map details for slot {slot_name}"
288+
))
289+
})?;
290+
291+
let StorageMapEntries::EntriesWithProofs(proofs) = &map_details.entries else {
292+
return Err(RpcError::InvalidResponse(
293+
"response did not include storage map entry proofs".into(),
294+
));
295+
};
296+
297+
let proof = proofs.first().cloned().ok_or_else(|| {
298+
RpcError::InvalidResponse(
299+
"response did not include a proof for the requested key".into(),
300+
)
301+
})?;
302+
303+
StorageMapWitness::new(proof, [map_key])
304+
.map_err(|err| RpcError::InvalidResponse(err.as_report()))
185305
}
186306

307+
/// Fetches a note script by its root, returning `None` if the node does not know it.
308+
#[instrument(target = COMPONENT, name = "ntx.rpc.client.get_note_script_by_root", skip_all, err)]
187309
pub async fn get_note_script_by_root(
188310
&self,
189-
_script_root: Word,
311+
script_root: Word,
190312
) -> Result<Option<NoteScript>, RpcError> {
191-
unimplemented!("get_note_script_by_root is rewired in PR 2 of the ntx-builder refactor")
313+
let request = proto::note::NoteScriptRoot { root: Some(script_root.into()) };
314+
315+
let script = self
316+
.inner
317+
.clone()
318+
.get_note_script_by_root(request)
319+
.await
320+
.map_err(RpcError::GrpcClientError)?
321+
.into_inner()
322+
.script;
323+
324+
script.map(NoteScript::try_from).transpose().map_err(RpcError::Conversion)
192325
}
326+
327+
/// Issues a `GetAccount` request and decodes the response into the domain [`AccountResponse`].
328+
async fn get_account(
329+
&self,
330+
request: proto::rpc::AccountRequest,
331+
) -> Result<AccountResponse, RpcError> {
332+
let response = self
333+
.inner
334+
.clone()
335+
.get_account(request)
336+
.await
337+
.map_err(RpcError::GrpcClientError)?
338+
.into_inner();
339+
340+
AccountResponse::try_from(response).map_err(RpcError::Conversion)
341+
}
342+
}
343+
344+
/// Builds a minimal partial account from account details.
345+
fn build_minimal_partial_account(details: &AccountDetails) -> Result<PartialAccount, RpcError> {
346+
let code_bytes = details
347+
.account_code
348+
.as_ref()
349+
.ok_or_else(|| RpcError::InvalidResponse("response did not include account code".into()))?;
350+
let account_code = AccountCode::read_from_bytes(code_bytes).map_err(RpcError::Deserialize)?;
351+
352+
let partial_storage = PartialStorage::new(details.storage_details.header.clone(), [])
353+
.map_err(|err| RpcError::InvalidResponse(err.as_report()))?;
354+
355+
let partial_vault = PartialVault::new(details.account_header.vault_root());
356+
357+
PartialAccount::new(
358+
details.account_header.id(),
359+
details.account_header.nonce(),
360+
account_code,
361+
partial_storage,
362+
partial_vault,
363+
None,
364+
)
365+
.map_err(|err| RpcError::InvalidResponse(err.as_report()))
193366
}
194367

195368
// RPC ERROR
@@ -201,4 +374,8 @@ pub enum RpcError {
201374
GrpcClientError(#[source] tonic::Status),
202375
#[error("failed to deserialize RPC payload")]
203376
Deserialize(#[source] miden_protocol::utils::serde::DeserializationError),
377+
#[error("failed to convert RPC response")]
378+
Conversion(#[source] ConversionError),
379+
#[error("invalid RPC response: {0}")]
380+
InvalidResponse(String),
204381
}

bin/ntx-builder/src/db/migrations.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ mod tests {
3030
use super::*;
3131

3232
const EXPECTED_SCHEMA_HASHES: [SchemaHash; 1] = [SchemaHash::from_hex(
33-
"e7383731af6f594a2f84ea8c3863325f0219899cff13e1396630c4ea8fed8157",
33+
"c631b773787903a3dd5ea4df5e7374119b3f02b35bacf14d11eacd8d8500e3d9",
3434
)];
3535

3636
#[test]

bin/ntx-builder/src/db/migrations/001_initial.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ CREATE TABLE chain_state (
1111
block_header BLOB NOT NULL,
1212
-- Serialized PartialMmr corresponding to `block_header`.
1313
chain_mmr BLOB NOT NULL,
14+
-- Serialized genesis block commitment (Word). Set once at bootstrap and retained across tip
15+
-- updates; used for the `genesis` Accept-header param required by write RPCs.
16+
genesis_commitment BLOB NOT NULL,
1417

1518
CONSTRAINT chain_state_block_num_is_u32 CHECK (block_num BETWEEN 0 AND 0xFFFFFFFF)
1619
);

0 commit comments

Comments
 (0)