458 lines
15 KiB
Rust
458 lines
15 KiB
Rust
|
|
// 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);
|
|||
|
|
}
|