Skip to content
Closed
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
- [BREAKING] Renamed `SubmitProvenTransaction` RPC endpoint to `SubmitProvenTx` ([#2094](https://github.com/0xMiden/node/pull/2094)).
- [BREAKING] Renamed `SubmitProvenBatch` RPC endpoint to `SubmitProvenTxBatch` ([#2094](https://github.com/0xMiden/node/pull/2094)).
- Updated `miden-protocol` and bumped `miden-crypto` to `v0.25`. `AccountId::is_network()` was removed upstream, so `SubmitProvenTx` and `SubmitProvenTxBatch` now consult the store to classify post-deployment public-account transactions as network accounts.
- The RPC service now memoizes store-confirmed network-account classifications in memory, so repeat post-deployment submissions for a known network account are rejected by `SubmitProvenTx`/`SubmitProvenTxBatch` without a store round-trip. Classification is fixed at account creation, so cached entries never go stale ([#2145](https://github.com/0xMiden/node/pull/2145)).
- [BREAKING] Removed `Network` variant from genesis config `StorageMode`. The implicit default for wallets and fungible faucets is now `Private` (previously `Network`, which mapped to `Public` storage) ([#2095](https://github.com/0xMiden/node/pull/2095)).
- [BREAKING] Updated `miden-protocol` family of crates to the published `v0.15.0` on crates.io (previously tracked the `next` branch). The published release removes the multi-variant `AccountType` (`RegularAccount*`, `FungibleFaucet`, `NonFungibleFaucet`) and renames the former `AccountStorageMode` to `AccountType` with only `Public`/`Private`. Faucet-vs-wallet distinction is now component-based.
- [BREAKING] Removed the `has_updatable_code` field from genesis `[[wallet]]` config entries. Updatable/immutable code is no longer modeled by the protocol, so any genesis config that explicitly sets this field will fail to parse — remove the field.
Expand Down
104 changes: 68 additions & 36 deletions crates/rpc/src/server/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use miden_node_utils::limiter::{
};
use miden_node_utils::lru_cache::LruCache;
use miden_node_utils::tracing::OpenTelemetrySpanExt;
use miden_protocol::account::AccountId;
use miden_protocol::batch::{ProposedBatch, ProvenBatch};
use miden_protocol::block::{BlockHeader, BlockNumber};
use miden_protocol::transaction::{
Expand All @@ -49,13 +50,17 @@ use crate::COMPONENT;
// RPC SERVICE
// ================================================================================================

/// Error returned when a user submits a transaction for an account classified as a network account.
const NETWORK_TX_REJECTION_MSG: &str = "Network transactions may not be submitted by users yet";

pub struct RpcService {
store: StoreRpcClient,
block_producer: Option<BlockProducerClient>,
validator: ValidatorClient,
ntx_builder: Option<NtxBuilderClient>,
genesis_commitment: Option<Word>,
block_commitment_cache: LruCache<BlockNumber, Word>,
network_account_cache: LruCache<AccountId, ()>,
}

impl RpcService {
Expand All @@ -65,6 +70,7 @@ impl RpcService {
validator_url: Url,
ntx_builder_url: Option<Url>,
commitment_cache_capacity: NonZeroUsize,
network_account_cache_capacity: NonZeroUsize,
) -> Self {
let store = {
info!(target: COMPONENT, store_endpoint = %store_url, "Initializing store client");
Expand Down Expand Up @@ -129,6 +135,7 @@ impl RpcService {
ntx_builder,
genesis_commitment: None,
block_commitment_cache: LruCache::new(commitment_cache_capacity),
network_account_cache: LruCache::new(network_account_cache_capacity),
}
}

Expand Down Expand Up @@ -237,6 +244,58 @@ impl RpcService {

Ok(())
}

/// Rejects the request if any of `candidate_ids` is classified as a network account.
///
/// Known network accounts are served from the local LRU cache; on a cache miss the store is
/// the source of truth and any account it confirms is memoized so later gate checks skip the
/// store. Callers should pre-filter to post-deployment, public-account ids; `Ok(())` on empty.
#[tracing::instrument(target = COMPONENT, name = "reject_if_any_network_accounts", skip_all)]
async fn reject_if_any_network_accounts(
&self,
candidate_ids: Vec<AccountId>,
) -> Result<(), Status> {
if candidate_ids.is_empty() {
return Ok(());
}

// A cached id is a known network account, so the gate fails without touching the store.
if self
.network_account_cache
.get_many(candidate_ids.iter())
.iter()
.any(Option::is_some)
{
return Err(Status::invalid_argument(NETWORK_TX_REJECTION_MSG));
}

let response = self
.store
.clone()
.are_network_accounts(tonic::Request::new(proto::account::AccountIdList {
account_ids: candidate_ids.iter().map(|id| (*id).into()).collect(),
}))
.await
.map_err(|err| {
Status::internal(format!("network-account classification failed: {err}"))
})?;

let network_ids: Vec<AccountId> = read_account_ids(
response.into_inner().network_account_ids,
)
.map_err(|err: ConversionError| {
Status::internal(format!("malformed network-account response: {err}"))
})?;

if network_ids.is_empty() {
return Ok(());
}

// Memoize the confirmed network accounts so subsequent gate checks skip the store.
self.network_account_cache.put_many(network_ids.into_iter().map(|id| (id, ())));

Err(Status::invalid_argument(NETWORK_TX_REJECTION_MSG))
}
}

// API IMPLEMENTATION
Expand Down Expand Up @@ -523,24 +582,14 @@ impl api_server::Api for RpcService {
// account; the store is the source of truth because network-ness now lives in account
// storage and isn't derivable from an AccountId alone. Network accounts must be public, so
// private-account txs short-circuit and skip the store roundtrip.
if !tx.account_update().initial_state_commitment().is_empty() && tx.account_id().is_public()
let candidate_ids = if !tx.account_update().initial_state_commitment().is_empty()
&& tx.account_id().is_public()
{
let response = self
.store
.clone()
.are_network_accounts(tonic::Request::new(proto::account::AccountIdList {
account_ids: vec![tx.account_id().into()],
}))
.await
.map_err(|err| {
Status::internal(format!("network-account classification failed: {err}"))
})?;
if !response.into_inner().network_account_ids.is_empty() {
return Err(Status::invalid_argument(
"Network transactions may not be submitted by users yet",
));
}
}
vec![tx.account_id()]
} else {
Vec::new()
};
self.reject_if_any_network_accounts(candidate_ids).await?;

let tx_verifier = TransactionVerifier::new(MIN_PROOF_SECURITY_LEVEL);
tx_verifier.verify(&tx).map_err(|err| {
Expand Down Expand Up @@ -625,26 +674,9 @@ impl api_server::Api for RpcService {
!tx.account_update().initial_state_commitment().is_empty()
&& tx.account_id().is_public()
})
.map(|tx| proto::account::AccountId::from(tx.account_id()))
.map(|tx| tx.account_id())
.collect();

if !non_deployment_ids.is_empty() {
let response = self
.store
.clone()
.are_network_accounts(tonic::Request::new(proto::account::AccountIdList {
account_ids: non_deployment_ids,
}))
.await
.map_err(|err| {
Status::internal(format!("network-account classification failed: {err}"))
})?;
if !response.into_inner().network_account_ids.is_empty() {
return Err(Status::invalid_argument(
"Network transactions may not be submitted by users yet",
));
}
}
self.reject_if_any_network_accounts(non_deployment_ids).await?;

// Verify batch transaction proofs.
//
Expand Down
1 change: 1 addition & 0 deletions crates/rpc/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ impl Rpc {
self.validator_url,
self.ntx_builder_url.clone(),
NonZeroUsize::new(1_000_000).unwrap(),
NonZeroUsize::new(65_536).unwrap(),
);

let genesis = api
Expand Down
Loading