mirror of
https://github.com/logos-blockchain/lez-fuzzing.git
synced 2026-06-07 11:39:30 +00:00
fix: add assert_nonce_increment_correctness helper
This commit is contained in:
parent
720cce4efc
commit
b4997ba1af
@ -33,8 +33,10 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use arbitrary::{Arbitrary, Unstructured};
|
use arbitrary::{Arbitrary, Unstructured};
|
||||||
|
use common::transaction::NSSATransaction;
|
||||||
use fuzz_props::arbitrary_types::ArbNSSATransaction;
|
use fuzz_props::arbitrary_types::ArbNSSATransaction;
|
||||||
use fuzz_props::generators::arbitrary_fuzz_state;
|
use fuzz_props::generators::arbitrary_fuzz_state;
|
||||||
|
use fuzz_props::invariants::{NonceSnapshot, assert_nonce_increment_correctness};
|
||||||
use libfuzzer_sys::fuzz_target;
|
use libfuzzer_sys::fuzz_target;
|
||||||
use nssa::V03State;
|
use nssa::V03State;
|
||||||
|
|
||||||
@ -72,6 +74,31 @@ fuzz_target!(|data: &[u8]| {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Extract signer IDs and capture nonce snapshot before apply ────────────
|
||||||
|
// Signer IDs are private to ValidatedStateDiff; derive them from the transaction's
|
||||||
|
// witness set before the diff is consumed by apply_state_diff.
|
||||||
|
let signer_ids: Vec<nssa::AccountId> = match &tx {
|
||||||
|
NSSATransaction::Public(pub_tx) => pub_tx
|
||||||
|
.witness_set()
|
||||||
|
.signatures_and_public_keys()
|
||||||
|
.iter()
|
||||||
|
.map(|(_, pk)| nssa::AccountId::from(pk))
|
||||||
|
.collect(),
|
||||||
|
NSSATransaction::PrivacyPreserving(pp_tx) => pp_tx
|
||||||
|
.witness_set()
|
||||||
|
.signatures_and_public_keys()
|
||||||
|
.iter()
|
||||||
|
.map(|(_, pk)| nssa::AccountId::from(pk))
|
||||||
|
.collect(),
|
||||||
|
NSSATransaction::ProgramDeployment(_) => vec![],
|
||||||
|
};
|
||||||
|
let nonces_before = NonceSnapshot(
|
||||||
|
signer_ids
|
||||||
|
.iter()
|
||||||
|
.map(|&id| (id, state.get_account_by_id(id).nonce))
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
|
||||||
// Capture the IDs declared in the diff before consuming it in apply_state_diff.
|
// Capture the IDs declared in the diff before consuming it in apply_state_diff.
|
||||||
let diff_account_ids: Vec<nssa::AccountId> =
|
let diff_account_ids: Vec<nssa::AccountId> =
|
||||||
diff.public_diff().keys().copied().collect();
|
diff.public_diff().keys().copied().collect();
|
||||||
@ -80,6 +107,11 @@ fuzz_target!(|data: &[u8]| {
|
|||||||
let mut split_state = state.clone();
|
let mut split_state = state.clone();
|
||||||
split_state.apply_state_diff(diff);
|
split_state.apply_state_diff(diff);
|
||||||
|
|
||||||
|
// ── Standalone invariant: NonceIncrementCorrectness (split path) ──────────
|
||||||
|
// Asserts that every signer account's nonce was incremented by exactly one,
|
||||||
|
// catching bugs in the two-step apply_state_diff nonce-increment logic.
|
||||||
|
assert_nonce_increment_correctness(&signer_ids, &nonces_before, &split_state);
|
||||||
|
|
||||||
// ── Direct path: execute_check_on_state ───────────────────────────────────
|
// ── Direct path: execute_check_on_state ───────────────────────────────────
|
||||||
// This consumes `tx`; it must succeed because validate_on_state already did.
|
// This consumes `tx`; it must succeed because validate_on_state already did.
|
||||||
let mut exec_state = state.clone();
|
let mut exec_state = state.clone();
|
||||||
|
|||||||
@ -35,9 +35,11 @@
|
|||||||
//! the total; only mint/burn bugs or token-inflation bugs would break it.
|
//! the total; only mint/burn bugs or token-inflation bugs would break it.
|
||||||
|
|
||||||
use arbitrary::{Arbitrary, Unstructured};
|
use arbitrary::{Arbitrary, Unstructured};
|
||||||
|
use common::transaction::NSSATransaction;
|
||||||
use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction};
|
use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction};
|
||||||
use fuzz_props::invariants::{
|
use fuzz_props::invariants::{
|
||||||
BalanceSnapshot, InvariantCtx, NonceSnapshot, assert_invariants, assert_replay_rejection,
|
BalanceSnapshot, InvariantCtx, NonceSnapshot, assert_invariants, assert_nonce_increment_correctness,
|
||||||
|
assert_replay_rejection,
|
||||||
};
|
};
|
||||||
use libfuzzer_sys::fuzz_target;
|
use libfuzzer_sys::fuzz_target;
|
||||||
use nssa::V03State;
|
use nssa::V03State;
|
||||||
@ -111,13 +113,29 @@ fuzz_target!(|data: &[u8]| {
|
|||||||
state_after: &state,
|
state_after: &state,
|
||||||
execution_succeeded,
|
execution_succeeded,
|
||||||
balances_before,
|
balances_before,
|
||||||
nonces_before,
|
nonces_before: nonces_before.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── ReplayRejection (per-block) ───────────────────────────────────────
|
// ── NonceIncrementCorrectness + ReplayRejection (per-block) ──────────
|
||||||
// execute_check_on_state returns the NSSATransaction on Ok; replay it
|
// First verify every signer's nonce was incremented by exactly one, then
|
||||||
// immediately in the next block and assert it is rejected (nonce consumed).
|
// replay in the next block to confirm the nonce is permanently consumed.
|
||||||
if let Ok(applied_tx) = result {
|
if let Ok(applied_tx) = result {
|
||||||
|
let signer_ids: Vec<nssa::AccountId> = match &applied_tx {
|
||||||
|
NSSATransaction::Public(t) => t
|
||||||
|
.witness_set()
|
||||||
|
.signatures_and_public_keys()
|
||||||
|
.iter()
|
||||||
|
.map(|(_, pk)| nssa::AccountId::from(pk))
|
||||||
|
.collect(),
|
||||||
|
NSSATransaction::PrivacyPreserving(t) => t
|
||||||
|
.witness_set()
|
||||||
|
.signatures_and_public_keys()
|
||||||
|
.iter()
|
||||||
|
.map(|(_, pk)| nssa::AccountId::from(pk))
|
||||||
|
.collect(),
|
||||||
|
NSSATransaction::ProgramDeployment(_) => vec![],
|
||||||
|
};
|
||||||
|
assert_nonce_increment_correctness(&signer_ids, &nonces_before, &state);
|
||||||
assert_replay_rejection(applied_tx, &mut state, block_id + 1, timestamp + 1);
|
assert_replay_rejection(applied_tx, &mut state, block_id + 1, timestamp + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,9 +23,11 @@
|
|||||||
//! - **ReplayRejection** — accepted tx rejected on replay
|
//! - **ReplayRejection** — accepted tx rejected on replay
|
||||||
|
|
||||||
use arbitrary::{Arbitrary, Unstructured};
|
use arbitrary::{Arbitrary, Unstructured};
|
||||||
|
use common::transaction::NSSATransaction;
|
||||||
use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction};
|
use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction};
|
||||||
use fuzz_props::invariants::{
|
use fuzz_props::invariants::{
|
||||||
BalanceSnapshot, InvariantCtx, NonceSnapshot, assert_invariants, assert_replay_rejection,
|
BalanceSnapshot, InvariantCtx, NonceSnapshot, assert_invariants, assert_nonce_increment_correctness,
|
||||||
|
assert_replay_rejection,
|
||||||
};
|
};
|
||||||
use libfuzzer_sys::fuzz_target;
|
use libfuzzer_sys::fuzz_target;
|
||||||
use nssa::V03State;
|
use nssa::V03State;
|
||||||
@ -87,13 +89,29 @@ fuzz_target!(|data: &[u8]| {
|
|||||||
state_after: &state,
|
state_after: &state,
|
||||||
execution_succeeded,
|
execution_succeeded,
|
||||||
balances_before,
|
balances_before,
|
||||||
nonces_before,
|
nonces_before: nonces_before.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── ReplayRejection ───────────────────────────────────────────────────────
|
// ── NonceIncrementCorrectness + ReplayRejection ───────────────────────────
|
||||||
// tx is returned on success; assert that applying it again in the next block
|
// First verify every signer's nonce was incremented by exactly one, then
|
||||||
// is rejected (nonce was consumed on first acceptance).
|
// assert that replaying in the next block is rejected (nonce permanently consumed).
|
||||||
if let Ok(applied_tx) = result {
|
if let Ok(applied_tx) = result {
|
||||||
|
let signer_ids: Vec<nssa::AccountId> = match &applied_tx {
|
||||||
|
NSSATransaction::Public(t) => t
|
||||||
|
.witness_set()
|
||||||
|
.signatures_and_public_keys()
|
||||||
|
.iter()
|
||||||
|
.map(|(_, pk)| nssa::AccountId::from(pk))
|
||||||
|
.collect(),
|
||||||
|
NSSATransaction::PrivacyPreserving(t) => t
|
||||||
|
.witness_set()
|
||||||
|
.signatures_and_public_keys()
|
||||||
|
.iter()
|
||||||
|
.map(|(_, pk)| nssa::AccountId::from(pk))
|
||||||
|
.collect(),
|
||||||
|
NSSATransaction::ProgramDeployment(_) => vec![],
|
||||||
|
};
|
||||||
|
assert_nonce_increment_correctness(&signer_ids, &nonces_before, &state);
|
||||||
assert_replay_rejection(applied_tx, &mut state, 2, 1);
|
assert_replay_rejection(applied_tx, &mut state, 2, 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
#![no_main]
|
#![no_main]
|
||||||
|
|
||||||
use arbitrary::{Arbitrary, Unstructured};
|
use arbitrary::{Arbitrary, Unstructured};
|
||||||
|
use common::transaction::NSSATransaction;
|
||||||
use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction};
|
use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction};
|
||||||
use fuzz_props::invariants::{
|
use fuzz_props::invariants::{
|
||||||
BalanceSnapshot, InvariantCtx, NonceSnapshot, assert_invariants, assert_replay_rejection,
|
BalanceSnapshot, InvariantCtx, NonceSnapshot, assert_invariants, assert_nonce_increment_correctness,
|
||||||
|
assert_replay_rejection,
|
||||||
};
|
};
|
||||||
use libfuzzer_sys::fuzz_target;
|
use libfuzzer_sys::fuzz_target;
|
||||||
use nssa::V03State;
|
use nssa::V03State;
|
||||||
@ -85,13 +87,30 @@ fuzz_target!(|data: &[u8]| {
|
|||||||
state_after: &state,
|
state_after: &state,
|
||||||
execution_succeeded,
|
execution_succeeded,
|
||||||
balances_before,
|
balances_before,
|
||||||
nonces_before,
|
nonces_before: nonces_before.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── ReplayRejection ───────────────────────────────────────────────────
|
// ── NonceIncrementCorrectness + ReplayRejection ───────────────────────
|
||||||
// execute_check_on_state returns the NSSATransaction on Ok; replay it
|
// execute_check_on_state returns the NSSATransaction on Ok.
|
||||||
// immediately in the next block and assert it is rejected.
|
// First verify every signer's nonce was incremented by exactly one, then
|
||||||
|
// replay in the next block to confirm the nonce is permanently consumed.
|
||||||
if let Ok(applied_tx) = result {
|
if let Ok(applied_tx) = result {
|
||||||
|
let signer_ids: Vec<nssa::AccountId> = match &applied_tx {
|
||||||
|
NSSATransaction::Public(t) => t
|
||||||
|
.witness_set()
|
||||||
|
.signatures_and_public_keys()
|
||||||
|
.iter()
|
||||||
|
.map(|(_, pk)| nssa::AccountId::from(pk))
|
||||||
|
.collect(),
|
||||||
|
NSSATransaction::PrivacyPreserving(t) => t
|
||||||
|
.witness_set()
|
||||||
|
.signatures_and_public_keys()
|
||||||
|
.iter()
|
||||||
|
.map(|(_, pk)| nssa::AccountId::from(pk))
|
||||||
|
.collect(),
|
||||||
|
NSSATransaction::ProgramDeployment(_) => vec![],
|
||||||
|
};
|
||||||
|
assert_nonce_increment_correctness(&signer_ids, &nonces_before, &state);
|
||||||
assert_replay_rejection(applied_tx, &mut state, block_id + 1, timestamp + 1);
|
assert_replay_rejection(applied_tx, &mut state, block_id + 1, timestamp + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,8 +25,10 @@
|
|||||||
//! reachable by the fuzzer.
|
//! reachable by the fuzzer.
|
||||||
|
|
||||||
use arbitrary::{Arbitrary, Unstructured};
|
use arbitrary::{Arbitrary, Unstructured};
|
||||||
|
use common::transaction::NSSATransaction;
|
||||||
use fuzz_props::arbitrary_types::ArbNSSATransaction;
|
use fuzz_props::arbitrary_types::ArbNSSATransaction;
|
||||||
use fuzz_props::generators::arbitrary_fuzz_state;
|
use fuzz_props::generators::arbitrary_fuzz_state;
|
||||||
|
use fuzz_props::invariants::{NonceSnapshot, assert_nonce_increment_correctness};
|
||||||
use libfuzzer_sys::fuzz_target;
|
use libfuzzer_sys::fuzz_target;
|
||||||
use nssa::V03State;
|
use nssa::V03State;
|
||||||
|
|
||||||
@ -57,6 +59,15 @@ fuzz_target!(|data: &[u8]| {
|
|||||||
|
|
||||||
let state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0);
|
let state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0);
|
||||||
|
|
||||||
|
// Capture nonces of all known accounts before execution so that
|
||||||
|
// assert_nonce_increment_correctness can verify the +1 step on success.
|
||||||
|
let nonces_before = NonceSnapshot(
|
||||||
|
init_accs
|
||||||
|
.iter()
|
||||||
|
.map(|&(id, _)| (id, state.get_account_by_id(id).nonce))
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
|
||||||
// validate_on_state borrows `tx` and `state` — does NOT mutate state.
|
// validate_on_state borrows `tx` and `state` — does NOT mutate state.
|
||||||
let validate_result = tx.validate_on_state(&state, 1, 0);
|
let validate_result = tx.validate_on_state(&state, 1, 0);
|
||||||
|
|
||||||
@ -66,7 +77,7 @@ fuzz_target!(|data: &[u8]| {
|
|||||||
|
|
||||||
// INVARIANT 1: both must agree on success vs failure.
|
// INVARIANT 1: both must agree on success vs failure.
|
||||||
match (validate_result, execute_result) {
|
match (validate_result, execute_result) {
|
||||||
(Ok(diff), Ok(_)) => {
|
(Ok(diff), Ok(applied_tx)) => {
|
||||||
let public_diff = diff.public_diff();
|
let public_diff = diff.public_diff();
|
||||||
|
|
||||||
// INVARIANT 2a (forward): every account in the diff matches the post-execute state.
|
// INVARIANT 2a (forward): every account in the diff matches the post-execute state.
|
||||||
@ -143,6 +154,28 @@ fuzz_target!(|data: &[u8]| {
|
|||||||
"INVARIANT VIOLATION: total balance of known accounts changed after successful \
|
"INVARIANT VIOLATION: total balance of known accounts changed after successful \
|
||||||
transaction (possible double-credit or token-inflation bug)",
|
transaction (possible double-credit or token-inflation bug)",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// INVARIANT 4 (nonce increment correctness): every signer's nonce must
|
||||||
|
// have advanced by exactly one. This is orthogonal to the balance and
|
||||||
|
// consistency checks above: it catches bugs where validate_on_state and
|
||||||
|
// execute_check_on_state agree (passing INVARIANT 1) but both increment
|
||||||
|
// the wrong account's nonce, or skip the increment entirely.
|
||||||
|
let signer_ids: Vec<nssa::AccountId> = match &applied_tx {
|
||||||
|
NSSATransaction::Public(t) => t
|
||||||
|
.witness_set()
|
||||||
|
.signatures_and_public_keys()
|
||||||
|
.iter()
|
||||||
|
.map(|(_, pk)| nssa::AccountId::from(pk))
|
||||||
|
.collect(),
|
||||||
|
NSSATransaction::PrivacyPreserving(t) => t
|
||||||
|
.witness_set()
|
||||||
|
.signatures_and_public_keys()
|
||||||
|
.iter()
|
||||||
|
.map(|(_, pk)| nssa::AccountId::from(pk))
|
||||||
|
.collect(),
|
||||||
|
NSSATransaction::ProgramDeployment(_) => vec![],
|
||||||
|
};
|
||||||
|
assert_nonce_increment_correctness(&signer_ids, &nonces_before, &exec_state);
|
||||||
}
|
}
|
||||||
(Err(_), Err(_)) => {
|
(Err(_), Err(_)) => {
|
||||||
// Both failed — correct.
|
// Both failed — correct.
|
||||||
|
|||||||
@ -180,6 +180,33 @@ impl ProtocolInvariant for ReplayRejection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A successfully applied transaction must increment the nonce of every signer account
|
||||||
|
/// by exactly one.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This invariant **cannot** be exercised through [`InvariantCtx`] alone because
|
||||||
|
/// `InvariantCtx` does not carry a signer-ID list — that information is private to the
|
||||||
|
/// `nssa` crate and is consumed by `apply_state_diff` before it returns. The
|
||||||
|
/// `ProtocolInvariant` impl here is a registry placeholder only; it always returns `None`.
|
||||||
|
///
|
||||||
|
/// Use the standalone [`assert_nonce_increment_correctness`] function instead, passing
|
||||||
|
/// the signer IDs derived from the transaction's witness set, the [`NonceSnapshot`]
|
||||||
|
/// captured before execution, and the post-execution state.
|
||||||
|
pub struct NonceIncrementCorrectness;
|
||||||
|
|
||||||
|
impl ProtocolInvariant for NonceIncrementCorrectness {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"NonceIncrementCorrectness"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check(&self, _ctx: &InvariantCtx<'_>) -> Option<InvariantViolation> {
|
||||||
|
// NonceIncrementCorrectness requires explicit signer_ids not available in InvariantCtx.
|
||||||
|
// Use `assert_nonce_increment_correctness(signer_ids, nonces_before, state_after)` instead.
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Standalone helpers ────────────────────────────────────────────────────────
|
// ── Standalone helpers ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Assert that a successfully-applied transaction is **rejected** when replayed.
|
/// Assert that a successfully-applied transaction is **rejected** when replayed.
|
||||||
@ -220,6 +247,70 @@ pub fn assert_replay_rejection(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Assert that every signer account's nonce was incremented by exactly one after a
|
||||||
|
/// successfully applied transaction.
|
||||||
|
///
|
||||||
|
/// Call this immediately after `apply_state_diff` (or `execute_check_on_state`) succeeds,
|
||||||
|
/// passing the signer IDs derived from the transaction's witness set, the [`NonceSnapshot`]
|
||||||
|
/// captured **before** execution, and the post-execution state.
|
||||||
|
///
|
||||||
|
/// For a `NSSATransaction::Public(tx)`, derive signer IDs as:
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// let signer_ids: Vec<nssa::AccountId> = tx
|
||||||
|
/// .witness_set()
|
||||||
|
/// .signatures_and_public_keys()
|
||||||
|
/// .iter()
|
||||||
|
/// .map(|(_, pk)| nssa::AccountId::from(pk))
|
||||||
|
/// .collect();
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// For `NSSATransaction::ProgramDeployment`, there are no signers; pass an empty slice.
|
||||||
|
///
|
||||||
|
/// # Why a standalone function?
|
||||||
|
///
|
||||||
|
/// `apply_state_diff` consumes the `ValidatedStateDiff`, whose `signer_account_ids` field
|
||||||
|
/// is private to the `nssa` crate. The caller must therefore derive signer IDs from the
|
||||||
|
/// transaction's witness set before consuming the diff, and thread them into this helper.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// let signer_ids = /* derived from tx.witness_set() */;
|
||||||
|
/// let nonces_before = NonceSnapshot(
|
||||||
|
/// signer_ids.iter().map(|&id| (id, state.get_account_by_id(id).nonce)).collect(),
|
||||||
|
/// );
|
||||||
|
/// state.apply_state_diff(diff);
|
||||||
|
/// assert_nonce_increment_correctness(&signer_ids, &nonces_before, &state);
|
||||||
|
/// ```
|
||||||
|
pub fn assert_nonce_increment_correctness(
|
||||||
|
signer_ids: &[nssa::AccountId],
|
||||||
|
nonces_before: &NonceSnapshot,
|
||||||
|
state_after: &V03State,
|
||||||
|
) {
|
||||||
|
for &id in signer_ids {
|
||||||
|
let nonce_before = match nonces_before.0.get(&id) {
|
||||||
|
Some(n) => *n,
|
||||||
|
None => continue, // Account not in snapshot (e.g. newly created); skip.
|
||||||
|
};
|
||||||
|
let nonce_after = state_after.get_account_by_id(id).nonce;
|
||||||
|
let expected = Nonce(
|
||||||
|
nonce_before
|
||||||
|
.0
|
||||||
|
.checked_add(1)
|
||||||
|
.expect("nonce overflow — signer nonce at u128::MAX"),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
nonce_after, expected,
|
||||||
|
"INVARIANT VIOLATION [NonceIncrementCorrectness]: signer account {:?} nonce \
|
||||||
|
not incremented by 1 after successful transaction \
|
||||||
|
— before={:?}, expected={:?}, got={:?} \
|
||||||
|
(apply_state_diff failed to increment nonce exactly once)",
|
||||||
|
id, nonce_before, expected, nonce_after,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Dispatcher ───────────────────────────────────────────────────────────────
|
// ── Dispatcher ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Run every registered [`ProtocolInvariant`] and panic with a structured message
|
/// Run every registered [`ProtocolInvariant`] and panic with a structured message
|
||||||
@ -230,12 +321,14 @@ pub fn assert_replay_rejection(
|
|||||||
/// - [`BalanceConservation`] — total balance conserved on success
|
/// - [`BalanceConservation`] — total balance conserved on success
|
||||||
/// - [`FailedTxNonceStability`] — nonces unchanged on rejection
|
/// - [`FailedTxNonceStability`] — nonces unchanged on rejection
|
||||||
/// - [`ReplayRejection`] — stub only; use [`assert_replay_rejection`] directly
|
/// - [`ReplayRejection`] — stub only; use [`assert_replay_rejection`] directly
|
||||||
|
/// - [`NonceIncrementCorrectness`] — stub only; use [`assert_nonce_increment_correctness`] directly
|
||||||
pub fn assert_invariants(ctx: &InvariantCtx<'_>) {
|
pub fn assert_invariants(ctx: &InvariantCtx<'_>) {
|
||||||
let invariants: &[&dyn ProtocolInvariant] = &[
|
let invariants: &[&dyn ProtocolInvariant] = &[
|
||||||
&StateIsolationOnFailure,
|
&StateIsolationOnFailure,
|
||||||
&BalanceConservation,
|
&BalanceConservation,
|
||||||
&FailedTxNonceStability,
|
&FailedTxNonceStability,
|
||||||
&ReplayRejection,
|
&ReplayRejection,
|
||||||
|
&NonceIncrementCorrectness,
|
||||||
];
|
];
|
||||||
for inv in invariants {
|
for inv in invariants {
|
||||||
if let Some(violation) = inv.check(ctx) {
|
if let Some(violation) = inv.check(ctx) {
|
||||||
@ -316,6 +409,38 @@ mod tests {
|
|||||||
assert!(result.is_err(), "expected panic for balance inflation");
|
assert!(result.is_err(), "expected panic for balance inflation");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nonce_increment_correctness_passes_with_no_signers() {
|
||||||
|
// Empty signer list — no accounts to check; trivially satisfies the invariant.
|
||||||
|
let state = make_empty_state();
|
||||||
|
assert_nonce_increment_correctness(&[], &make_empty_nonce_snapshot(), &state);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nonce_increment_correctness_passes_when_signer_not_in_snapshot() {
|
||||||
|
// Signer ID is present in the list but absent from the snapshot — skipped.
|
||||||
|
let acc_id = nssa::AccountId::new([9u8; 32]);
|
||||||
|
let state = make_empty_state();
|
||||||
|
// Empty snapshot → `continue` branch fires; no assertion is made.
|
||||||
|
assert_nonce_increment_correctness(&[acc_id], &make_empty_nonce_snapshot(), &state);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nonce_increment_correctness_catches_unchanged_nonce() {
|
||||||
|
// Arrange: signer has nonce 5 in the snapshot; the state returns Nonce(0) for the
|
||||||
|
// same account (genesis default). expected = Nonce(6), actual = Nonce(0) → VIOLATION.
|
||||||
|
let acc_id = nssa::AccountId::new([3u8; 32]);
|
||||||
|
let state = V03State::new_with_genesis_accounts(&[], vec![], 0);
|
||||||
|
|
||||||
|
let mut nonces = std::collections::HashMap::new();
|
||||||
|
nonces.insert(acc_id, Nonce(5));
|
||||||
|
|
||||||
|
let result = std::panic::catch_unwind(|| {
|
||||||
|
assert_nonce_increment_correctness(&[acc_id], &NonceSnapshot(nonces), &state);
|
||||||
|
});
|
||||||
|
assert!(result.is_err(), "expected panic for unchanged nonce");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn failed_tx_nonce_stability_catches_nonce_mutation() {
|
fn failed_tx_nonce_stability_catches_nonce_mutation() {
|
||||||
let acc_id = nssa::AccountId::new([2u8; 32]);
|
let acc_id = nssa::AccountId::new([2u8; 32]);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user