miden-node-proto 0.17.1

Miden node message definitions (Store, Block Producer and RPC)
Documentation
use std::collections::HashMap;
use std::error::Error;

use miden_node_proto::domain::account::AccountStorageRequest;
use miden_node_proto::errors::ConversionError;
use miden_node_proto::{DecodeMessage, DecodeMessageExt, Verify, generated as proto};
use miden_protocol::Word;
use miden_protocol::account::{AccountId, AccountIdVersion, AccountType, AssetCallbackFlag};
use miden_protocol::utils::serde::DeserializationError;
use prost::Message;

fn get_account_request() -> proto::miden::node::v1::GetAccountRequest {
    let account_id = AccountId::dummy(
        [7; 15],
        AccountIdVersion::Version1,
        AccountType::Public,
        AssetCallbackFlag::Disabled,
    );
    proto::miden::node::v1::GetAccountRequest {
        account_id: Some(account_id.into()),
        block_num: None,
        details: None,
    }
}

#[test]
fn account_request_preserves_optional_fields_on_the_wire() {
    let request = get_account_request();
    let decoded =
        proto::miden::node::v1::GetAccountRequest::decode(request.encode_to_vec().as_slice())
            .unwrap()
            .decode_and_verify()
            .unwrap();
    assert!(decoded.block_num.is_none());
    assert!(decoded.details.is_none());

    let request = proto::miden::node::v1::GetAccountRequest {
        block_num: Some(miden_protocol::block::BlockNumber::GENESIS.into()),
        details: Some(proto::miden::node::v1::get_account_request::AccountDetailRequest {
            code_commitment: Some(Word::empty().into()),
            ..Default::default()
        }),
        ..get_account_request()
    };
    let decoded =
        proto::miden::node::v1::GetAccountRequest::decode(request.encode_to_vec().as_slice())
            .unwrap()
            .decode_and_verify()
            .unwrap();
    assert_eq!(decoded.block_num.unwrap().as_u32(), 0);
    let details = decoded.details.unwrap();
    assert_eq!(details.code_commitment, Some(Word::empty()));
    assert!(details.asset_vault_commitment.is_none());
    assert_eq!(details.storage_request, AccountStorageRequest::None);
}

#[test]
fn missing_account_id_is_an_invalid_argument() {
    let error = proto::miden::node::v1::GetAccountRequest::default()
        .decode_and_verify()
        .unwrap_err();
    let status = error.into_status();
    assert_eq!(status.code(), tonic::Code::InvalidArgument);
    assert!(status.message().starts_with("failed to decode: account_id:"), "{status}");
    assert!(status.message().contains("missing"));
}

#[test]
fn conversion_status_preserves_field_context_and_nested_causes() {
    let error = ConversionError::with_source(
        "invalid object",
        std::io::Error::other("underlying validation failure"),
    )
    .context("transaction");

    let status = error.into_status();
    assert_eq!(status.code(), tonic::Code::InvalidArgument);
    assert!(status.message().starts_with("transaction: invalid object"));
    assert!(status.message().contains("underlying validation failure"));
    assert!(status.source().unwrap().is::<ConversionError>());
}

#[test]
fn nested_map_keys_report_the_field_index_and_original_error() {
    use detail::storage_map_detail_request::{MapKeys, SlotData};
    use proto::miden::node::v1::get_account_request::account_detail_request as detail;

    let request = proto::miden::node::v1::GetAccountRequest {
        details: Some(proto::miden::node::v1::get_account_request::AccountDetailRequest {
            storage_request: Some(detail::StorageRequest::StorageMaps(
                detail::StorageMapDetailRequests {
                    storage_maps: vec![detail::StorageMapDetailRequest {
                        slot_name: "miden::test::map".into(),
                        slot_data: Some(SlotData::MapKeys(MapKeys {
                            map_keys: vec![
                                Word::empty().into(),
                                proto::primitives::Word { encoded: vec![0xff; 32] },
                            ],
                        })),
                    }],
                },
            )),
            ..Default::default()
        }),
        ..get_account_request()
    };
    let error = request.decode_fields().unwrap_err();
    assert!(error.to_string().starts_with(
        "details.storage_request.storage_maps.storage_maps[0].slot_data.map_keys.map_keys[1].encoded:"
    ), "{error}");
    assert!(error.source().unwrap().is::<DeserializationError>());
}

#[test]
fn account_details_require_vault_data_but_allow_absent_code() {
    let account_id = get_account_request().decode_fields().unwrap().verify().unwrap().account_id;
    let storage = miden_protocol::account::AccountStorageHeader::new(Vec::new()).unwrap();
    let header = miden_protocol::account::AccountHeader::new(
        account_id,
        miden_protocol::Felt::ONE,
        Word::empty(),
        storage.to_commitment(),
        Word::empty(),
    );
    let details = proto::miden::node::v1::get_account_response::AccountDetails {
        header: Some(header.into()),
        storage_details: Some(proto::miden::node::v1::AccountStorageDetails {
            header: Some(storage.into()),
            map_details: Vec::new(),
        }),
        code: None,
        vault_details: Some(proto::miden::node::v1::AccountVaultDetails::default()),
    };
    let decoded = details.clone().decode_fields().unwrap().verify().unwrap();
    assert!(decoded.account_code.is_none());

    let error = proto::miden::node::v1::get_account_response::AccountDetails {
        vault_details: None,
        ..details
    }
    .decode_fields()
    .unwrap_err();
    assert!(error.to_string().starts_with("vault_details:"), "{error}");
}

#[test]
fn absent_blocks_and_scripts_remain_optional() {
    let block = proto::miden::node::v1::GetBlockByNumberResponse::default()
        .decode_fields()
        .unwrap();
    assert!(block.block.as_ref().is_none());
    assert!(block.proof.as_ref().is_none());
    assert!(
        proto::miden::node::v1::GetNoteScriptByRootResponse::default()
            .decode_fields()
            .unwrap()
            .script
            .as_ref()
            .is_none()
    );
}

#[test]
fn prover_requires_a_request_variant() {
    let error = proto::miden::remote_prover::v1::ProveRequest::default()
        .decode_fields()
        .unwrap_err();
    assert!(error.to_string().starts_with("request:"), "{error}");
}

#[test]
fn rpc_limits_preserve_endpoint_and_parameter_names() {
    let parameters = HashMap::from([("max_items".to_string(), 10), ("max_bytes".to_string(), 0)]);
    let message = proto::miden::node::v1::GetLimitsResponse {
        endpoints: HashMap::from([(
            "SyncNotes".to_string(),
            proto::miden::node::v1::EndpointLimits { parameters: parameters.clone() },
        )]),
    };
    let decoded =
        proto::miden::node::v1::GetLimitsResponse::decode(message.encode_to_vec().as_slice())
            .unwrap()
            .decode_fields()
            .unwrap();
    assert_eq!(decoded.endpoints.as_ref().len(), 1);
    assert_eq!(decoded.endpoints.as_ref()["SyncNotes"].parameters.as_ref(), &parameters);
}

#[test]
fn rpc_limits_preserve_empty_maps() {
    let decoded = proto::miden::node::v1::GetLimitsResponse::default().decode_fields().unwrap();
    assert!(decoded.endpoints.as_ref().is_empty());
    let message = proto::miden::node::v1::GetLimitsResponse {
        endpoints: HashMap::from([(
            "SyncNotes".to_string(),
            proto::miden::node::v1::EndpointLimits::default(),
        )]),
    };
    let decoded = message.decode_fields().unwrap();
    assert!(decoded.endpoints.as_ref()["SyncNotes"].parameters.as_ref().is_empty());
}