use std::collections::HashSet;
use miden_node_proto::domain::account::{
AccountRequest,
AccountResponse,
AccountStorageRequest,
SlotData,
};
use miden_node_proto::generated as proto;
use miden_node_store::GetAccountError;
use miden_node_utils::limiter::{QueryParamStorageMapKeyTotalLimit, QueryParamStorageMapSlotLimit};
use miden_node_utils::tracing::{miden_instrument, miden_span_record};
use tonic::Status;
use tracing::{debug, info_span};
use super::{RpcService, check};
use crate::{COMPONENT, LOG_TARGET};
#[tonic::async_trait]
impl proto::server::rpc_api::GetAccount for RpcService {
type Input = AccountRequest;
type Output = AccountResponse;
fn decode(request: proto::rpc::AccountRequest) -> tonic::Result<Self::Input> {
AccountRequest::try_from(request).map_err(Into::into)
}
fn encode(output: Self::Output) -> tonic::Result<proto::rpc::AccountResponse> {
Ok(output.into())
}
#[miden_instrument(
target = COMPONENT,
name = "get_account",
skip_all,
err,
)]
async fn handle(&self, request: Self::Input) -> tonic::Result<Self::Output> {
miden_span_record!(
account.id = %request.account_id,
);
if let Some(block) = request.block_num {
miden_span_record!(
block.number = %block,
);
}
tracing::trace!(target: LOG_TARGET, ?request);
debug!(target: LOG_TARGET, "Getting account");
if let Some(details) = &request.details {
let _span = info_span!(target: COMPONENT, "validate_storage_map_keys").entered();
validate_storage_request(&details.storage_request)?;
}
let account_data =
self.store.get_account(request).await.map_err(get_account_error_to_status)?;
Ok(account_data)
}
}
fn validate_storage_request(storage_request: &AccountStorageRequest) -> Result<(), Status> {
let AccountStorageRequest::Explicit(requests) = storage_request else {
return Ok(());
};
check::<QueryParamStorageMapSlotLimit>(requests.len())?;
let mut seen = HashSet::with_capacity(requests.len());
for request in requests {
if !seen.insert(&request.slot_name) {
return Err(Status::invalid_argument(format!(
"duplicate storage map slot in request: {}",
request.slot_name
)));
}
}
let total_keys: usize = requests
.iter()
.filter_map(|request| match &request.slot_data {
SlotData::All => None,
SlotData::MapKeys(items) => Some(items.len()),
})
.sum();
check::<QueryParamStorageMapKeyTotalLimit>(total_keys)?;
Ok(())
}
fn get_account_error_to_status(err: GetAccountError) -> Status {
let message = err.to_string();
match err {
GetAccountError::DatabaseError(err) => super::database_error_to_status(&err),
GetAccountError::DeserializationFailed(_)
| GetAccountError::AccountNotFound(..)
| GetAccountError::AccountNotPublic(_)
| GetAccountError::UnknownBlock(_)
| GetAccountError::BlockPruned(_) => Status::invalid_argument(message),
}
}
#[cfg(test)]
mod tests {
use miden_node_proto::domain::account::StorageMapRequest;
use miden_node_utils::limiter::QueryParamLimiter;
use miden_protocol::account::{StorageMapKey, StorageSlotName};
use tonic::Code;
use super::*;
fn slot_request(name: &str, slot_data: SlotData) -> StorageMapRequest {
StorageMapRequest {
slot_name: StorageSlotName::new(name).unwrap(),
slot_data,
}
}
#[test]
fn none_and_all_storage_maps_requests_are_always_valid() {
validate_storage_request(&AccountStorageRequest::None).unwrap();
validate_storage_request(&AccountStorageRequest::AllStorageMaps).unwrap();
}
#[test]
fn explicit_request_within_limits_is_valid() {
let requests = vec![
slot_request("a::0", SlotData::All),
slot_request("a::1", SlotData::MapKeys(vec![StorageMapKey::from_index(1)])),
];
validate_storage_request(&AccountStorageRequest::Explicit(requests)).unwrap();
}
#[test]
fn too_many_all_entries_slots_are_rejected() {
let requests = (0..=QueryParamStorageMapSlotLimit::LIMIT)
.map(|index| slot_request(&format!("a::{index}"), SlotData::All))
.collect();
let status = validate_storage_request(&AccountStorageRequest::Explicit(requests))
.expect_err("request exceeding the slot limit must be rejected");
assert_eq!(status.code(), Code::OutOfRange);
}
#[test]
fn duplicate_slots_are_rejected() {
let requests =
vec![slot_request("a::0", SlotData::All), slot_request("a::0", SlotData::All)];
let status = validate_storage_request(&AccountStorageRequest::Explicit(requests))
.expect_err("duplicate slot must be rejected");
assert_eq!(status.code(), Code::InvalidArgument);
}
#[test]
fn too_many_map_keys_are_rejected() {
let keys: Vec<_> = (0..=QueryParamStorageMapKeyTotalLimit::LIMIT)
.map(|index| StorageMapKey::from_index(index as u32))
.collect();
let requests = vec![slot_request("a::0", SlotData::MapKeys(keys))];
let status = validate_storage_request(&AccountStorageRequest::Explicit(requests))
.expect_err("request exceeding the key limit must be rejected");
assert_eq!(status.code(), Code::OutOfRange);
}
}