// Automated determinism invariants для mt-account. // M3 audit prep — operation processing layer; любое non-determinism // = consensus fork. Invariants ловят regression если refactor случайно // ломает byte-exact apply semantics, op_hash stability, либо state // transition determinism. use mt_account::{ apply, apply_anchor, apply_change_key, apply_proposal, apply_transfer, build_genesis_state, op_hash, reward_moneta, settle_window, supply_moneta, validate_transfer, Anchor, ChangeKey, Operation, ProposalSettle, Transfer, TransferActivation, ANCHOR_SIZE, CHANGE_KEY_SIZE, GENESIS_SUITE_ID, TRANSFER_ACTIVATION_SIZE, TRANSFER_SIZE, TYPE_ANCHOR, TYPE_CHANGE_KEY, TYPE_TRANSFER, TYPE_TRANSFER_ACTIVATION, }; use mt_codec::CanonicalEncode; use mt_crypto::{PublicKey, Signature, PUBLIC_KEY_SIZE, SIGNATURE_SIZE}; use mt_state::{derive_account_id, derive_node_id, AccountRecord, AccountTable}; // ---------- Helpers ---------- fn sample_account(id_byte: u8, balance: u128) -> AccountRecord { AccountRecord { account_id: [id_byte; 32], balance, suite_id: 1, is_node_operator: false, frontier_hash: [0xBB; 32], op_height: 0, account_chain_length: 0, account_chain_length_snapshot: 0, current_pubkey: [0xCC; PUBLIC_KEY_SIZE], creation_window: 0, last_op_window: 0, last_activation_window: 0, } } fn sample_transfer(sender: [u8; 32], link: [u8; 32], amount: u128) -> Transfer { Transfer { prev_hash: [0xBB; 32], sender, link, amount, signature: Signature::from_array([0u8; SIGNATURE_SIZE]), } } fn sample_anchor(sender: [u8; 32]) -> Anchor { Anchor { prev_hash: [0xBB; 32], sender, app_id: [0x11; 32], data_hash: [0x22; 32], signature: Signature::from_array([0u8; SIGNATURE_SIZE]), } } fn sample_change_key(sender: [u8; 32]) -> ChangeKey { ChangeKey { prev_hash: [0xBB; 32], sender, new_suite_id: 1, new_pubkey: PublicKey::from_array([0xDD; PUBLIC_KEY_SIZE]), signature: Signature::from_array([0u8; SIGNATURE_SIZE]), } } // ---------- Encoded sizes match constants ([I-9] determinism) ---------- #[test] fn transfer_encoded_size_constant() { let mut buf = Vec::new(); sample_transfer([0x01; 32], [0x02; 32], 100).encode(&mut buf); assert_eq!( buf.len(), TRANSFER_SIZE, "Transfer encoded size drift: expected {}, got {}", TRANSFER_SIZE, buf.len() ); } #[test] fn change_key_encoded_size_constant() { let mut buf = Vec::new(); sample_change_key([0x01; 32]).encode(&mut buf); assert_eq!(buf.len(), CHANGE_KEY_SIZE); } #[test] fn anchor_encoded_size_constant() { let mut buf = Vec::new(); sample_anchor([0x01; 32]).encode(&mut buf); assert_eq!(buf.len(), ANCHOR_SIZE); } #[test] fn transfer_activation_encoded_size_constant() { let op = TransferActivation { prev_hash: [0xBB; 32], sender: [0x01; 32], receiver: [0x02; 32], suite_id: 1, receiver_pubkey: PublicKey::from_array([0xCC; PUBLIC_KEY_SIZE]), amount: 100, signature: Signature::from_array([0u8; SIGNATURE_SIZE]), }; let mut buf = Vec::new(); op.encode(&mut buf); assert_eq!(buf.len(), TRANSFER_ACTIVATION_SIZE); } // ---------- Type byte stability (consensus type codes) ---------- #[test] fn type_codes_stable() { // SSOT [I-10]: type codes immutable. Изменение = consensus fork. assert_eq!(TYPE_TRANSFER, 0x02); assert_eq!(TYPE_CHANGE_KEY, 0x03); assert_eq!(TYPE_ANCHOR, 0x04); assert_eq!(TYPE_TRANSFER_ACTIVATION, 0x0A); } // ---------- op_hash determinism (R2 invariant) ---------- #[test] fn op_hash_deterministic_transfer() { let op = Operation::Transfer(sample_transfer([0x01; 32], [0x02; 32], 100)); assert_eq!(op_hash(&op), op_hash(&op)); } #[test] fn op_hash_stable_under_signature_mutation() { // R2: identifier(op) = SHA-256("mt-op" || signed_scope(op)) — signature // НЕ входит в hash. Защита от schemes where signature randomized. let t1 = sample_transfer([0x01; 32], [0x02; 32], 100); let mut t2 = t1.clone(); t2.signature = Signature::from_array([0xFF; SIGNATURE_SIZE]); let op1 = Operation::Transfer(t1); let op2 = Operation::Transfer(t2); assert_eq!(op_hash(&op1), op_hash(&op2)); } #[test] fn op_hash_changes_on_field_mutation() { let base = Operation::Transfer(sample_transfer([0x01; 32], [0x02; 32], 100)); let mutated_amount = Operation::Transfer(sample_transfer([0x01; 32], [0x02; 32], 101)); let mutated_sender = Operation::Transfer(sample_transfer([0xAA; 32], [0x02; 32], 100)); let mutated_link = Operation::Transfer(sample_transfer([0x01; 32], [0xBB; 32], 100)); assert_ne!(op_hash(&base), op_hash(&mutated_amount)); assert_ne!(op_hash(&base), op_hash(&mutated_sender)); assert_ne!(op_hash(&base), op_hash(&mutated_link)); } #[test] fn op_hash_distinct_across_op_types() { // Type byte должен различать operations даже если поля совпадают. let t = Operation::Transfer(sample_transfer([0x01; 32], [0x02; 32], 100)); let a = Operation::Anchor(sample_anchor([0x01; 32])); let c = Operation::ChangeKey(sample_change_key([0x01; 32])); assert_ne!(op_hash(&t), op_hash(&a)); assert_ne!(op_hash(&t), op_hash(&c)); assert_ne!(op_hash(&a), op_hash(&c)); } // ---------- apply_* determinism (state transition) ---------- #[test] fn apply_transfer_deterministic() { let mut state1 = AccountTable::new(); state1.insert(sample_account(0xAA, 1_000_000)); state1.insert(sample_account(0xBB, 0)); let mut state2 = state1.clone(); // Set frontier_hash to match what validate_transfer expects (sample's prev_hash) let mut s_acct = state1.get(&[0xAA; 32]).unwrap().clone(); s_acct.frontier_hash = [0xBB; 32]; // matches sample_transfer.prev_hash state1.insert(s_acct.clone()); state2.insert(s_acct); let op = sample_transfer([0xAA; 32], [0xBB; 32], 1000); apply_transfer(&op, &mut state1, 5); apply_transfer(&op, &mut state2, 5); assert_eq!(state1.root(), state2.root()); } #[test] fn apply_anchor_does_not_change_balance() { let mut state = AccountTable::new(); let mut acct = sample_account(0xAA, 1_000_000); acct.frontier_hash = [0xBB; 32]; state.insert(acct); let op = sample_anchor([0xAA; 32]); let balance_before = state.get(&[0xAA; 32]).unwrap().balance; apply_anchor(&op, &mut state, 5); let balance_after = state.get(&[0xAA; 32]).unwrap().balance; assert_eq!(balance_before, balance_after, "Anchor не меняет balance"); } #[test] fn apply_change_key_updates_pubkey() { let mut state = AccountTable::new(); let mut acct = sample_account(0xAA, 1_000_000); acct.frontier_hash = [0xBB; 32]; state.insert(acct); let op = sample_change_key([0xAA; 32]); apply_change_key(&op, &mut state, 5); let updated = state.get(&[0xAA; 32]).unwrap(); assert_eq!(updated.current_pubkey, [0xDD; PUBLIC_KEY_SIZE]); } // ---------- Validation determinism ---------- #[test] fn validate_rejects_self_transfer() { let mut state = AccountTable::new(); let mut acct = sample_account(0xAA, 1_000_000); acct.frontier_hash = [0xBB; 32]; state.insert(acct); // Insert receiver state.insert(sample_account(0xCC, 0)); let op = sample_transfer([0xAA; 32], [0xAA; 32], 100); assert!(validate_transfer(&op, &state).is_err()); } #[test] fn validate_rejects_zero_amount() { let mut state = AccountTable::new(); let mut acct = sample_account(0xAA, 1_000_000); acct.frontier_hash = [0xBB; 32]; state.insert(acct); state.insert(sample_account(0xBB, 0)); let op = sample_transfer([0xAA; 32], [0xBB; 32], 0); assert!(validate_transfer(&op, &state).is_err()); } #[test] fn validate_rejects_insufficient_balance() { let mut state = AccountTable::new(); let mut acct = sample_account(0xAA, 100); acct.frontier_hash = [0xBB; 32]; state.insert(acct); state.insert(sample_account(0xBB, 0)); let op = sample_transfer([0xAA; 32], [0xBB; 32], 1000); // более чем balance assert!(validate_transfer(&op, &state).is_err()); } // ---------- settle_window determinism (op_hash sort order) ---------- #[test] fn settle_window_op_order_independent() { // settle_window sorts by op_hash — порядок входа не влияет на итог. let mut state1 = AccountTable::new(); let mut acct_aa = sample_account(0xAA, 1_000_000); acct_aa.frontier_hash = [0xBB; 32]; state1.insert(acct_aa.clone()); state1.insert(sample_account(0xCC, 0)); state1.insert(sample_account(0xDD, 0)); let mut state2 = state1.clone(); let op_a = Operation::Transfer(sample_transfer([0xAA; 32], [0xCC; 32], 100)); let op_b = Operation::Transfer(sample_transfer([0xAA; 32], [0xDD; 32], 200)); // Note: после первого apply, frontier меняется → второй op fails validate // (prev_hash mismatch). Но settle_window не вызывает validate; смотрим // только что order-independence сохраняется на уровне sort+apply // (skipping validation gates за scope теста). settle_window(&mut state1, &[op_a.clone(), op_b.clone()], 5); settle_window(&mut state2, &[op_b, op_a], 5); // После sort'ов by op_hash — оба state равны. assert_eq!(state1.root(), state2.root()); } // ---------- Genesis state determinism ---------- #[test] fn build_genesis_state_deterministic() { let p = mt_genesis::genesis_params(); let g1 = build_genesis_state(p); let g2 = build_genesis_state(p); assert_eq!(g1.account_table.root(), g2.account_table.root()); assert_eq!(g1.node_table.root(), g2.node_table.root()); assert_eq!(g1.candidate_pool.root(), g2.candidate_pool.root()); } #[test] fn genesis_supply_zero() { // spec: Genesis State до первого окна — supply = 0. let p = mt_genesis::genesis_params(); let g = build_genesis_state(p); let total: u128 = g.account_table.iter().map(|r| r.balance).sum(); assert_eq!(total, 0); } #[test] fn genesis_node_chain_length_is_one() { // spec invariant: chain_length ≥ 1 для любого узла, включая genesis bootstrap. let p = mt_genesis::genesis_params(); let g = build_genesis_state(p); let node_id = derive_node_id(&p.bootstrap_node_pubkey); let node = g.node_table.get(&node_id).expect("genesis node"); assert_eq!(node.chain_length, 1); } // ---------- reward_moneta + supply_moneta consistency ---------- #[test] fn reward_moneta_returns_emission_const() { let p = mt_genesis::genesis_params(); assert_eq!(reward_moneta(p), p.emission_moneta); } #[test] fn supply_moneta_window_zero_is_emission() { let p = mt_genesis::genesis_params(); assert_eq!(supply_moneta(0, p), p.emission_moneta); } #[test] fn supply_moneta_grows_linearly() { let p = mt_genesis::genesis_params(); let s0 = supply_moneta(0, p); let s10 = supply_moneta(10, p); let s100 = supply_moneta(100, p); assert!(s0 < s10); assert!(s10 < s100); assert_eq!(s10, p.emission_moneta * 11); assert_eq!(s100, p.emission_moneta * 101); } // ---------- apply_proposal determinism ---------- #[test] fn apply_proposal_deterministic() { let p = mt_genesis::genesis_params(); let g1 = build_genesis_state(p); let g2 = build_genesis_state(p); let mut a1 = g1.account_table; let mut n1 = g1.node_table; let c1 = g1.candidate_pool; let mut a2 = g2.account_table; let mut n2 = g2.node_table; let c2 = g2.candidate_pool; let node_id = derive_node_id(&p.bootstrap_node_pubkey); let input = ProposalSettle { window_w: 5, winner_id: node_id, cemented_confirmers: vec![], }; let r1 = apply_proposal(&mut a1, &mut n1, &c1, &input, p); let r2 = apply_proposal(&mut a2, &mut n2, &c2, &input, p); assert_eq!(r1, r2); } #[test] fn apply_proposal_emission_changes_balance() { let p = mt_genesis::genesis_params(); let g = build_genesis_state(p); let mut account_table = g.account_table; let mut node_table = g.node_table; let candidate_pool = g.candidate_pool; let account_id = derive_account_id(GENESIS_SUITE_ID, &p.bootstrap_account_pubkey); let node_id = derive_node_id(&p.bootstrap_node_pubkey); let balance_before = account_table.get(&account_id).unwrap().balance; let input = ProposalSettle { window_w: 3, winner_id: node_id, cemented_confirmers: vec![], }; apply_proposal( &mut account_table, &mut node_table, &candidate_pool, &input, p, ); let balance_after = account_table.get(&account_id).unwrap().balance; assert_eq!( balance_after, balance_before + p.emission_moneta, "Operator получает EMISSION_moneta" ); } // ---------- Static API invariants ---------- #[test] fn record_sizes_positive() { const _: () = assert!(TRANSFER_SIZE > 0); const _: () = assert!(CHANGE_KEY_SIZE > 0); const _: () = assert!(ANCHOR_SIZE > 0); const _: () = assert!(TRANSFER_ACTIVATION_SIZE > 0); } #[test] fn genesis_suite_id_is_one() { assert_eq!(GENESIS_SUITE_ID, 1); } // ---------- M3-3 closure: checked arithmetic panic on protocol breach ---------- #[test] #[should_panic(expected = "balance underflow")] fn apply_transfer_panics_on_unsanitized_underflow() { // Если кто-то вызывает apply_transfer без предварительного validate_transfer // и balance < amount — должен быть controlled halt, не silent wrap. let mut state = AccountTable::new(); let mut acct = sample_account(0xAA, 100); // balance = 100 acct.frontier_hash = [0xBB; 32]; state.insert(acct); state.insert(sample_account(0xBB, 0)); let op = sample_transfer([0xAA; 32], [0xBB; 32], 1000); // amount = 1000 > balance apply_transfer(&op, &mut state, 5); } // ---------- M3-1 closure: window_w u64 unification ---------- #[test] fn apply_accepts_u64_window() { // Compile-time проверка signature — все apply_* принимают u64. let mut state = AccountTable::new(); let mut acct = sample_account(0xAA, 1_000_000); acct.frontier_hash = [0xBB; 32]; state.insert(acct); state.insert(sample_account(0xBB, 0)); let op_t = Operation::Transfer(sample_transfer([0xAA; 32], [0xBB; 32], 100)); let large_window: u64 = 1_000_000_000; // > u32::MAX/4 but < u32::MAX apply(&op_t, &mut state, large_window); } #[test] #[should_panic(expected = "encoded arithmetic horizon")] fn apply_panics_on_window_w_above_u32_max() { // window_w > u32::MAX → controlled halt (encoded arithmetic horizon). let mut state = AccountTable::new(); let mut acct = sample_account(0xAA, 1_000_000); acct.frontier_hash = [0xBB; 32]; state.insert(acct); state.insert(sample_account(0xBB, 0)); let op = sample_transfer([0xAA; 32], [0xBB; 32], 100); apply_transfer(&op, &mut state, u64::from(u32::MAX) + 1); }