use diesel::prelude::*;
use miden_node_proto::domain::account::NetworkAccountId;
use miden_protocol::Word;
use miden_protocol::account::{
AccountComponentMetadata,
AccountId,
AccountStorageMode,
AccountType,
};
use miden_protocol::block::BlockNumber;
use miden_protocol::testing::account_id::{
ACCOUNT_ID_REGULAR_NETWORK_ACCOUNT_IMMUTABLE_CODE,
AccountIdBuilder,
};
use miden_protocol::transaction::TransactionId;
use miden_standards::note::{NetworkAccountTarget, NoteExecutionHint};
use miden_standards::testing::note::NoteBuilder;
use rand_chacha::ChaCha20Rng;
use rand_chacha::rand_core::SeedableRng;
use super::*;
use crate::db::models::conv as conversions;
use crate::db::{Db, schema};
fn test_conn() -> (SqliteConnection, tempfile::TempDir) {
Db::test_conn()
}
fn mock_network_account_id() -> NetworkAccountId {
let account_id: AccountId =
ACCOUNT_ID_REGULAR_NETWORK_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap();
NetworkAccountId::try_from(account_id).unwrap()
}
fn mock_network_account_id_seeded(seed: u8) -> NetworkAccountId {
let account_id = AccountIdBuilder::new()
.account_type(AccountType::RegularAccountImmutableCode)
.storage_mode(AccountStorageMode::Network)
.build_with_seed([seed; 32]);
NetworkAccountId::try_from(account_id).unwrap()
}
fn mock_tx_id(seed: u64) -> TransactionId {
let w = |n: u64| Word::try_from([n, 0, 0, 0]).unwrap();
TransactionId::new(w(seed), w(seed + 1), w(seed + 2), w(seed + 3))
}
fn mock_single_target_note(
network_account_id: NetworkAccountId,
seed: u8,
) -> AccountTargetNetworkNote {
let mut rng = ChaCha20Rng::from_seed([seed; 32]);
let sender = AccountIdBuilder::new()
.account_type(AccountType::RegularAccountImmutableCode)
.storage_mode(AccountStorageMode::Private)
.build_with_rng(&mut rng);
let target = NetworkAccountTarget::new(network_account_id.inner(), NoteExecutionHint::Always)
.expect("network account should be valid target");
let note = NoteBuilder::new(sender, rng).attachment(target).build().unwrap();
AccountTargetNetworkNote::new(note).expect("note should be single-target network note")
}
fn count_notes(conn: &mut SqliteConnection) -> i64 {
schema::notes::table.count().get_result(conn).unwrap()
}
fn count_accounts(conn: &mut SqliteConnection) -> i64 {
schema::accounts::table.count().get_result(conn).unwrap()
}
fn count_inflight_accounts(conn: &mut SqliteConnection) -> i64 {
schema::accounts::table
.filter(schema::accounts::transaction_id.is_not_null())
.count()
.get_result(conn)
.unwrap()
}
fn count_committed_accounts(conn: &mut SqliteConnection) -> i64 {
schema::accounts::table
.filter(schema::accounts::transaction_id.is_null())
.count()
.get_result(conn)
.unwrap()
}
#[test]
fn purge_inflight_clears_all_inflight_state() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let tx_id = mock_tx_id(1);
let note = mock_single_target_note(account_id, 10);
upsert_committed_account(conn, account_id, &mock_account(account_id)).unwrap();
add_transaction(conn, &tx_id, None, std::slice::from_ref(¬e), &[]).unwrap();
assert!(count_inflight_accounts(conn) == 0); assert_eq!(count_notes(conn), 1);
let tx_id2 = mock_tx_id(2);
add_transaction(conn, &tx_id2, None, &[], &[note.as_note().nullifier()]).unwrap();
let consumed_count: i64 = schema::notes::table
.filter(schema::notes::consumed_by.is_not_null())
.count()
.get_result(conn)
.unwrap();
assert_eq!(consumed_count, 1);
purge_inflight(conn).unwrap();
assert_eq!(count_inflight_accounts(conn), 0);
assert_eq!(count_committed_accounts(conn), 1);
assert_eq!(count_notes(conn), 0);
}
#[test]
fn transaction_added_inserts_notes_and_marks_consumed() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let tx_id = mock_tx_id(1);
let note1 = mock_single_target_note(account_id, 10);
let note2 = mock_single_target_note(account_id, 20);
insert_committed_notes(conn, std::slice::from_ref(¬e1)).unwrap();
assert_eq!(count_notes(conn), 1);
add_transaction(
conn,
&tx_id,
None,
std::slice::from_ref(¬e2),
&[note1.as_note().nullifier()],
)
.unwrap();
assert_eq!(count_notes(conn), 2);
let consumed: Option<Vec<u8>> = schema::notes::table
.find(conversions::nullifier_to_bytes(¬e1.as_note().nullifier()))
.select(schema::notes::consumed_by)
.first(conn)
.unwrap();
assert!(consumed.is_some());
let created: Option<Vec<u8>> = schema::notes::table
.find(conversions::nullifier_to_bytes(¬e2.as_note().nullifier()))
.select(schema::notes::created_by)
.first(conn)
.unwrap();
assert!(created.is_some());
}
#[test]
fn transaction_added_is_idempotent_for_notes() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let tx_id = mock_tx_id(1);
let note = mock_single_target_note(account_id, 10);
add_transaction(conn, &tx_id, None, std::slice::from_ref(¬e), &[]).unwrap();
add_transaction(conn, &tx_id, None, std::slice::from_ref(¬e), &[]).unwrap();
assert_eq!(count_notes(conn), 1);
}
#[test]
fn block_committed_promotes_inflight_notes_to_committed() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let tx_id = mock_tx_id(1);
let note = mock_single_target_note(account_id, 10);
let block_num = BlockNumber::from(1u32);
let header = mock_block_header(block_num);
add_transaction(conn, &tx_id, None, std::slice::from_ref(¬e), &[]).unwrap();
let created: Option<Vec<u8>> = schema::notes::table
.find(conversions::nullifier_to_bytes(¬e.as_note().nullifier()))
.select(schema::notes::created_by)
.first(conn)
.unwrap();
assert!(created.is_some());
commit_block(conn, &[tx_id], block_num, &header).unwrap();
let created: Option<Vec<u8>> = schema::notes::table
.find(conversions::nullifier_to_bytes(¬e.as_note().nullifier()))
.select(schema::notes::created_by)
.first(conn)
.unwrap();
assert!(created.is_none());
}
#[test]
fn block_committed_deletes_consumed_notes() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let note = mock_single_target_note(account_id, 10);
insert_committed_notes(conn, std::slice::from_ref(¬e)).unwrap();
assert_eq!(count_notes(conn), 1);
let tx_id = mock_tx_id(1);
add_transaction(conn, &tx_id, None, &[], &[note.as_note().nullifier()]).unwrap();
let block_num = BlockNumber::from(1u32);
let header = mock_block_header(block_num);
commit_block(conn, &[tx_id], block_num, &header).unwrap();
assert_eq!(count_notes(conn), 0);
}
#[test]
fn block_committed_promotes_inflight_account_to_committed() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let account = mock_account(account_id);
upsert_committed_account(conn, account_id, &account).unwrap();
assert_eq!(count_committed_accounts(conn), 1);
let tx_id = mock_tx_id(1);
let row = AccountInsert {
account_id: conversions::network_account_id_to_bytes(account_id),
transaction_id: Some(conversions::transaction_id_to_bytes(&tx_id)),
account_data: conversions::account_to_bytes(&account),
};
diesel::insert_into(schema::accounts::table).values(&row).execute(conn).unwrap();
assert_eq!(count_inflight_accounts(conn), 1);
assert_eq!(count_committed_accounts(conn), 1);
let block_num = BlockNumber::from(1u32);
let header = mock_block_header(block_num);
commit_block(conn, &[tx_id], block_num, &header).unwrap();
assert_eq!(count_committed_accounts(conn), 1);
assert_eq!(count_inflight_accounts(conn), 0);
}
#[test]
fn transactions_reverted_restores_consumed_notes() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let note = mock_single_target_note(account_id, 10);
insert_committed_notes(conn, std::slice::from_ref(¬e)).unwrap();
let tx_id = mock_tx_id(1);
add_transaction(conn, &tx_id, None, &[], &[note.as_note().nullifier()]).unwrap();
let consumed: Option<Vec<u8>> = schema::notes::table
.find(conversions::nullifier_to_bytes(¬e.as_note().nullifier()))
.select(schema::notes::consumed_by)
.first(conn)
.unwrap();
assert!(consumed.is_some());
let reverted = revert_transaction(conn, &[tx_id]).unwrap();
assert!(reverted.is_empty());
let consumed: Option<Vec<u8>> = schema::notes::table
.find(conversions::nullifier_to_bytes(¬e.as_note().nullifier()))
.select(schema::notes::consumed_by)
.first(conn)
.unwrap();
assert!(consumed.is_none());
}
#[test]
fn transactions_reverted_deletes_inflight_created_notes() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let tx_id = mock_tx_id(1);
let note = mock_single_target_note(account_id, 10);
add_transaction(conn, &tx_id, None, std::slice::from_ref(¬e), &[]).unwrap();
assert_eq!(count_notes(conn), 1);
revert_transaction(conn, &[tx_id]).unwrap();
assert_eq!(count_notes(conn), 0);
}
#[test]
fn transactions_reverted_reports_reverted_account_creations() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let account = mock_account(account_id);
let tx_id = mock_tx_id(1);
let row = AccountInsert {
account_id: conversions::network_account_id_to_bytes(account_id),
transaction_id: Some(conversions::transaction_id_to_bytes(&tx_id)),
account_data: conversions::account_to_bytes(&account),
};
diesel::insert_into(schema::accounts::table).values(&row).execute(conn).unwrap();
let reverted = revert_transaction(conn, &[tx_id]).unwrap();
assert_eq!(reverted.len(), 1);
assert_eq!(reverted[0], account_id);
assert_eq!(count_accounts(conn), 0);
}
#[test]
fn available_notes_filters_consumed_and_exceeded_attempts() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let note_good = mock_single_target_note(account_id, 10);
let note_consumed = mock_single_target_note(account_id, 20);
let note_failed = mock_single_target_note(account_id, 30);
insert_committed_notes(conn, &[note_good.clone(), note_consumed.clone(), note_failed.clone()])
.unwrap();
let tx_id = mock_tx_id(1);
add_transaction(conn, &tx_id, None, &[], &[note_consumed.as_note().nullifier()]).unwrap();
let block_num = BlockNumber::from(100u32);
notes_failed(conn, &[note_failed.as_note().nullifier()], block_num).unwrap();
notes_failed(conn, &[note_failed.as_note().nullifier()], block_num).unwrap();
notes_failed(conn, &[note_failed.as_note().nullifier()], block_num).unwrap();
let result = available_notes(conn, account_id, block_num, 3).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].nullifier(), note_good.as_note().nullifier());
}
#[test]
fn available_notes_only_returns_notes_for_specified_account() {
let (conn, _dir) = &mut test_conn();
let account_id_1 = mock_network_account_id();
let account_id_2 = mock_network_account_id_seeded(42);
let note_acct1 = mock_single_target_note(account_id_1, 10);
let note_acct2 = mock_single_target_note(account_id_2, 20);
insert_committed_notes(conn, &[note_acct1.clone(), note_acct2]).unwrap();
let block_num = BlockNumber::from(100u32);
let result = available_notes(conn, account_id_1, block_num, 30).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].nullifier(), note_acct1.as_note().nullifier());
}
#[test]
fn notes_failed_increments_attempt_count() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let note = mock_single_target_note(account_id, 10);
insert_committed_notes(conn, std::slice::from_ref(¬e)).unwrap();
let block_num = BlockNumber::from(5u32);
notes_failed(conn, &[note.as_note().nullifier()], block_num).unwrap();
notes_failed(conn, &[note.as_note().nullifier()], block_num).unwrap();
let (attempt_count, last_attempt): (i32, Option<i64>) = schema::notes::table
.find(conversions::nullifier_to_bytes(¬e.as_note().nullifier()))
.select((schema::notes::attempt_count, schema::notes::last_attempt))
.first(conn)
.unwrap();
assert_eq!(attempt_count, 2);
assert_eq!(last_attempt, Some(conversions::block_num_to_i64(block_num)));
}
#[test]
fn upsert_chain_state_updates_singleton() {
let (conn, _dir) = &mut test_conn();
let block_num_1 = BlockNumber::from(1u32);
let header_1 = mock_block_header(block_num_1);
upsert_chain_state(conn, block_num_1, &header_1).unwrap();
let block_num_2 = BlockNumber::from(2u32);
let header_2 = mock_block_header(block_num_2);
upsert_chain_state(conn, block_num_2, &header_2).unwrap();
let row_count: i64 = schema::chain_state::table.count().get_result(conn).unwrap();
assert_eq!(row_count, 1);
let stored_block_num: i64 = schema::chain_state::table
.select(schema::chain_state::block_num)
.first(conn)
.unwrap();
assert_eq!(stored_block_num, conversions::block_num_to_i64(block_num_2));
}
#[test]
fn note_script_insert_and_lookup() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let note: miden_protocol::note::Note = mock_single_target_note(account_id, 10).into_note();
let script = note.script().clone();
let root = script.root();
insert_note_script(conn, &root, &script).unwrap();
let found = lookup_note_script(conn, &root).unwrap();
assert!(found.is_some());
assert_eq!(found.unwrap().root(), script.root());
}
#[test]
fn note_script_lookup_returns_none_for_missing() {
let (conn, _dir) = &mut test_conn();
let missing_root = Word::default();
let found = lookup_note_script(conn, &missing_root).unwrap();
assert!(found.is_none());
}
#[test]
fn note_script_insert_is_idempotent() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let note: miden_protocol::note::Note = mock_single_target_note(account_id, 10).into_note();
let script = note.script().clone();
let root = script.root();
insert_note_script(conn, &root, &script).unwrap();
insert_note_script(conn, &root, &script).unwrap();
let found = lookup_note_script(conn, &root).unwrap();
assert!(found.is_some());
}
fn mock_account(_account_id: NetworkAccountId) -> miden_protocol::account::Account {
use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment};
use miden_protocol::account::{AccountBuilder, AccountComponent};
use miden_standards::account::auth::AuthSingleSig;
let component_code = miden_standards::code_builder::CodeBuilder::default()
.compile_component_code("test::interface", "pub proc test_proc push.1.2 add end")
.unwrap();
let component = AccountComponent::new(
component_code,
vec![],
AccountComponentMetadata::mock("test").with_supports_all_types(),
)
.unwrap();
AccountBuilder::new([0u8; 32])
.account_type(AccountType::RegularAccountImmutableCode)
.storage_mode(AccountStorageMode::Network)
.with_component(component)
.with_auth_component(AuthSingleSig::new(
PublicKeyCommitment::from(Word::default()),
AuthScheme::Falcon512Rpo,
))
.build_existing()
.unwrap()
}
fn mock_block_header(block_num: BlockNumber) -> miden_protocol::block::BlockHeader {
miden_protocol::block::BlockHeader::mock(block_num, None, None, &[], Word::default())
}