montana/Монтана-Протокол/Код/crates/mt-account/tests/determinism_invariants.rs

458 lines
15 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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);
}