Skip to content
Merged
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ A quick overview of the binaries:
blocks.
- [`network-monitor`](./bin/network-monitor/README.md): a tool which monitors a network's infrastructure, e.g. block
production, RPC, validator, prover, faucet, explorer, and note transport.
- [`funding-service`](./bin/funding-service/README.md): sends the chain's native asset to any account which asks for it,
so infrastructure can pay transaction fees.

There are additional binaries but they're more supplementary; see their READMEs for more information.

Expand Down
12 changes: 8 additions & 4 deletions bin/funding-service/src/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@ pub async fn execute(
.await
.context("the funding transaction task failed")??;

// The notes and the expiration are what the caller promises to the requester, so a mismatch
// must fail here instead of after the transaction is submitted.
// The notes are what the caller promises to the requester, so a mismatch must fail here instead
// of after the transaction is submitted.
let output_note_ids: Vec<_> =
executed_tx.output_notes().iter().map(RawOutputNote::id).collect();
for note in &notes {
Expand All @@ -130,9 +130,13 @@ pub async fn execute(
note.id(),
);
}

// A procedure which reads mutable state through a foreign call lowers the transaction's
// expiration delta, and the kernel keeps the lowest value.
anyhow::ensure!(
executed_tx.expiration_block_num() == expected_expiration,
"the executed transaction expires at block {} instead of {expected_expiration}",
executed_tx.expiration_block_num() <= expected_expiration,
"the executed transaction expires at block {}, after the requested block \
{expected_expiration}",
executed_tx.expiration_block_num(),
);

Expand Down
6 changes: 3 additions & 3 deletions bin/network-monitor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ unless it can verify the advertised encryption key. The monitor obtains the acti
configuration returned by RPC and verifies it against the transaction's reference block.

On a chain with a non-zero verification base fee, network transaction checks additionally require
`MIDEN_MONITOR_FAUCET_URL`: the monitor funds its in-memory accounts by claiming the native fee asset from the faucet,
and it tops the balance up automatically when it runs low. Without a configured faucet the monitor refuses to start its
network transaction checks on such chains.
`MIDEN_MONITOR_FUNDING_SERVICE_URL`: the monitor funds its in-memory accounts from the funding service and tops the
balance up automatically when it runs low. Without it the monitor refuses to start its network transaction checks on
such chains. `MIDEN_MONITOR_FAUCET_URL` is only used for the faucet checks.

Use the binary help output for the current command and configuration surface. The help output is the source of truth for
flags and environment variables.
Expand Down
18 changes: 18 additions & 0 deletions bin/network-monitor/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,24 @@ pub struct MonitorConfig {
)]
pub faucet_url: Option<Url>,

/// The URL of the funding service (optional).
#[arg(
long = "funding-service-url",
env = "MIDEN_MONITOR_FUNDING_SERVICE_URL",
help = "The URL of the funding service (optional)"
)]
pub funding_service_url: Option<Url>,

/// Timeout for a funding request to the funding service.
#[arg(
long = "funding-request-timeout",
env = "MIDEN_MONITOR_FUNDING_REQUEST_TIMEOUT",
default_value = "2m",
value_parser = humantime::parse_duration,
help = "Timeout for a funding request to the funding service"
)]
pub funding_request_timeout: Duration,

/// The interval at which to test the remote provers services.
#[arg(
long = "remote-prover-test-interval",
Expand Down
200 changes: 32 additions & 168 deletions bin/network-monitor/src/counter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@ use std::time::{Duration, Instant};
use anyhow::{Context, Result};
use miden_node_proto::clients::RpcClient;
use miden_node_proto::generated::account::account_storage_header::storage_slot::Content as SlotContent;
use miden_node_proto::{DecodeMessage, Verify};
use miden_node_tracing::spawn::spawn_blocking_in_current_span;
use miden_node_tracing::{debug, error, info, miden_instrument, warn};
use miden_protocol::account::auth::AuthSecretKey;
use miden_protocol::account::{Account, AccountCode, AccountId, AccountPatch};
use miden_protocol::asset::{AssetId, AssetVault};
use miden_protocol::account::{Account, AccountId, AccountPatch};
use miden_protocol::asset::AssetId;
use miden_protocol::block::BlockNumber;
use miden_protocol::crypto::dsa::falcon512_poseidon2::SecretKey;
use miden_protocol::crypto::rand::{FeltRng, RandomCoin};
Expand Down Expand Up @@ -51,8 +50,10 @@ use crate::deploy::{
TransactionSubmissionClient,
create_and_deploy_accounts,
create_genesis_aware_rpc_client,
fetch_account_at_tip,
refresh_counter_anchor,
};
use crate::funding::{FaucetClient, FeeFunder, wallet_funding_amount, wallet_topup_threshold};
use crate::funding::{FeeFunder, FundingClient, wallet_funding_amount, wallet_topup_threshold};
use crate::service::Service;
use crate::status::{
CounterTrackingDetails,
Expand Down Expand Up @@ -195,8 +196,8 @@ pub struct IncrementService {
accounts_sender: watch::Sender<TrackedAccounts>,
/// Shared client for attestation verification, sealing, and transaction submission.
submission_client: TransactionSubmissionClient,
/// Faucet access for fee funding; `None` when no faucet is configured (zero-fee chains only).
funding: Option<FaucetClient>,
/// The funding service client; `None` when none is configured (zero-fee chains only).
funding: Option<FundingClient>,
Comment on lines +199 to +200

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not this PRs problem, but I wonder if we can't somehow make it so that these things are always required, even on zero-fee chains.

One option is of course to just not have zero fee chains.. but I was hoping the flows could remain agnostic to the fee amount, as in fee=N holds even for N=0.

/// Committed faucet note to be consumed by the next increment. Cleared once consumed.
pending_funding_note: Option<Note>,
}
Expand All @@ -213,7 +214,7 @@ impl IncrementService {
submission_client: TransactionSubmissionClient,
accounts_sender: watch::Sender<TrackedAccounts>,
latency_state: Arc<Mutex<LatencyState>>,
funding: Option<FaucetClient>,
funding: Option<FundingClient>,
) -> Result<Self> {
let rpc_client = submission_client.rpc_client();
let pending_funding_note = accounts.wallet_funding_note;
Expand Down Expand Up @@ -279,25 +280,20 @@ impl IncrementService {
err,
)]
async fn try_resync_wallet_account(&mut self) -> Result<()> {
let fresh_account = fetch_wallet_account(&mut self.rpc_client, self.tx.wallet_account.id())
.await
.inspect_err(|e| {
error!(
e,
target: LOG_TARGET,
"Failed to re-sync wallet account from RPC",
account.id = self.tx.wallet_account.id()
);
})?
.context("wallet account not found on-chain during re-sync")
.inspect_err(|e| {
error!(
e,
target: LOG_TARGET,
"Wallet account not found on-chain during re-sync",
account.id = self.tx.wallet_account.id()
);
})?;
let fresh_account = fetch_account_at_tip(
&mut self.rpc_client,
self.tx.wallet_account.id(),
self.submission_client.genesis_commitment(),
)
.await
.inspect_err(|e| {
error!(
e,
target: LOG_TARGET,
"Failed to re-sync wallet account from RPC",
account.id = self.tx.wallet_account.id()
);
})?;

debug!(
target: LOG_TARGET,
Expand Down Expand Up @@ -364,6 +360,15 @@ impl IncrementService {
err,
)]
async fn submit_increment(&mut self) -> Result<(String, AccountPatch, BlockNumber)> {
let anchor = refresh_counter_anchor(
&mut self.rpc_client,
self.tx.counter_id,
self.submission_client.genesis_commitment(),
)
.await
.context("failed to refresh the counter anchor")?;
self.tx.counter_anchor = Arc::new(anchor);

let (network_note, note_recipient) = create_network_note(
&self.tx.wallet_account,
self.tx.counter_id,
Expand Down Expand Up @@ -487,7 +492,7 @@ impl IncrementService {
account.id = self.tx.wallet_account.id(),
asset.balance = balance
);
let mut funder = FeeFunder::new(funding, self.rpc_client.clone(), fee_faucet_id);
let mut funder = FeeFunder::new(funding, fee_faucet_id);
match funder
.fund(self.tx.wallet_account.id(), wallet_funding_amount(verification_base_fee))
.await
Expand Down Expand Up @@ -1057,147 +1062,6 @@ fn build_account_request(
}
}

/// Fetch an account from RPC and reconstruct the full Account.
///
/// Uses dummy commitments to force the server to return all data (code, vault, storage header).
/// Only supports accounts with value slots; returns an error if storage maps are present.
async fn fetch_wallet_account(
rpc_client: &mut RpcClient,
account_id: AccountId,
) -> Result<Option<Account>> {
let request = build_account_request(account_id, true);

let response = match rpc_client.get_account(request).await {
Ok(response) => response.into_inner(),
Err(e) => {
warn!(
&e,
target: LOG_TARGET,
"Failed to fetch wallet account via RPC",
account.id = account_id
);
return Ok(None);
},
};

let Some(details) = response.details else {
if response.witness.is_some() {
info!(
target: LOG_TARGET,
"account found on-chain but cannot reconstruct full account from RPC response",
account.id = account_id
);
}
return Ok(None);
};

let header = details.header.context("missing account header")?;
let nonce: u64 = header.nonce;

let code: AccountCode = details
.code
.context("server did not return account code")?
.decode_fields()
.context("failed to decode account code")?
.verify()
.context("failed to verify account code")?;

let vault = match details.vault_details {
Some(vault_details) if vault_details.too_many_assets => {
anyhow::bail!("account {account_id} has too many assets, cannot fetch full account");
},
Some(vault_details) => {
let assets: Vec<miden_protocol::asset::Asset> = vault_details
.assets
.into_iter()
.map(|asset| {
asset
.decode_fields()
.map_err(anyhow::Error::from)
.and_then(|asset| asset.verify().map_err(anyhow::Error::from))
})
.collect::<Result<_, _>>()
.context("failed to convert assets")?;
AssetVault::new(&assets).context("failed to create vault")?
},
None => anyhow::bail!("server did not return asset vault for account {account_id}"),
};

let storage_details = details.storage_details.context("missing storage details")?;
let storage = build_account_storage(storage_details)?;

let account = Account::new(account_id, vault, storage, code, Felt::new_unchecked(nonce), None)
.context("failed to create account")?;

// Sanity check: verify reconstructed account matches header commitments
let expected_code_commitment: Word = header
.code_commitment
.context("missing code commitment in header")?
.try_into()
.context("invalid code commitment")?;
let expected_vault_root: Word = header
.vault_root
.context("missing vault root in header")?
.try_into()
.context("invalid vault root")?;
let expected_storage_commitment: Word = header
.storage_commitment
.context("missing storage commitment in header")?
.try_into()
.context("invalid storage commitment")?;

anyhow::ensure!(
account.code().commitment() == expected_code_commitment,
"code commitment mismatch: rebuilt={:?}, expected={:?}",
account.code().commitment(),
expected_code_commitment
);
anyhow::ensure!(
account.vault().root() == expected_vault_root,
"vault root mismatch: rebuilt={:?}, expected={:?}",
account.vault().root(),
expected_vault_root
);
anyhow::ensure!(
account.storage().to_commitment() == expected_storage_commitment,
"storage commitment mismatch: rebuilt={:?}, expected={:?}",
account.storage().to_commitment(),
expected_storage_commitment
);

info!(target: LOG_TARGET, "Fetched wallet account from RPC", account.id = account_id);
Ok(Some(account))
}

/// Build account storage from the storage details returned by the server.
///
/// This function only supports accounts with value slots. If any storage map slots
/// are encountered, an error is returned since the monitor only uses simple accounts.
fn build_account_storage(
storage_details: miden_node_proto::generated::rpc::AccountStorageDetails,
) -> Result<miden_protocol::account::AccountStorage> {
use miden_protocol::account::{AccountStorage, StorageSlot};

let storage_header = storage_details.header.context("missing storage header")?;

let mut slots = Vec::new();
for slot in storage_header.slots {
let slot_name = miden_protocol::account::StorageSlotName::new(slot.slot_name.clone())
.context("invalid slot name")?;
let value: Word = match slot.content {
Some(SlotContent::Value(value)) => value.try_into().context("invalid slot value")?,
Some(SlotContent::MapRoot(_)) => {
anyhow::bail!("storage map slots are not supported for this account")
},
None => anyhow::bail!("missing slot value"),
};

slots.push(StorageSlot::with_value(slot_name, value));
}

AccountStorage::new(slots).context("failed to create account storage")
}

/// Create the increment procedure script.
pub(crate) fn create_increment_script() -> Result<NoteScript> {
let script =
Expand Down
Loading
Loading