miden-node-proto 0.17.0-rc.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, conversion_error_to_status};
use miden_node_proto::{DecodeMessage, 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 account_request() -> proto::rpc::AccountRequest {
    let account_id = AccountId::dummy(
        [7; 15],
        AccountIdVersion::Version1,
        AccountType::Public,
        AssetCallbackFlag::Disabled,
    );
    proto::rpc::AccountRequest {
        account_id: Some(account_id.into()),
        block_num: None,
        details: None,
    }
}

#[test]
fn account_request_preserves_optional_fields_on_the_wire() {
    let request = account_request();
    let decoded = proto::rpc::AccountRequest::decode(request.encode_to_vec().as_slice())
        .unwrap()
        .decode_fields()
        .unwrap()
        .verify()
        .unwrap();
    assert!(decoded.block_num.is_none());
    assert!(decoded.details.is_none());

    let request = proto::rpc::AccountRequest {
        block_num: Some(miden_protocol::block::BlockNumber::GENESIS.into()),
        details: Some(proto::rpc::account_request::AccountDetailRequest {
            code_commitment: Some(Word::empty().into()),
            ..Default::default()
        }),
        ..account_request()
    };
    let decoded = proto::rpc::AccountRequest::decode(request.encode_to_vec().as_slice())
        .unwrap()
        .decode_fields()
        .unwrap()
        .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::rpc::AccountRequest::default().decode_fields().unwrap_err();
    assert!(error.to_string().starts_with("account_id:"), "{error}");
    let status = conversion_error_to_status(error);
    assert_eq!(status.code(), tonic::Code::InvalidArgument);
    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 = conversion_error_to_status(error);
    assert_eq!(status.code(), tonic::Code::InvalidArgument);
    assert!(status.message().starts_with("transaction: invalid object"));
    assert!(status.message().contains("underlying validation failure"));
}

#[test]
fn nested_map_keys_report_the_field_index_and_original_error() {
    use detail::storage_map_detail_request::{MapKeys, SlotData};
    use proto::rpc::account_request::account_detail_request as detail;

    let request = proto::rpc::AccountRequest {
        details: Some(proto::rpc::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()
        }),
        ..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 = 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::rpc::account_response::AccountDetails {
        header: Some(header.into()),
        storage_details: Some(proto::rpc::AccountStorageDetails {
            header: Some(storage.into()),
            map_details: Vec::new(),
        }),
        code: None,
        vault_details: Some(proto::rpc::AccountVaultDetails::default()),
    };
    let decoded = details.clone().decode_fields().unwrap().verify().unwrap();
    assert!(decoded.account_code.is_none());

    let error = proto::rpc::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::rpc::MaybeBlock::default().decode_fields().unwrap();
    assert!(block.block.as_ref().is_none());
    assert!(block.proof.as_ref().is_none());
    assert!(
        proto::rpc::MaybeNoteScript::default()
            .decode_fields()
            .unwrap()
            .script
            .as_ref()
            .is_none()
    );
}

#[test]
fn prover_requires_a_request_variant() {
    let error = proto::remote_prover::ProofRequest::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::rpc::RpcLimits {
        endpoints: HashMap::from([(
            "SyncNotes".to_string(),
            proto::rpc::EndpointLimits { parameters: parameters.clone() },
        )]),
    };
    let decoded = proto::rpc::RpcLimits::decode(message.encode_to_vec().as_slice())
        .unwrap()
        .decode_fields()
        .unwrap();
    assert_eq!(decoded, HashMap::from([("SyncNotes".to_string(), parameters)]));
}