Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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 @@ -19,6 +19,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
225 changes: 203 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 @@ -32,7 +32,6 @@ use miden_objects::note::{
NoteType,
};
use miden_objects::transaction::{InputNotes, PartialBlockchain, TransactionArgs};
use miden_objects::utils::Deserializable;
use miden_objects::{Felt, Word, ZERO};
use miden_tx::auth::BasicAuthenticator;
use miden_tx::utils::Serializable;
Expand Down Expand Up @@ -90,45 +89,227 @@ async fn fetch_counter_value(
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 account_id_proto =
miden_node_proto::generated::account::AccountId { id: id_bytes.to_vec() };

// Request account details with storage header (but no storage maps needed)
let request = 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: None,
asset_vault_commitment: None,
storage_maps: vec![],
}),
};

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 resp = rpc_client.get_account(request).await?.into_inner();

Ok(Some(value))
} else {
Ok(None)
// Extract the counter value from the storage header
if let Some(details) = resp.details {
let storage_details = details.storage_details.context("missing storage details")?;

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

// The counter value is in the first storage slot (index 0)
let first_slot = storage_header.slots.first().context("no storage slots found")?;

// For value slots, the slot data is the value itself (not a hash commitment)
let slot_value: Word = first_slot
.commitment
.as_ref()
.context("missing storage slot value")?
.try_into()
.context("failed to convert slot value to word")?;

// The counter value is stored in the last element of the Word
let value = slot_value.as_elements().last().expect("word has 4 elements").as_int();

return Ok(Some(value));
}

Ok(None)
}

/// 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).
/// Storage map slots will have empty maps since we don't request map entries.
async fn fetch_wallet_account(
rpc_client: &mut RpcClient,
account_id: AccountId,
) -> Result<Option<Account>> {
use miden_objects::account::AccountCode;
use miden_objects::asset::AssetVault;
use miden_objects::utils::Deserializable;

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;
let account_id_proto =
miden_node_proto::generated::account::AccountId { id: id_bytes.to_vec() };

// Pass dummy commitments to force server to return code and vault
// (server returns data only if commitment differs from what we send)
let dummy_commitment: miden_node_proto::generated::primitives::Digest = Word::default().into();

let request = 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: Some(dummy_commitment),
asset_vault_commitment: Some(dummy_commitment),
storage_maps: vec![],
}),
};

// If the RPC call fails, return None
if resp.is_err() {
return Ok(None);
}
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(account_details) = resp.expect("Previously checked for error").into_inner().details
else {
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_objects::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(account_id, 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.
fn build_account_storage(
account_id: AccountId,
storage_details: miden_node_proto::generated::rpc::AccountStorageDetails,
) -> Result<miden_objects::account::AccountStorage> {
use miden_objects::account::{AccountStorage, StorageSlot};

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

// Build a map of slot_name -> map entries from the response
let map_entries: std::collections::HashMap<String, Vec<(Word, Word)>> = storage_details
.map_details
.into_iter()
.filter_map(|map_detail| {
let entries = map_detail.entries?.entries;
let converted: Vec<(Word, Word)> = entries
.into_iter()
.filter_map(|e| {
let key: Word = e.key?.try_into().ok()?;
let value: Word = e.value?.try_into().ok()?;
Some((key, value))
})
.collect();
Some((map_detail.slot_name, converted))
})
.collect();

let mut slots = Vec::new();
for slot in storage_header.slots {
let slot_name = miden_objects::account::StorageSlotName::new(slot.slot_name.clone())
.context("invalid slot name")?;
let commitment: Word = slot
.commitment
.context("missing slot commitment")?
.try_into()
.context("invalid slot commitment")?;

// slot_type: 0 = Value, 1 = Map
let storage_slot = if slot.slot_type == 0 {
StorageSlot::with_value(slot_name, commitment)
} else {
let entries = map_entries.get(&slot.slot_name).cloned().unwrap_or_default();
if entries.is_empty() {
warn!(
account.id = %account_id,
slot_name = %slot.slot_name,
"storage map slot has no entries (not requested or empty)"
);
}
let storage_map = miden_objects::account::StorageMap::with_entries(entries)
.context("failed to create storage map")?;
StorageSlot::with_map(slot_name, storage_map)
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar to the above comment: I think we can probably just error out if there are any storage maps in the account.

};
slots.push(storage_slot);
}

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

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