montana/Монтана-Протокол/Код/crates/mt-examples/examples/m3_account.rs

817 lines
30 KiB
Rust
Raw Permalink Normal View History

// M3 — Account operations shakedown.
// Subcommands:
// genesis-state build_genesis_state(params): bootstrap acc + node + chain_length=1
// open-account TransferActivation: sponsor → new account
// transfer-scenario sponsor → A → B → Transfer A→B 50% balance
// change-key ChangeKey: rotate pk; old pk → InvalidSignature на следующей op
// anchor Anchor: chain payload hashes
// emission-schedule [W1 W2 ...] reward + supply ledger
// validation-rejects 6 OpError cases байт-в-байт REJECTED
// all Прогнать все 7
//
// Exit code 0=PASS, 1=FAIL.
use std::env;
use std::process::ExitCode;
use mt_account::{
apply, build_genesis_state, genesis_state_root, op_hash, reward_moneta, supply_moneta,
validate_anchor, validate_change_key, validate_transfer, validate_transfer_activation, Anchor,
ChangeKey, GenesisState, OpError, Operation, Transfer, TransferActivation,
};
use mt_crypto::{keypair_from_seed, sign, PublicKey, SecretKey, KEYPAIR_SEED_SIZE};
use mt_examples::{hex_full, print_kv, print_note, print_section, print_subsection};
use mt_genesis::genesis_params;
use mt_state::{derive_account_id, AccountId, AccountRecord, AccountTable};
const SUITE_ID: u16 = 0x0001;
// ---- helpers ----
// Deterministic keypair из seed-byte (повторяемо между прогонами).
fn kp(seed_byte: u8) -> (PublicKey, SecretKey) {
let mut seed = [0u8; KEYPAIR_SEED_SIZE];
seed[0] = seed_byte;
seed[31] = seed_byte ^ 0xAA; // entropy
keypair_from_seed(&seed).expect("keypair_from_seed")
}
// Краткий fingerprint pubkey (8 байт) для отображения.
fn pk8(pk: &PublicKey) -> String {
hex_full(&pk.as_bytes()[..8])
}
// Создать sponsor account с явным балансом — synthetic setup без build_genesis_state.
fn synthetic_sponsor(state: &mut AccountTable, pk: &PublicKey, balance: u128) -> AccountId {
let account_id = derive_account_id(SUITE_ID, pk.as_bytes());
let frontier = mt_crypto::hash(mt_codec::domain::GENESIS, &[&account_id]);
state.insert(AccountRecord {
account_id,
balance,
suite_id: SUITE_ID,
is_node_operator: false,
frontier_hash: frontier,
op_height: 0,
account_chain_length: 0,
account_chain_length_snapshot: 0,
current_pubkey: *pk.as_bytes(),
creation_window: 0,
last_op_window: 0,
last_activation_window: 0,
});
account_id
}
// ---- builders для операций ----
fn mk_transfer_activation(
sponsor_id: AccountId,
sponsor_sk: &SecretKey,
receiver_pk: &PublicKey,
amount: u128,
prev_hash: mt_crypto::Hash32,
) -> TransferActivation {
let receiver_id = derive_account_id(SUITE_ID, receiver_pk.as_bytes());
let mut op = TransferActivation {
prev_hash,
sender: sponsor_id,
receiver: receiver_id,
suite_id: SUITE_ID,
receiver_pubkey: receiver_pk.clone(),
amount,
signature: mt_crypto::Signature::from_array([0u8; mt_crypto::SIGNATURE_SIZE]),
};
let mut scope = Vec::new();
op.encode_signed_scope(&mut scope);
op.signature = sign(sponsor_sk, &scope).expect("sign TransferActivation");
op
}
fn mk_transfer(
sender_id: AccountId,
sender_sk: &SecretKey,
link_id: AccountId,
amount: u128,
prev_hash: mt_crypto::Hash32,
) -> Transfer {
let mut op = Transfer {
prev_hash,
sender: sender_id,
link: link_id,
amount,
signature: mt_crypto::Signature::from_array([0u8; mt_crypto::SIGNATURE_SIZE]),
};
let mut scope = Vec::new();
op.encode_signed_scope(&mut scope);
op.signature = sign(sender_sk, &scope).expect("sign Transfer");
op
}
fn mk_change_key(
sender_id: AccountId,
old_sk: &SecretKey,
new_pk: &PublicKey,
prev_hash: mt_crypto::Hash32,
) -> ChangeKey {
let mut op = ChangeKey {
prev_hash,
sender: sender_id,
new_suite_id: SUITE_ID,
new_pubkey: new_pk.clone(),
signature: mt_crypto::Signature::from_array([0u8; mt_crypto::SIGNATURE_SIZE]),
};
let mut scope = Vec::new();
op.encode_signed_scope(&mut scope);
op.signature = sign(old_sk, &scope).expect("sign ChangeKey");
op
}
fn mk_anchor(
sender_id: AccountId,
sk: &SecretKey,
app_id: [u8; 32],
data_hash: [u8; 32],
prev_hash: mt_crypto::Hash32,
) -> Anchor {
let mut op = Anchor {
prev_hash,
sender: sender_id,
app_id,
data_hash,
signature: mt_crypto::Signature::from_array([0u8; mt_crypto::SIGNATURE_SIZE]),
};
let mut scope = Vec::new();
op.encode_signed_scope(&mut scope);
op.signature = sign(sk, &scope).expect("sign Anchor");
op
}
// ---- subcommands ----
fn cmd_genesis_state() -> bool {
print_section("GENESIS STATE — build_genesis_state(params) sanity");
print_note(
"spec, раздел \"Genesis Decree\": bootstrap acc + bootstrap node creates initial state. node.chain_length = 1 (invariant ≥ 1).",
);
let params = genesis_params();
let GenesisState {
account_table,
node_table,
candidate_pool,
} = build_genesis_state(params);
print_subsection("Tables population");
let acc_count = account_table.len();
let node_count = node_table.len();
let cand_count = candidate_pool.len();
print_kv("AccountTable rows", format!("{acc_count}"));
print_kv("NodeTable rows", format!("{node_count}"));
print_kv("CandidatePool rows", format!("{cand_count}"));
print_subsection("Bootstrap account");
let bootstrap_acct_id = derive_account_id(
mt_account::GENESIS_SUITE_ID,
&params.bootstrap_account_pubkey,
);
let acct = account_table
.get(&bootstrap_acct_id)
.expect("bootstrap account in table");
print_kv("account_id", hex_full(&acct.account_id));
print_kv("balance", format!("{}", acct.balance));
print_kv("is_node_operator", format!("{}", acct.is_node_operator));
print_kv(
"account_chain_length",
format!("{}", acct.account_chain_length),
);
print_subsection("Bootstrap node");
let node_id_expected = mt_state::derive_node_id(&params.bootstrap_node_pubkey);
let node = node_table.get(&node_id_expected).expect("bootstrap node");
print_kv("node_id", hex_full(&node.node_id));
print_kv("chain_length", format!("{}", node.chain_length));
print_kv("operator_account_id", hex_full(&node.operator_account_id));
print_subsection("genesis_state_root determinism");
let genesis_state = build_genesis_state(params);
let r1 = genesis_state_root(&genesis_state);
let r2 = genesis_state_root(&build_genesis_state(params));
print_kv("root run #1", hex_full(&r1));
print_kv("root run #2", hex_full(&r2));
let det = r1 == r2;
print_kv("byte-exact determinism", format!("{det}"));
let pass = acc_count == 1
&& node_count == 1
&& cand_count == 0
&& acct.is_node_operator
&& node.chain_length == 1
&& node.operator_account_id == acct.account_id
&& det;
println!(
"\n[result] GENESIS-STATE: {}",
if pass { "PASS" } else { "FAIL" }
);
pass
}
fn cmd_open_account() -> bool {
print_section("OPEN ACCOUNT — TransferActivation от sponsor создаёт receiver");
print_note(
"spec, разделы \"TransferActivation\" + \"Account Chain (Block Lattice)\": sender balance -= amount, receiver = новый AccountRecord с balance = amount, frontier_hash receiver = 0x00 ... (receiver chain genesis), sender frontier_hash = op_hash.",
);
let mut state = AccountTable::new();
let (sponsor_pk, sponsor_sk) = kp(0x01);
let (alice_pk, _alice_sk) = kp(0x02);
let sponsor_id = synthetic_sponsor(&mut state, &sponsor_pk, 1_000_000);
let sponsor_frontier = state.get(&sponsor_id).unwrap().frontier_hash;
print_subsection("Pre-state");
print_kv("sponsor balance", "1_000_000");
print_kv("alice balance", "");
print_kv("sponsor frontier", hex_full(&sponsor_frontier));
let op = mk_transfer_activation(
sponsor_id,
&sponsor_sk,
&alice_pk,
100_000,
sponsor_frontier,
);
let opc = Operation::TransferActivation(op.clone());
print_subsection("Operation");
print_kv("amount", format!("{}", op.amount));
print_kv("receiver_pk(8B)", pk8(&alice_pk));
let h = op_hash(&opc);
print_kv("op_hash", hex_full(&h));
let h2 = op_hash(&opc);
print_kv("op_hash recompute", hex_full(&h2));
print_kv(
"op_hash deterministic (R2: identifier(op) = SHA-256(\"mt-op\" || signed_scope))",
format!("{}", h == h2),
);
print_subsection("Validate");
let r = validate_transfer_activation(&op, &state, /*W*/ 1000, /*τ₂*/ 20160);
print_kv("validate", format!("{r:?}"));
if r.is_err() {
println!("\n[result] OPEN-ACCOUNT: FAIL");
return false;
}
apply(&opc, &mut state, /*W*/ 1000);
print_subsection("Post-state");
let sponsor_after = state.get(&sponsor_id).unwrap();
let alice_id = derive_account_id(SUITE_ID, alice_pk.as_bytes());
let alice_after = state.get(&alice_id).expect("alice created");
print_kv("sponsor balance", format!("{}", sponsor_after.balance));
print_kv("alice balance", format!("{}", alice_after.balance));
print_kv(
"sponsor account_chain_length",
format!("{}", sponsor_after.account_chain_length),
);
print_kv(
"alice account_chain_length",
format!("{}", alice_after.account_chain_length),
);
print_kv(
"sponsor frontier (new)",
hex_full(&sponsor_after.frontier_hash),
);
print_kv(
"alice frontier (genesis)",
hex_full(&alice_after.frontier_hash),
);
let pass = sponsor_after.balance == 900_000
&& alice_after.balance == 100_000
&& sponsor_after.account_chain_length == 1
&& alice_after.account_chain_length == 0
&& alice_after.last_activation_window == 0
&& sponsor_after.last_activation_window == 1000;
println!(
"\n[result] OPEN-ACCOUNT: {}",
if pass { "PASS" } else { "FAIL" }
);
pass
}
fn cmd_transfer_scenario() -> bool {
print_section("TRANSFER SCENARIO — sponsor → A → B → Transfer A→B 50%");
print_note(
"Полная цепочка: 2× TransferActivation (создаём A, B), затем Transfer A→B. Σ delta_balance = 0 после каждого шага. spec, разделы \"TransferActivation\" + \"Transfer\".",
);
let mut state = AccountTable::new();
let (sponsor_pk, sponsor_sk) = kp(0x10);
let (alice_pk, alice_sk) = kp(0x11);
let (bob_pk, _bob_sk) = kp(0x12);
let sponsor_id = synthetic_sponsor(&mut state, &sponsor_pk, 1_000_000);
// Шаг 1: sponsor → alice (создание).
print_subsection("Step 1: sponsor → alice (TransferActivation 200_000), W=100");
let f1 = state.get(&sponsor_id).unwrap().frontier_hash;
let op1 = mk_transfer_activation(sponsor_id, &sponsor_sk, &alice_pk, 200_000, f1);
validate_transfer_activation(&op1, &state, 100, 20160).expect("step 1 validate");
apply(&Operation::TransferActivation(op1.clone()), &mut state, 100);
let alice_id = derive_account_id(SUITE_ID, alice_pk.as_bytes());
print_kv(
"sponsor balance",
format!("{}", state.get(&sponsor_id).unwrap().balance),
);
print_kv(
"alice balance",
format!("{}", state.get(&alice_id).unwrap().balance),
);
// Шаг 2: sponsor → bob (создание). Cooldown τ₂ — поэтому W=100+20160=20260.
print_subsection(
"Step 2: sponsor → bob (TransferActivation 100_000), W=20260 (после cooldown τ₂)",
);
let f2 = state.get(&sponsor_id).unwrap().frontier_hash;
let op2 = mk_transfer_activation(sponsor_id, &sponsor_sk, &bob_pk, 100_000, f2);
validate_transfer_activation(&op2, &state, 20260, 20160).expect("step 2 validate");
apply(
&Operation::TransferActivation(op2.clone()),
&mut state,
20260,
);
let bob_id = derive_account_id(SUITE_ID, bob_pk.as_bytes());
print_kv(
"sponsor balance",
format!("{}", state.get(&sponsor_id).unwrap().balance),
);
print_kv(
"bob balance",
format!("{}", state.get(&bob_id).unwrap().balance),
);
// Шаг 3: Alice → Bob, 50% balance Alice = 100_000.
print_subsection("Step 3: Alice → Bob (Transfer 100_000)");
let alice_frontier = state.get(&alice_id).unwrap().frontier_hash;
let alice_balance_before = state.get(&alice_id).unwrap().balance;
let bob_balance_before = state.get(&bob_id).unwrap().balance;
let op3 = mk_transfer(alice_id, &alice_sk, bob_id, 100_000, alice_frontier);
let validate_r = validate_transfer(&op3, &state);
print_kv("validate Transfer", format!("{validate_r:?}"));
apply(&Operation::Transfer(op3.clone()), &mut state, 20300);
let alice_after = state.get(&alice_id).unwrap();
let bob_after = state.get(&bob_id).unwrap();
print_kv("alice balance", format!("{}", alice_after.balance));
print_kv("bob balance", format!("{}", bob_after.balance));
print_kv(
"alice account_chain_length",
format!("{}", alice_after.account_chain_length),
);
print_kv("alice frontier_hash", hex_full(&alice_after.frontier_hash));
print_subsection("Conservation: Σ delta_balance == 0 для Transfer step");
let alice_delta: i128 = alice_after.balance as i128 - alice_balance_before as i128;
let bob_delta: i128 = bob_after.balance as i128 - bob_balance_before as i128;
let total_delta = alice_delta + bob_delta;
print_kv("alice delta", format!("{alice_delta}"));
print_kv("bob delta", format!("{bob_delta}"));
print_kv("total delta (Σ)", format!("{total_delta}"));
let pass = validate_r.is_ok()
&& alice_after.balance == 100_000
&& bob_after.balance == 200_000
&& alice_after.account_chain_length == 1
&& bob_after.account_chain_length == 0
&& alice_delta == -100_000
&& bob_delta == 100_000
&& total_delta == 0;
println!(
"\n[result] TRANSFER-SCENARIO: {}",
if pass { "PASS" } else { "FAIL" }
);
pass
}
fn cmd_change_key() -> bool {
print_section("CHANGE KEY — rotate pubkey, signature transition");
print_note(
"spec, раздел \"ChangeKey\": op подписан СТАРЫМ ключом (current_pubkey в state до apply). После apply: state.current_pubkey = new_pubkey. Следующая op подписывается новым ключом.",
);
let mut state = AccountTable::new();
let (sponsor_pk, sponsor_sk_old) = kp(0x20);
let (new_pk, sponsor_sk_new) = kp(0x21);
let acc_id = synthetic_sponsor(&mut state, &sponsor_pk, 500_000);
let f0 = state.get(&acc_id).unwrap().frontier_hash;
print_subsection("Pre-state");
print_kv("current_pubkey (old, 8B)", pk8(&sponsor_pk));
print_subsection("ChangeKey signed by OLD sk");
let op_change = mk_change_key(acc_id, &sponsor_sk_old, &new_pk, f0);
let r1 = validate_change_key(&op_change, &state);
print_kv("validate (old sk → ok)", format!("{r1:?}"));
apply(&Operation::ChangeKey(op_change.clone()), &mut state, 500);
let after_change = state.get(&acc_id).unwrap();
print_kv(
"current_pubkey now (8B)",
pk8(&PublicKey::from_array(after_change.current_pubkey)),
);
let pk_rotated = after_change.current_pubkey == *new_pk.as_bytes();
print_subsection("Anchor подписан НОВЫМ sk → должен валидироваться");
let f1 = after_change.frontier_hash;
let op_anchor_new = mk_anchor(acc_id, &sponsor_sk_new, [0xAB; 32], [0xCD; 32], f1);
let r2 = validate_anchor(&op_anchor_new, &state);
print_kv("validate (new sk)", format!("{r2:?}"));
print_subsection("Anchor подписан СТАРЫМ sk → должен быть отвергнут");
let op_anchor_old = mk_anchor(acc_id, &sponsor_sk_old, [0xAB; 32], [0xCD; 32], f1);
let r3 = validate_anchor(&op_anchor_old, &state);
print_kv("validate (old sk)", format!("{r3:?}"));
let old_rejected = matches!(r3, Err(OpError::InvalidSignature));
let pass = r1.is_ok() && pk_rotated && r2.is_ok() && old_rejected;
println!(
"\n[result] CHANGE-KEY: {}",
if pass { "PASS" } else { "FAIL" }
);
pass
}
fn cmd_anchor() -> bool {
print_section("ANCHOR — chain payload hashes");
print_note(
"spec, раздел \"Anchor\": user opcode фиксирует hash off-chain контента в chain. Multiple anchors образуют chain через frontier_hash линковку.",
);
let mut state = AccountTable::new();
let (pk, sk) = kp(0x30);
let acc_id = synthetic_sponsor(&mut state, &pk, 100_000);
let mut prev = state.get(&acc_id).unwrap().frontier_hash;
let mut applied_count = 0u32;
for i in 1u8..=3 {
print_subsection(&format!("Anchor #{i}"));
let op = mk_anchor(acc_id, &sk, [i; 32], [i ^ 0xAA; 32], prev);
let r = validate_anchor(&op, &state);
print_kv("validate", format!("{r:?}"));
if r.is_err() {
break;
}
apply(
&Operation::Anchor(op.clone()),
&mut state,
u64::from(i) * 10,
);
let after = state.get(&acc_id).unwrap();
print_kv("frontier_hash", hex_full(&after.frontier_hash));
print_kv(
"account_chain_length",
format!("{}", after.account_chain_length),
);
prev = after.frontier_hash;
applied_count += 1;
}
let final_state = state.get(&acc_id).unwrap();
let pass = applied_count == 3
&& final_state.account_chain_length == 3
&& final_state.balance == 100_000;
println!("\n[result] ANCHOR: {}", if pass { "PASS" } else { "FAIL" });
pass
}
fn cmd_emission_schedule(windows: &[u64]) -> bool {
print_section("EMISSION SCHEDULE — reward + supply ledger");
print_note(
"spec, раздел \"Эмиссия\": reward_moneta = EMISSION_moneta = 13 Ɉ const per window. supply_moneta(W) = (W+1) × EMISSION_moneta.",
);
let params = genesis_params();
let reward = reward_moneta(params);
print_kv("EMISSION_moneta (per window)", format!("{reward}"));
let mut all_match = true;
print_subsection("Schedule");
for &w in windows {
let supply = supply_moneta(w, params);
let expected = u128::from(w + 1) * reward;
let ok = supply == expected;
print_kv(
&format!("W = {w:8}"),
format!(
"supply = {supply:20} nɈ (expected {expected}) {}",
if ok { "" } else { "" }
),
);
if !ok {
all_match = false;
}
}
print_subsection("Linearity sanity");
let s_a = supply_moneta(1000, params);
let s_b = supply_moneta(2000, params);
let diff = s_b - s_a;
let expected_diff = u128::from(1000u64) * reward;
print_kv("supply(2000) - supply(1000)", format!("{diff}"));
print_kv("expected (1000 × reward)", format!("{expected_diff}"));
let lin_ok = diff == expected_diff;
let pass = all_match && lin_ok && reward > 0;
println!(
"\n[result] EMISSION-SCHEDULE: {}",
if pass { "PASS" } else { "FAIL" }
);
pass
}
fn cmd_validation_rejects() -> bool {
print_section("VALIDATION REJECTS — 10 adversarial cases");
print_note(
"Десять OpError cases должны быть REJECTED detection правилами validate_*. spec, разделы \"Account Chain (Block Lattice) → Верификация\" + \"Anti-spam\".",
);
let mut state = AccountTable::new();
let (alice_pk, alice_sk) = kp(0x40);
let (bob_pk, bob_sk) = kp(0x41);
let (charlie_pk, _charlie_sk) = kp(0x42);
let (dave_pk, _dave_sk) = kp(0x43);
let alice_id = synthetic_sponsor(&mut state, &alice_pk, 500_000);
let bob_id = synthetic_sponsor(&mut state, &bob_pk, 100_000);
let alice_frontier = state.get(&alice_id).unwrap().frontier_hash;
let mut all_correctly_rejected = true;
fn report(label: &str, expected: &str, actual: &Result<(), OpError>, matches: bool) {
print_kv("validate", format!("{actual:?}"));
print_kv("ожидание", format!("{expected} — правило: {label}"));
print_kv(
"verdict",
if matches {
"REJECTED ✓"
} else {
"ACCEPTED ✗"
},
);
}
// 1. InsufficientBalance.
print_subsection("1. InsufficientBalance — Transfer amount > sender balance");
let op = mk_transfer(alice_id, &alice_sk, bob_id, 1_000_000, alice_frontier);
let r = validate_transfer(&op, &state);
let m = matches!(r, Err(OpError::InsufficientBalance));
report(
"amount=1_000_000 > balance=500_000",
"InsufficientBalance",
&r,
m,
);
if !m {
all_correctly_rejected = false;
}
// 2. SelfTransfer.
print_subsection("2. SelfTransfer — sender == link");
let op = mk_transfer(alice_id, &alice_sk, alice_id, 100, alice_frontier);
let r = validate_transfer(&op, &state);
let m = matches!(r, Err(OpError::SelfTransfer));
report("Alice → Alice", "SelfTransfer", &r, m);
if !m {
all_correctly_rejected = false;
}
// 3. ZeroAmount.
print_subsection("3. ZeroAmount — Transfer amount = 0");
let op = mk_transfer(alice_id, &alice_sk, bob_id, 0, alice_frontier);
let r = validate_transfer(&op, &state);
let m = matches!(r, Err(OpError::ZeroAmount));
report("amount=0", "ZeroAmount", &r, m);
if !m {
all_correctly_rejected = false;
}
// 4. InvalidPrevHash.
print_subsection("4. InvalidPrevHash — prev_hash != sender.frontier_hash");
let op = mk_transfer(alice_id, &alice_sk, bob_id, 100, [0xFF; 32]);
let r = validate_transfer(&op, &state);
let m = matches!(r, Err(OpError::InvalidPrevHash));
report("stale prev_hash 0xFF×32", "InvalidPrevHash", &r, m);
if !m {
all_correctly_rejected = false;
}
// 5. InvalidSignature.
print_subsection("5. InvalidSignature — подпись чужим sk");
let op = mk_transfer(alice_id, &bob_sk, bob_id, 100, alice_frontier);
let r = validate_transfer(&op, &state);
let m = matches!(r, Err(OpError::InvalidSignature));
report("Alice's op подписан Bob's sk", "InvalidSignature", &r, m);
if !m {
all_correctly_rejected = false;
}
// 6. ReceiverAlreadyExists.
print_subsection("6. ReceiverAlreadyExists — TransferActivation для уже-existing receiver");
let op = mk_transfer_activation(alice_id, &alice_sk, &bob_pk, 100, alice_frontier);
let r = validate_transfer_activation(&op, &state, 5000, 20160);
let m = matches!(r, Err(OpError::ReceiverAlreadyExists));
report("Bob уже в AccountTable", "ReceiverAlreadyExists", &r, m);
if !m {
all_correctly_rejected = false;
}
// 7. ActivationCooldownNotElapsed.
print_subsection("7. ActivationCooldownNotElapsed — второй TransferActivation до τ₂");
let mut state_cd = state.clone();
let f0 = state_cd.get(&alice_id).unwrap().frontier_hash;
let op_first = mk_transfer_activation(alice_id, &alice_sk, &charlie_pk, 100, f0);
apply(&Operation::TransferActivation(op_first), &mut state_cd, 100);
let f1 = state_cd.get(&alice_id).unwrap().frontier_hash;
let op_second = mk_transfer_activation(alice_id, &alice_sk, &dave_pk, 100, f1);
let r = validate_transfer_activation(&op_second, &state_cd, 200, 20160);
let m = matches!(r, Err(OpError::ActivationCooldownNotElapsed));
report(
"apply at W=100, validate at W=200 < 100+τ₂",
"ActivationCooldownNotElapsed",
&r,
m,
);
if !m {
all_correctly_rejected = false;
}
// 8. UnsupportedSuite.
print_subsection("8. UnsupportedSuite — TransferActivation с suite_id = 0xFFFF");
let receiver_id_bad = derive_account_id(0xFFFF, dave_pk.as_bytes());
let mut op = TransferActivation {
prev_hash: alice_frontier,
sender: alice_id,
receiver: receiver_id_bad,
suite_id: 0xFFFF,
receiver_pubkey: dave_pk.clone(),
amount: 100,
signature: mt_crypto::Signature::from_array([0u8; mt_crypto::SIGNATURE_SIZE]),
};
let mut scope = Vec::new();
op.encode_signed_scope(&mut scope);
op.signature = sign(&alice_sk, &scope).expect("sign 0xFFFF suite");
let r = validate_transfer_activation(&op, &state, 5000, 20160);
let m = matches!(r, Err(OpError::UnsupportedSuite));
report(
"suite_id = 0xFFFF не зарегистрирован",
"UnsupportedSuite",
&r,
m,
);
if !m {
all_correctly_rejected = false;
}
// 9. AccountNotFound.
print_subsection("9. AccountNotFound — Transfer от account_id не в AccountTable");
let phantom_id: AccountId = [0xDE; 32];
let op = mk_transfer(phantom_id, &alice_sk, bob_id, 100, alice_frontier);
let r = validate_transfer(&op, &state);
let m = matches!(r, Err(OpError::AccountNotFound));
report("phantom sender 0xDE×32", "AccountNotFound", &r, m);
if !m {
all_correctly_rejected = false;
}
// 10. InvalidBinding.
print_subsection("10. InvalidBinding — receiver_id не derived из (suite_id, receiver_pubkey)");
let mut op = TransferActivation {
prev_hash: alice_frontier,
sender: alice_id,
receiver: [0x00; 32],
suite_id: SUITE_ID,
receiver_pubkey: dave_pk.clone(),
amount: 100,
signature: mt_crypto::Signature::from_array([0u8; mt_crypto::SIGNATURE_SIZE]),
};
let mut scope = Vec::new();
op.encode_signed_scope(&mut scope);
op.signature = sign(&alice_sk, &scope).expect("sign bad binding");
let r = validate_transfer_activation(&op, &state, 5000, 20160);
let m = matches!(r, Err(OpError::InvalidBinding));
report(
"receiver = 0x00×32 ≠ derive_account_id(suite, pk)",
"InvalidBinding",
&r,
m,
);
if !m {
all_correctly_rejected = false;
}
// Sanity positive.
print_subsection("Sanity positive — legitimate TransferActivation Alice → Charlie");
let op = mk_transfer_activation(alice_id, &alice_sk, &charlie_pk, 100, alice_frontier);
let r = validate_transfer_activation(&op, &state, 5000, 20160);
print_kv("validate", format!("{r:?}"));
let positive_ok = r.is_ok();
print_kv(
"verdict",
if positive_ok {
"ACCEPTED ✓"
} else {
"REJECTED ✗"
},
);
let pass = all_correctly_rejected && positive_ok;
println!(
"\n[result] VALIDATION-REJECTS: {}",
if pass { "PASS" } else { "FAIL" }
);
pass
}
fn cmd_all() -> bool {
print_section("M3 ACCOUNT — FULL SHAKEDOWN");
let a = cmd_genesis_state();
let b = cmd_open_account();
let c = cmd_transfer_scenario();
let d = cmd_change_key();
let e = cmd_anchor();
let f = cmd_emission_schedule(&[0, 100, 1000, 20160, 1_000_000]);
let g = cmd_validation_rejects();
print_section("SUMMARY");
print_kv("genesis-state", if a { "PASS" } else { "FAIL" });
print_kv("open-account", if b { "PASS" } else { "FAIL" });
print_kv("transfer-scenario", if c { "PASS" } else { "FAIL" });
print_kv("change-key", if d { "PASS" } else { "FAIL" });
print_kv("anchor", if e { "PASS" } else { "FAIL" });
print_kv("emission-schedule", if f { "PASS" } else { "FAIL" });
print_kv("validation-rejects", if g { "PASS" } else { "FAIL" });
let pass = a && b && c && d && e && f && g;
println!(
"\n[result] ALL SCENARIOS: {}",
if pass { "PASS" } else { "FAIL" }
);
pass
}
fn bool_to_exit(pass: bool) -> ExitCode {
if pass {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
}
}
fn usage() {
eprintln!("M3 — Account operations shakedown");
eprintln!();
eprintln!("usage: m3_account <subcommand> [args]");
eprintln!();
eprintln!(" genesis-state build_genesis_state(params): bootstrap acc + node");
eprintln!(" open-account TransferActivation: sponsor → new account");
eprintln!(" transfer-scenario sponsor → A → B → Transfer A→B");
eprintln!(" change-key ChangeKey: pk rotation + signature transition");
eprintln!(" anchor Anchor: chain payload hashes");
eprintln!(" emission-schedule [W ...] reward + supply ledger");
eprintln!(" validation-rejects 6 OpError adversarial cases");
eprintln!(" all Прогнать все 7");
eprintln!();
eprintln!("Exit code 0=PASS, 1=FAIL.");
}
fn main() -> ExitCode {
let args: Vec<String> = env::args().collect();
let sub = match args.get(1) {
Some(s) => s.as_str(),
None => {
usage();
return ExitCode::FAILURE;
},
};
let pass = match sub {
"genesis-state" => cmd_genesis_state(),
"open-account" => cmd_open_account(),
"transfer-scenario" => cmd_transfer_scenario(),
"change-key" => cmd_change_key(),
"anchor" => cmd_anchor(),
"emission-schedule" => {
let ws: Vec<u64> = args[2..].iter().filter_map(|s| s.parse().ok()).collect();
let default = vec![0u64, 100, 1000, 20160, 1_000_000];
let use_args: &[u64] = if ws.is_empty() { &default } else { &ws };
cmd_emission_schedule(use_args)
},
"validation-rejects" => cmd_validation_rejects(),
"all" => cmd_all(),
_ => {
usage();
return ExitCode::FAILURE;
},
};
bool_to_exit(pass)
}