Skip to content
Open
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 @@ -20,6 +20,7 @@

### Changes

- [BREAKING] Removed `GetAccountDetails` RPC endpoint. Use `GetAccount` instead ([#1185](https://github.com/0xMiden/miden-node/issues/1185)).
- [BREAKING] Renamed `SyncTransactions` response fields ([#1357](https://github.com/0xMiden/miden-node/pull/1357)).
- Normalize response size in endpoints to 4 MB ([#1357](https://github.com/0xMiden/miden-node/pull/1357)).
- [BREAKING] Renamed `ProxyWorkerStatus::address` to `ProxyWorkerStatus::name` ([#1348](https://github.com/0xMiden/miden-node/pull/1348)).
Expand Down
209 changes: 187 additions & 22 deletions bin/network-monitor/src/counter.rs
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: not for this PR, but I'd try to break up this file into a couple of smaller files.

Original file line number Diff line number Diff line change
Expand Up @@ -83,51 +83,216 @@ async fn get_genesis_block_header(rpc_client: &mut RpcClient) -> Result<BlockHea
Ok(block_header)
}

/// Fetch the storage header of the given account from RPC.
///
/// Returns `None` if the account does not exist or has no details available.
async fn fetch_account_storage_header(
rpc_client: &mut RpcClient,
account_id: AccountId,
) -> Result<Option<miden_node_proto::generated::account::AccountStorageHeader>> {
let request = build_account_request(account_id, false);
let resp = rpc_client.get_account(request).await?.into_inner();

let Some(details) = resp.details else {
return Ok(None);
};

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

Ok(Some(storage_header))
}

/// Fetch the latest nonce of the given account from RPC.
async fn fetch_counter_value(
rpc_client: &mut RpcClient,
account_id: AccountId,
) -> Result<Option<u64>> {
let id_bytes: [u8; 15] = account_id.into();
let req = miden_node_proto::generated::account::AccountId { id: id_bytes.to_vec() };
let resp = rpc_client.get_account_details(req).await?.into_inner();
if let Some(raw) = resp.details {
let account = Account::read_from_bytes(&raw)
.map_err(|e| anyhow::anyhow!("failed to deserialize account details: {e}"))?;
let Some(storage_header) = fetch_account_storage_header(rpc_client, account_id).await? else {
return Ok(None);
};

let first_slot = storage_header.slots.first().context("no storage slots found")?;

// The counter value is stored as a Word, with the actual u64 value in the last element
let slot_value: Word = first_slot
.commitment
.as_ref()
.context("missing storage slot value")?
.try_into()
.context("failed to convert slot value to word")?;

let storage_slot = account.storage().slots().first().expect("storage slot is always value");
let word = storage_slot.value();
let value = word.as_elements().last().expect("a word is always 4 elements").as_int();
let value = slot_value.as_elements().last().expect("Word has 4 elements").as_int();

Ok(Some(value))
Ok(Some(value))
}

/// Build an account request for the given account ID.
///
/// If `include_code_and_vault` is true, uses dummy commitments to force the server
/// to return code and vault data (server only returns data when our commitment differs).
fn build_account_request(
account_id: AccountId,
include_code_and_vault: bool,
) -> miden_node_proto::generated::rpc::AccountRequest {
let id_bytes: [u8; 15] = account_id.into();
let account_id_proto =
miden_node_proto::generated::account::AccountId { id: id_bytes.to_vec() };

let (code_commitment, asset_vault_commitment) = if include_code_and_vault {
let dummy: miden_node_proto::generated::primitives::Digest = Word::default().into();
(Some(dummy), Some(dummy))
} else {
Ok(None)
(None, None)
};

miden_node_proto::generated::rpc::AccountRequest {
account_id: Some(account_id_proto),
block_num: None,
details: Some(miden_node_proto::generated::rpc::account_request::AccountDetailRequest {
code_commitment,
asset_vault_commitment,
storage_maps: vec![],
}),
}
}

/// 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 id_bytes: [u8; 15] = account_id.into();
let req = miden_node_proto::generated::account::AccountId { id: id_bytes.to_vec() };
let resp = rpc_client.get_account_details(req).await;
use miden_protocol::account::AccountCode;
use miden_protocol::asset::AssetVault;

// If the RPC call fails, return None
if resp.is_err() {
return Ok(None);
}
let request = build_account_request(account_id, true);

let Some(account_details) = resp.expect("Previously checked for error").into_inner().details
else {
let response = match rpc_client.get_account(request).await {
Ok(response) => response.into_inner(),
Err(e) => {
warn!(account.id = %account_id, err = %e, "failed to fetch wallet account via RPC");
return Ok(None);
},
};

let Some(details) = response.details else {
if response.witness.is_some() {
info!(
account.id = %account_id,
"account found on-chain but cannot reconstruct full account from RPC response"
);
}
return Ok(None);
};
let account = Account::read_from_bytes(&account_details)
.map_err(|e| anyhow::anyhow!("failed to deserialize account details: {e}"))?;

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

let code = details
.code
.map(|code_bytes| AccountCode::read_from_bytes(&code_bytes))
.transpose()
.context("failed to deserialize account code")?
.context("server did not return 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(TryInto::try_into)
.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(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!(account.id = %account_id, "fetched wallet account from RPC");
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 = slot
.commitment
.context("missing slot value")?
.try_into()
.context("invalid slot value")?;

// slot_type: 0 = Value, 1 = Map
anyhow::ensure!(
slot.slot_type == 0,
"storage map slots are not supported for this account"
);

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

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

async fn setup_increment_task(
config: MonitorConfig,
rpc_client: &mut RpcClient,
Expand Down
Loading