miden-node-proto 0.17.1

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

use miden_protocol::Word;
use miden_protocol::note::Nullifier;

use crate::DecodeMessageExt;
use crate::generated::miden::node::v1::{self as rpc, SyncNullifiersResponse};

impl SyncNullifiersResponse {
    fn fixture(chain_tip: u32, checked: u32, spent: &[(u32, u32)]) -> Self {
        Self {
            pagination_info: Some(rpc::PaginationInfo { chain_tip, block_num: checked }),
            nullifiers: spent
                .iter()
                .map(|&(id, block_num)| rpc::sync_nullifiers_response::NullifierUpdate {
                    nullifier: Some(Word::from([id, 0, 0, 0]).into()),
                    block_num,
                })
                .collect(),
        }
    }
}

#[test]
fn sync_nullifiers_preserves_spend_blocks_and_partial_page_boundary() {
    let response = SyncNullifiersResponse::fixture(20, 12, &[(1, 12), (2, 4)])
        .decode_and_verify()
        .unwrap();

    assert_eq!(response.pagination_info.chain_tip.as_u32(), 20);
    assert_eq!(response.pagination_info.block_num.as_u32(), 12);
    assert_eq!(
        response.nullifiers,
        HashMap::from([
            (Nullifier::from_raw(Word::from([1_u32, 0, 0, 0])), 12.into()),
            (Nullifier::from_raw(Word::from([2_u32, 0, 0, 0])), 4.into()),
        ])
    );
}

#[test]
fn sync_nullifiers_accepts_empty_pages() {
    for (chain_tip, checked) in [(0, 0), (20, 12), (20, 20)] {
        let response = SyncNullifiersResponse::fixture(chain_tip, checked, &[])
            .decode_and_verify()
            .unwrap();
        assert!(response.nullifiers.is_empty());
        assert_eq!(response.pagination_info.block_num.as_u32(), checked);
    }
}

#[test]
fn sync_nullifiers_rejects_pagination_beyond_chain_tip() {
    let error = SyncNullifiersResponse::fixture(20, 21, &[]).decode_and_verify().unwrap_err();
    assert!(error.to_string().contains("pagination_info.block_num"), "{error}");
}

#[test]
fn sync_nullifiers_rejects_spends_beyond_last_checked_block() {
    let error = SyncNullifiersResponse::fixture(20, 12, &[(1, 13)])
        .decode_and_verify()
        .unwrap_err();
    assert!(error.to_string().contains("nullifiers[0].block_num"), "{error}");
}

#[test]
fn sync_nullifiers_rejects_duplicate_spends() {
    for second_block in [4, 5] {
        let error = SyncNullifiersResponse::fixture(20, 12, &[(1, 4), (1, second_block)])
            .decode_and_verify()
            .unwrap_err();
        assert!(error.to_string().contains("duplicate nullifier"), "{error}");
        assert!(error.to_string().contains("nullifiers[1].nullifier"), "{error}");
    }
}

#[test]
fn sync_nullifiers_rejects_missing_fields() {
    let mut missing_pagination = SyncNullifiersResponse::fixture(20, 12, &[]);
    missing_pagination.pagination_info = None;
    let error = missing_pagination.decode_and_verify().unwrap_err();
    assert!(error.to_string().contains("pagination_info"), "{error}");

    let mut missing_nullifier = SyncNullifiersResponse::fixture(20, 12, &[(1, 4)]);
    missing_nullifier.nullifiers[0].nullifier = None;
    let error = missing_nullifier.decode_and_verify().unwrap_err();
    assert!(error.to_string().contains("nullifiers[0].nullifier"), "{error}");
}

#[test]
fn sync_nullifiers_rejects_malformed_nullifiers() {
    let mut response = SyncNullifiersResponse::fixture(20, 12, &[(1, 4)]);
    response.nullifiers[0].nullifier.as_mut().unwrap().encoded = vec![0xff; 32];
    let error = response.decode_and_verify().unwrap_err();
    assert!(error.to_string().contains("nullifiers[0].nullifier"), "{error}");
}