mirror of
https://github.com/logos-blockchain/lez-fuzzing.git
synced 2026-06-07 11:39:30 +00:00
fix: improve API around tx invariants
This commit is contained in:
parent
db477a42d0
commit
88e780b865
@ -111,7 +111,7 @@ The extension noted in [`docs/fuzzing.md`](docs/fuzzing.md:356) is:
|
||||
| Execution time | Slow (recompile per mutation) | Continuous |
|
||||
| Output | Surviving mutants = assertion gaps | Crash artifacts |
|
||||
|
||||
**Decision-maker view**: `cargo-mutants` would **audit the invariant assertions themselves** — revealing if [`assert_invariants()`](fuzz_props/src/invariants.rs:325) has gaps. Three invariants are fully implemented: [`StateIsolationOnFailure`](fuzz_props/src/invariants.rs:60), [`BalanceConservation`](fuzz_props/src/invariants.rs:94), and [`FailedTxNonceStability`](fuzz_props/src/invariants.rs:130). Two are registry stubs: [`ReplayRejection`](fuzz_props/src/invariants.rs:169) and [`NonceIncrementCorrectness`](fuzz_props/src/invariants.rs:196) — each enforced via dedicated standalone helpers (`assert_replay_rejection`, `assert_nonce_increment_correctness`). This is a **complementary quality gate**, not a fuzzing replacement. Low cost (~1 day), highly useful before an external security audit.
|
||||
**Decision-maker view**: `cargo-mutants` would **audit the invariant assertions themselves** — revealing if [`assert_invariants()`](fuzz_props/src/invariants.rs) has gaps. Three invariants are fully implemented and registered in `assert_invariants()`: [`StateIsolationOnFailure`](fuzz_props/src/invariants.rs:60), [`BalanceConservation`](fuzz_props/src/invariants.rs:94), and [`FailedTxNonceStability`](fuzz_props/src/invariants.rs:130). Two additional invariants — [`ReplayRejection`](fuzz_props/src/invariants.rs:167) and [`NonceIncrementCorrectness`](fuzz_props/src/invariants.rs:194) — are enforced exclusively via standalone helpers (`assert_replay_rejection`, `assert_nonce_increment_correctness`) and are **not** in the `assert_invariants()` registry; this is intentional because they require data consumed before `InvariantCtx` is built. This is a **complementary quality gate**, not a fuzzing replacement. Low cost (~1 day), highly useful before an external security audit.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -478,37 +478,50 @@ automates steps 2–5 and uploads the report as a workflow artifact.
|
||||
|
||||
## Invariant Framework
|
||||
|
||||
Shared invariants live in `fuzz_props/src/invariants.rs`. Each invariant implements
|
||||
`ProtocolInvariant` and is automatically run by `assert_invariants()`.
|
||||
Shared invariants live in `fuzz_props/src/invariants.rs`. There are two layers:
|
||||
|
||||
Concrete invariants currently registered in `assert_invariants()`:
|
||||
### Primary API — `assert_tx_execution_invariants()`
|
||||
|
||||
| Invariant | Description | Implementation status |
|
||||
|-----------|-------------|----------------------|
|
||||
| `StateIsolationOnFailure` | Per-account balance must not change for any tracked account when a transaction is rejected | ✅ Fully implemented |
|
||||
| `BalanceConservation` | Total balance of all known accounts must be conserved when a transaction succeeds | ✅ Fully implemented |
|
||||
| `FailedTxNonceStability` | Every account's nonce must remain unchanged when a transaction is rejected | ✅ Fully implemented |
|
||||
| `ReplayRejection` | An accepted transaction must be rejected when replayed | ⚠️ Registry stub — always returns `None` from `InvariantCtx`; use `assert_replay_rejection()` directly (see note below) |
|
||||
| `NonceIncrementCorrectness` | Every signer account's nonce must be incremented by exactly one after a successful transaction | ⚠️ Registry stub — always returns `None` from `InvariantCtx`; use `assert_nonce_increment_correctness()` directly (see note below) |
|
||||
For every fuzz target that calls `execute_check_on_state`, use the single unified entry
|
||||
point. It enforces the five state-transition invariants in one call, routing by outcome:
|
||||
|
||||
> **Note on stub invariants:** `ReplayRejection` and `NonceIncrementCorrectness` cannot be
|
||||
> fully exercised through `InvariantCtx` alone. Each requires information that is consumed
|
||||
> before `InvariantCtx` is built:
|
||||
>
|
||||
> - **`ReplayRejection`**: `execute_check_on_state` returns the `NSSATransaction` on `Ok`,
|
||||
> consuming `self`. Replaying it requires re-applying the returned transaction to the
|
||||
> post-execution state — not possible via a shared `&InvariantCtx`. Use the standalone
|
||||
> `assert_replay_rejection(applied_tx, state, next_block_id, next_timestamp)` helper
|
||||
> immediately after each successful execution. The proptest suite `replay_rejection_proptest`
|
||||
> in `fuzz_props/src/invariants.rs` provides reproducible structured coverage of this
|
||||
> invariant.
|
||||
>
|
||||
> - **`NonceIncrementCorrectness`**: `apply_state_diff` consumes the `ValidatedStateDiff`
|
||||
> whose signer-account list is private to the `nssa` crate. The caller must derive signer
|
||||
> IDs from the transaction's witness set before consuming the diff, then call the standalone
|
||||
> `assert_nonce_increment_correctness(signer_ids, nonces_before, state_after)` helper.
|
||||
> The `signer_account_ids()` helper in `fuzz_props::generators` extracts signer `AccountId`s
|
||||
> from an `NSSATransaction`'s witness set.
|
||||
| Invariant | Active when |
|
||||
|-----------|-------------|
|
||||
| `StateIsolationOnFailure` | `execution_result` is `Err` |
|
||||
| `FailedTxNonceStability` | `execution_result` is `Err` |
|
||||
| `BalanceConservation` | `execution_result` is `Ok` |
|
||||
| `NonceIncrementCorrectness` | `execution_result` is `Ok` |
|
||||
| `ReplayRejection` | `execution_result` is `Ok` |
|
||||
|
||||
```rust
|
||||
let state_snapshot = state.clone();
|
||||
let result = tx.execute_check_on_state(&mut state, block_id, timestamp);
|
||||
|
||||
assert_tx_execution_invariants(
|
||||
&state_snapshot,
|
||||
&mut state,
|
||||
balances_before,
|
||||
nonces_before,
|
||||
result,
|
||||
(block_id + 1, timestamp + 1),
|
||||
);
|
||||
```
|
||||
|
||||
One call. No standalone helpers to remember.
|
||||
|
||||
### Registry API — `assert_invariants()` + `ProtocolInvariant`
|
||||
|
||||
Each invariant is a zero-size struct implementing `ProtocolInvariant`; `assert_invariants()`
|
||||
runs the registry and panics on the first violation. This lower-level API is used
|
||||
internally by `assert_tx_execution_invariants` and is also available for targets where no
|
||||
transaction is available for replay (e.g. pure state-serialization targets).
|
||||
|
||||
```rust
|
||||
// Only use assert_invariants() directly for non-execution contexts.
|
||||
// For execute_check_on_state call sites, prefer assert_tx_execution_invariants().
|
||||
assert_invariants(&InvariantCtx { state_before, state_after, execution_succeeded,
|
||||
balances_before, nonces_before });
|
||||
```
|
||||
|
||||
Additional invariants enforced **inline** in specific targets (not via `ProtocolInvariant`):
|
||||
|
||||
|
||||
@ -17,15 +17,14 @@
|
||||
//!
|
||||
//! # Invariants
|
||||
//!
|
||||
//! The following per-transaction invariants are checked via the shared framework
|
||||
//! ([`fuzz_props::invariants::assert_invariants`]) on every iteration:
|
||||
//! The following per-transaction invariants are checked via
|
||||
//! [`fuzz_props::invariants::assert_tx_execution_invariants`] on every iteration:
|
||||
//!
|
||||
//! - **StateIsolationOnFailure** — balances unchanged on rejection.
|
||||
//! - **BalanceConservation** — total balance conserved on success.
|
||||
//! - **FailedTxNonceStability** — nonces unchanged on rejection.
|
||||
//!
|
||||
//! In addition, [`assert_replay_rejection`] is called on every successful
|
||||
//! transaction (per-block replay check).
|
||||
//! - **BalanceConservation** — total balance conserved on success.
|
||||
//! - **NonceIncrementCorrectness** — signer nonces each increment by exactly one on success.
|
||||
//! - **ReplayRejection** — every successful transaction rejected on replay (per-block).
|
||||
//!
|
||||
//! The following multi-block aggregate invariant is checked **after** the loop:
|
||||
//!
|
||||
@ -35,11 +34,8 @@
|
||||
//! the total; only mint/burn bugs or token-inflation bugs would break it.
|
||||
|
||||
use arbitrary::{Arbitrary, Unstructured};
|
||||
use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction, signer_account_ids};
|
||||
use fuzz_props::invariants::{
|
||||
BalanceSnapshot, InvariantCtx, NonceSnapshot, assert_invariants, assert_nonce_increment_correctness,
|
||||
assert_replay_rejection,
|
||||
};
|
||||
use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction};
|
||||
use fuzz_props::invariants::{BalanceSnapshot, NonceSnapshot, assert_tx_execution_invariants};
|
||||
use nssa::V03State;
|
||||
|
||||
fuzz_props::fuzz_entry!(|data: &[u8]| {
|
||||
@ -99,29 +95,19 @@ fuzz_props::fuzz_entry!(|data: &[u8]| {
|
||||
let state_snapshot = state.clone();
|
||||
|
||||
let result = tx.execute_check_on_state(&mut state, block_id, timestamp);
|
||||
let execution_succeeded = result.is_ok();
|
||||
|
||||
// ── Shared invariant checks ───────────────────────────────────────────
|
||||
// Asserts per-transaction:
|
||||
// • StateIsolationOnFailure — balances unchanged on rejection
|
||||
// • BalanceConservation — total balance conserved on success
|
||||
// • FailedTxNonceStability — nonces unchanged on rejection
|
||||
assert_invariants(&InvariantCtx {
|
||||
state_before: &state_snapshot,
|
||||
state_after: &state,
|
||||
execution_succeeded,
|
||||
// ── All five protocol invariants ──────────────────────────────────────
|
||||
// A single call enforces every invariant — no standalone helpers needed:
|
||||
// On rejection: StateIsolationOnFailure + FailedTxNonceStability
|
||||
// On success: BalanceConservation + NonceIncrementCorrectness + ReplayRejection
|
||||
assert_tx_execution_invariants(
|
||||
&state_snapshot,
|
||||
&mut state,
|
||||
balances_before,
|
||||
nonces_before: nonces_before.clone(),
|
||||
});
|
||||
|
||||
// ── NonceIncrementCorrectness + ReplayRejection (per-block) ──────────
|
||||
// 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 {
|
||||
let ids = signer_account_ids(&applied_tx);
|
||||
assert_nonce_increment_correctness(&ids, &nonces_before, &state);
|
||||
assert_replay_rejection(applied_tx, &mut state, block_id + 1, timestamp + 1);
|
||||
}
|
||||
nonces_before,
|
||||
result,
|
||||
(block_id + 1, timestamp + 1),
|
||||
);
|
||||
}
|
||||
|
||||
// ── LongRangeBalanceConservation ──────────────────────────────────────────
|
||||
|
||||
@ -14,22 +14,17 @@
|
||||
//!
|
||||
//! # Invariants checked
|
||||
//!
|
||||
//! The shared framework ([`assert_invariants`]) enforces per-transaction:
|
||||
//! [`assert_tx_execution_invariants`] enforces all five invariants per transaction
|
||||
//! in one call:
|
||||
//! - **StateIsolationOnFailure** — balances unchanged on rejection
|
||||
//! - **BalanceConservation** — total balance conserved on success
|
||||
//! - **FailedTxNonceStability** — nonces unchanged on rejection
|
||||
//!
|
||||
//! The dedicated [`assert_replay_rejection`] function enforces:
|
||||
//! - **NonceIncrementCorrectness** — signer nonces each increment by exactly one on success
|
||||
//! - **ReplayRejection** — accepted tx rejected on replay
|
||||
|
||||
use arbitrary::{Arbitrary, Unstructured};
|
||||
use fuzz_props::generators::{
|
||||
arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction, signer_account_ids,
|
||||
};
|
||||
use fuzz_props::invariants::{
|
||||
BalanceSnapshot, InvariantCtx, NonceSnapshot, assert_invariants,
|
||||
assert_nonce_increment_correctness, assert_replay_rejection,
|
||||
};
|
||||
use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction};
|
||||
use fuzz_props::invariants::{BalanceSnapshot, NonceSnapshot, assert_tx_execution_invariants};
|
||||
use nssa::V03State;
|
||||
|
||||
fuzz_props::fuzz_entry!(|data: &[u8]| {
|
||||
@ -77,27 +72,17 @@ fuzz_props::fuzz_entry!(|data: &[u8]| {
|
||||
|
||||
// First application — may legitimately fail for state-level reasons.
|
||||
let result = tx.execute_check_on_state(&mut state, 1, 0);
|
||||
let execution_succeeded = result.is_ok();
|
||||
|
||||
// ── Shared invariant checks ───────────────────────────────────────────────
|
||||
// Asserts:
|
||||
// • StateIsolationOnFailure — balances unchanged on rejection
|
||||
// • BalanceConservation — total balance conserved on success
|
||||
// • FailedTxNonceStability — nonces unchanged on rejection
|
||||
assert_invariants(&InvariantCtx {
|
||||
state_before: &state_snapshot,
|
||||
state_after: &state,
|
||||
execution_succeeded,
|
||||
// ── All five protocol invariants ──────────────────────────────────────────
|
||||
// A single call enforces every invariant — no standalone helpers needed:
|
||||
// On rejection: StateIsolationOnFailure + FailedTxNonceStability
|
||||
// On success: BalanceConservation + NonceIncrementCorrectness + ReplayRejection
|
||||
assert_tx_execution_invariants(
|
||||
&state_snapshot,
|
||||
&mut state,
|
||||
balances_before,
|
||||
nonces_before: nonces_before.clone(),
|
||||
});
|
||||
|
||||
// ── NonceIncrementCorrectness + ReplayRejection ───────────────────────────
|
||||
// First verify every signer's nonce was incremented by exactly one, then
|
||||
// assert that replaying in the next block is rejected (nonce permanently consumed).
|
||||
if let Ok(applied_tx) = result {
|
||||
let signer_ids = signer_account_ids(&applied_tx);
|
||||
assert_nonce_increment_correctness(&signer_ids, &nonces_before, &state);
|
||||
assert_replay_rejection(applied_tx, &mut state, 2, 1);
|
||||
}
|
||||
nonces_before,
|
||||
result,
|
||||
(2, 1),
|
||||
);
|
||||
});
|
||||
|
||||
@ -1,13 +1,8 @@
|
||||
#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)]
|
||||
|
||||
use arbitrary::{Arbitrary, Unstructured};
|
||||
use fuzz_props::generators::{
|
||||
arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction, signer_account_ids,
|
||||
};
|
||||
use fuzz_props::invariants::{
|
||||
BalanceSnapshot, InvariantCtx, NonceSnapshot, assert_invariants,
|
||||
assert_nonce_increment_correctness, assert_replay_rejection,
|
||||
};
|
||||
use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction};
|
||||
use fuzz_props::invariants::{BalanceSnapshot, NonceSnapshot, assert_tx_execution_invariants};
|
||||
use nssa::V03State;
|
||||
|
||||
fuzz_props::fuzz_entry!(|data: &[u8]| {
|
||||
@ -75,29 +70,18 @@ fuzz_props::fuzz_entry!(|data: &[u8]| {
|
||||
// Snapshot state before execution for isolation checks.
|
||||
let state_snapshot = state.clone();
|
||||
let result = tx.execute_check_on_state(&mut state, block_id, timestamp);
|
||||
let execution_succeeded = result.is_ok();
|
||||
|
||||
// ── Shared invariant checks ───────────────────────────────────────────
|
||||
// Asserts:
|
||||
// • StateIsolationOnFailure — balances unchanged on rejection
|
||||
// • BalanceConservation — total balance conserved on success
|
||||
// • FailedTxNonceStability — nonces unchanged on rejection
|
||||
assert_invariants(&InvariantCtx {
|
||||
state_before: &state_snapshot,
|
||||
state_after: &state,
|
||||
execution_succeeded,
|
||||
// ── All five protocol invariants ──────────────────────────────────────
|
||||
// A single call enforces every invariant — no standalone helpers needed:
|
||||
// On rejection: StateIsolationOnFailure + FailedTxNonceStability
|
||||
// On success: BalanceConservation + NonceIncrementCorrectness + ReplayRejection
|
||||
assert_tx_execution_invariants(
|
||||
&state_snapshot,
|
||||
&mut state,
|
||||
balances_before,
|
||||
nonces_before: nonces_before.clone(),
|
||||
});
|
||||
|
||||
// ── NonceIncrementCorrectness + ReplayRejection ───────────────────────
|
||||
// execute_check_on_state returns the NSSATransaction on Ok.
|
||||
// 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 {
|
||||
let signer_ids = signer_account_ids(&applied_tx);
|
||||
assert_nonce_increment_correctness(&signer_ids, &nonces_before, &state);
|
||||
assert_replay_rejection(applied_tx, &mut state, block_id + 1, timestamp + 1);
|
||||
}
|
||||
nonces_before,
|
||||
result,
|
||||
(block_id + 1, timestamp + 1),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@ -155,56 +155,47 @@ impl ProtocolInvariant for FailedTxNonceStability {
|
||||
|
||||
/// A successfully accepted transaction must be rejected when replayed.
|
||||
///
|
||||
/// # Note
|
||||
/// # Enforcement
|
||||
///
|
||||
/// This invariant **cannot** be exercised through [`InvariantCtx`] alone because
|
||||
/// the replay check requires re-applying the `NSSATransaction` that was consumed
|
||||
/// by `execute_check_on_state`. The `ProtocolInvariant` impl here is a registry
|
||||
/// placeholder only; it always returns `None`.
|
||||
/// This invariant **cannot** be enforced through [`InvariantCtx`] because the replay
|
||||
/// check requires re-applying the `NSSATransaction` that `execute_check_on_state`
|
||||
/// consumes and returns on `Ok`. It is therefore **not registered** in
|
||||
/// [`assert_invariants`]; calling `assert_invariants` alone does **not** cover
|
||||
/// `ReplayRejection`.
|
||||
///
|
||||
/// Use the standalone [`assert_replay_rejection`] function instead, which accepts
|
||||
/// the `NSSATransaction` returned on success and performs the replay inline.
|
||||
/// Every fuzz target that performs state transitions **must** call the standalone
|
||||
/// [`assert_replay_rejection`] function after each successful execution:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// if let Ok(applied_tx) = result {
|
||||
/// assert_replay_rejection(applied_tx, &mut state, block_id + 1, timestamp + 1);
|
||||
/// }
|
||||
/// ```
|
||||
pub struct ReplayRejection;
|
||||
|
||||
impl ProtocolInvariant for ReplayRejection {
|
||||
fn name(&self) -> &'static str {
|
||||
"ReplayRejection"
|
||||
}
|
||||
|
||||
fn check(&self, _ctx: &InvariantCtx<'_>) -> Option<InvariantViolation> {
|
||||
// ReplayRejection cannot be fully exercised through InvariantCtx alone.
|
||||
// Use `assert_replay_rejection(applied_tx, state, next_block_id, next_ts)` instead.
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A successfully applied transaction must increment the nonce of every signer account
|
||||
/// by exactly one.
|
||||
///
|
||||
/// # Note
|
||||
/// # Enforcement
|
||||
///
|
||||
/// 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`.
|
||||
/// This invariant **cannot** be enforced through [`InvariantCtx`] because signer
|
||||
/// account IDs are private to the `nssa` crate and are consumed by `apply_state_diff`
|
||||
/// before the caller can observe them. It is therefore **not registered** in
|
||||
/// [`assert_invariants`]; calling `assert_invariants` alone does **not** cover
|
||||
/// `NonceIncrementCorrectness`.
|
||||
///
|
||||
/// 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.
|
||||
/// Every fuzz target that performs state transitions **must** call the standalone
|
||||
/// [`assert_nonce_increment_correctness`] function after each successful execution,
|
||||
/// passing signer IDs derived from the transaction's witness set:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// if let Ok(applied_tx) = result {
|
||||
/// let signer_ids = signer_account_ids(&applied_tx);
|
||||
/// assert_nonce_increment_correctness(&signer_ids, &nonces_before, &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 ────────────────────────────────────────────────────────
|
||||
|
||||
/// Assert that a successfully-applied transaction is **rejected** when replayed.
|
||||
@ -308,24 +299,124 @@ pub fn assert_nonce_increment_correctness(
|
||||
}
|
||||
}
|
||||
|
||||
// ── Dispatcher ───────────────────────────────────────────────────────────────
|
||||
// ── Dispatchers ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// Run every registered [`ProtocolInvariant`] and panic with a structured message
|
||||
/// on the first violation.
|
||||
/// Assert the five state-transition invariants for a single `execute_check_on_state` call.
|
||||
///
|
||||
/// Covers the invariants that are defined over one transaction execution attempt —
|
||||
/// both the failure-isolation properties and the success-outcome correctness properties.
|
||||
/// All are enforced from a single call; no standalone helpers are needed:
|
||||
///
|
||||
/// | Invariant | Active when |
|
||||
/// |-----------|-------------|
|
||||
/// | [`StateIsolationOnFailure`] | `execution_result` is `Err` |
|
||||
/// | [`FailedTxNonceStability`] | `execution_result` is `Err` |
|
||||
/// | [`BalanceConservation`] | `execution_result` is `Ok` |
|
||||
/// | [`NonceIncrementCorrectness`] | `execution_result` is `Ok` |
|
||||
/// | [`ReplayRejection`] | `execution_result` is `Ok` |
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `state_before` — clone of the state captured **before** `execute_check_on_state`.
|
||||
/// * `state_after` — live state **after** execution (mutably borrowed for the replay attempt).
|
||||
/// * `balances_before` — per-account balance snapshot captured before execution.
|
||||
/// * `nonces_before` — per-account nonce snapshot captured before execution.
|
||||
/// * `execution_result` — the `Result` returned by `execute_check_on_state`.
|
||||
/// * `replay_context` — `(next_block_id, next_timestamp)` used for the mandatory replay attempt.
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// let state_snapshot = state.clone();
|
||||
/// let balances_before = BalanceSnapshot(
|
||||
/// accounts.iter().map(|&(id, _)| (id, state.get_account_by_id(id).balance)).collect(),
|
||||
/// );
|
||||
/// let nonces_before = NonceSnapshot(
|
||||
/// accounts.iter().map(|&(id, _)| (id, state.get_account_by_id(id).nonce)).collect(),
|
||||
/// );
|
||||
/// let result = tx.execute_check_on_state(&mut state, block_id, timestamp);
|
||||
///
|
||||
/// assert_tx_execution_invariants(
|
||||
/// &state_snapshot,
|
||||
/// &mut state,
|
||||
/// balances_before,
|
||||
/// nonces_before,
|
||||
/// result,
|
||||
/// (block_id + 1, timestamp + 1),
|
||||
/// );
|
||||
/// ```
|
||||
pub fn assert_tx_execution_invariants<E>(
|
||||
state_before: &V03State,
|
||||
state_after: &mut V03State,
|
||||
balances_before: BalanceSnapshot,
|
||||
nonces_before: NonceSnapshot,
|
||||
execution_result: Result<NSSATransaction, E>,
|
||||
replay_context: (u64, u64),
|
||||
) {
|
||||
let execution_succeeded = execution_result.is_ok();
|
||||
// Clone nonces_before before it is moved into InvariantCtx so the clone
|
||||
// remains available for assert_nonce_increment_correctness on the success path.
|
||||
let nonces_for_nonce_check = nonces_before.clone();
|
||||
|
||||
// ── Three InvariantCtx-based invariants ───────────────────────────────────
|
||||
// The shared reborrow of state_after ends when assert_invariants returns (NLL),
|
||||
// after which state_after is available mutably again for the replay attempt.
|
||||
assert_invariants(&InvariantCtx {
|
||||
state_before,
|
||||
state_after: &*state_after,
|
||||
execution_succeeded,
|
||||
balances_before,
|
||||
nonces_before,
|
||||
});
|
||||
|
||||
// ── Two success-only invariants ───────────────────────────────────────────
|
||||
if let Ok(applied_tx) = execution_result {
|
||||
// Derive signer IDs from the witness set. ProgramDeployment has no signers.
|
||||
let signer_ids: Vec<nssa::AccountId> = match &applied_tx {
|
||||
NSSATransaction::Public(pt) => pt
|
||||
.witness_set()
|
||||
.signatures_and_public_keys()
|
||||
.iter()
|
||||
.map(|(_, pk)| nssa::AccountId::from(pk))
|
||||
.collect(),
|
||||
NSSATransaction::PrivacyPreserving(pt) => pt
|
||||
.witness_set()
|
||||
.signatures_and_public_keys()
|
||||
.iter()
|
||||
.map(|(_, pk)| nssa::AccountId::from(pk))
|
||||
.collect(),
|
||||
NSSATransaction::ProgramDeployment(_) => vec![],
|
||||
};
|
||||
assert_nonce_increment_correctness(&signer_ids, &nonces_for_nonce_check, state_after);
|
||||
let (next_block_id, next_timestamp) = replay_context;
|
||||
assert_replay_rejection(applied_tx, state_after, next_block_id, next_timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the three [`InvariantCtx`]-based invariants and panic on the first violation.
|
||||
///
|
||||
/// Invariants checked:
|
||||
/// - [`StateIsolationOnFailure`] — balances unchanged on rejection
|
||||
/// - [`BalanceConservation`] — total balance conserved on success
|
||||
/// - [`FailedTxNonceStability`] — nonces unchanged on rejection
|
||||
/// - [`ReplayRejection`] — stub only; use [`assert_replay_rejection`] directly
|
||||
/// - [`NonceIncrementCorrectness`] — stub only; use [`assert_nonce_increment_correctness`] directly
|
||||
///
|
||||
/// | Invariant | Condition |
|
||||
/// |-----------|-----------|
|
||||
/// | [`StateIsolationOnFailure`] | balances unchanged on rejection |
|
||||
/// | [`BalanceConservation`] | total balance conserved on success |
|
||||
/// | [`FailedTxNonceStability`] | nonces unchanged on rejection |
|
||||
///
|
||||
/// # Prefer [`assert_tx_execution_invariants`] for `execute_check_on_state` call sites
|
||||
///
|
||||
/// [`ReplayRejection`] and [`NonceIncrementCorrectness`] are not checked here — they
|
||||
/// require data unavailable inside [`InvariantCtx`]. Use [`assert_tx_execution_invariants`]
|
||||
/// instead for any target that calls `execute_check_on_state`; it enforces all five
|
||||
/// invariants in one call.
|
||||
///
|
||||
/// Reserve `assert_invariants` for contexts where no transaction is available for
|
||||
/// replay (e.g. pure state-serialization or encoding targets).
|
||||
pub fn assert_invariants(ctx: &InvariantCtx<'_>) {
|
||||
let invariants: &[&dyn ProtocolInvariant] = &[
|
||||
&StateIsolationOnFailure,
|
||||
&BalanceConservation,
|
||||
&FailedTxNonceStability,
|
||||
&ReplayRejection,
|
||||
&NonceIncrementCorrectness,
|
||||
];
|
||||
for inv in invariants {
|
||||
if let Some(violation) = inv.check(ctx) {
|
||||
|
||||
@ -31,7 +31,7 @@ fn invariant_state_isolation_on_failure_does_not_panic_on_error() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invariant_replay_rejection_does_not_panic() {
|
||||
fn assert_invariants_does_not_panic_on_success_with_empty_state() {
|
||||
let state = make_empty_state();
|
||||
let ctx = InvariantCtx {
|
||||
state_before: &state,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user