chore: add linting formatting

- align workflows
This commit is contained in:
Roman 2026-05-27 14:39:10 +08:00
parent 3294923b87
commit 2320beb110
No known key found for this signature in database
GPG Key ID: 583BDF43C238B83E
7 changed files with 204 additions and 73 deletions

View File

@ -2,18 +2,18 @@ name: AFL++ Fuzzing
on:
schedule:
- cron: "0 2 * * *" # nightly at 02:00 UTC
workflow_dispatch: # manual trigger
- cron: "0 2 * * *"
workflow_dispatch:
push:
branches:
- feat-add-afl-fuzzing
branches: [main]
env:
RISC0_DEV_MODE: "1"
CARGO_TERM_COLOR: always
jobs:
# ────────────────────────────────────────────────────────────────────────────
# afl-smoke — 120-second campaign for all 15 targets
# afl-smoke — 60-second per targets
# ────────────────────────────────────────────────────────────────────────────
afl-smoke:
name: "AFL++ smoke — ${{ matrix.target }}"
@ -108,7 +108,7 @@ jobs:
fi
echo "Seed inputs: $(ls "$SEEDS" | wc -l)"
- name: Run AFL++ for 120 seconds
- name: Run AFL++ for 60 seconds
env:
AFL_SKIP_CPUFREQ: "1"
AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES: "1"
@ -118,7 +118,7 @@ jobs:
# Disable errexit so that timeout's exit code 124 (expected signal) does not
# cause bash -e to abort the script before the guard below can run.
set +e
timeout 120 \
timeout 60 \
afl-fuzz \
-i afl-seeds/${TARGET} \
-o afl-output/${TARGET} \

View File

@ -1,12 +1,11 @@
name: Fuzzing
on:
push:
branches: [main, develop, feat-add-afl-fuzzing]
pull_request:
schedule:
# Nightly full run
- cron: "0 2 * * *"
workflow_dispatch:
push:
branches: [main]
env:
RISC0_DEV_MODE: "1"

78
.github/workflows/lint.yml vendored Normal file
View File

@ -0,0 +1,78 @@
name: Lint
on:
push:
branches:
- main
paths-ignore:
- "**.md"
- "!.github/workflows/*.yml"
pull_request:
paths-ignore:
- "**.md"
- "!.github/workflows/*.yml"
env:
RISC0_DEV_MODE: "1"
CARGO_TERM_COLOR: always
permissions:
contents: read
pull-requests: read
jobs:
# ── rustfmt ──────────────────────────────────────────────────────────────────
fmt-rs:
name: Rust formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- name: Install nightly toolchain for rustfmt
run: rustup install nightly --profile minimal --component rustfmt
- name: Check Rust files are formatted
run: cargo +nightly fmt --check
# ── clippy ───────────────────────────────────────────────────────────────────
lint:
name: Clippy
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- name: Checkout logos-execution-zone alongside lez-fuzzing
uses: actions/checkout@v4
with:
repository: logos-blockchain/logos-execution-zone
path: logos-execution-zone
- name: Symlink logos-execution-zone to sibling directory
run: ln -s "$GITHUB_WORKSPACE/logos-execution-zone" "$GITHUB_WORKSPACE/../logos-execution-zone"
- name: Install logos-blockchain-circuits
uses: ./logos-execution-zone/.github/actions/install-logos-blockchain-circuits
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install stable toolchain with clippy
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Restore Rust cache
uses: Swatinem/rust-cache@v2
with:
shared-key: lint-rust-cache
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Lint workspace
env:
RISC0_DEV_MODE: "1"
run: cargo clippy --workspace --all-targets --all-features -- -D warnings

View File

@ -118,13 +118,10 @@ impl<'a> Arbitrary<'a> for ArbPublicKey {
// rejection path in `is_valid_for` independently.
let bytes = <[u8; 32]>::arbitrary(u)?;
let pk = PublicKey::try_new(bytes).unwrap_or_else(|_| {
PublicKey::new_from_private_key(
&ArbPrivateKey::arbitrary(u)
.map(|w| w.0)
.unwrap_or_else(|_| {
PrivateKey::try_new([1_u8; 32]).expect("known-good seed")
}),
)
PublicKey::new_from_private_key(&ArbPrivateKey::arbitrary(u).map_or_else(
|_| PrivateKey::try_new([1_u8; 32]).expect("known-good seed"),
|w| w.0,
))
});
Ok(Self(pk))
}
@ -145,11 +142,11 @@ impl<'a> Arbitrary<'a> for ArbPubTxMessage {
let program_id: [u32; 8] = <[u32; 8]>::arbitrary(u)?;
// Generate 07 accounts; nonces vector is given the same length.
let len = (u8::arbitrary(u)? as usize) % 8;
let account_ids = (0..len)
.map(|_| ArbAccountId::arbitrary(u).map(|a| a.0))
let account_ids = std::iter::repeat_with(|| ArbAccountId::arbitrary(u).map(|a| a.0))
.take(len)
.collect::<ArbResult<Vec<_>>>()?;
let nonces = (0..len)
.map(|_| ArbNonce::arbitrary(u).map(|n| n.0))
let nonces = std::iter::repeat_with(|| ArbNonce::arbitrary(u).map(|n| n.0))
.take(len)
.collect::<ArbResult<Vec<_>>>()?;
let instruction_data: Vec<u32> = Vec::<u32>::arbitrary(u)?;
Ok(Self(Message::new_preserialized(
@ -174,9 +171,11 @@ impl<'a> Arbitrary<'a> for ArbWitnessSet {
fn arbitrary(u: &mut Unstructured<'a>) -> ArbResult<Self> {
// 03 (signature, public_key) pairs
let n = (u8::arbitrary(u)? as usize) % 4;
let pairs = (0..n)
.map(|_| Ok((ArbSignature::arbitrary(u)?.0, ArbPublicKey::arbitrary(u)?.0)))
.collect::<ArbResult<Vec<_>>>()?;
let pairs = std::iter::repeat_with(|| {
Ok((ArbSignature::arbitrary(u)?.0, ArbPublicKey::arbitrary(u)?.0))
})
.take(n)
.collect::<ArbResult<Vec<_>>>()?;
Ok(Self(WitnessSet::from_raw_parts(pairs)))
}
}
@ -247,8 +246,8 @@ impl<'a> Arbitrary<'a> for ArbHashableBlockData {
fn arbitrary(u: &mut Unstructured<'a>) -> ArbResult<Self> {
// 07 transactions per block
let n = (u8::arbitrary(u)? as usize) % 8;
let transactions = (0..n)
.map(|_| ArbNSSATransaction::arbitrary(u).map(|t| t.0))
let transactions = std::iter::repeat_with(|| ArbNSSATransaction::arbitrary(u).map(|t| t.0))
.take(n)
.collect::<ArbResult<Vec<_>>>()?;
Ok(Self(HashableBlockData {
block_id: u64::arbitrary(u)?,

View File

@ -11,6 +11,7 @@ use testnet_initial_state::initial_pub_accounts_private_keys;
/// Extract the [`AccountId`]s of all signers from a transaction's
/// witness set. Used by fuzz targets that need to verify nonce
/// increments after `execute_check_on_state`.
#[must_use]
pub fn signer_account_ids(tx: &common::transaction::NSSATransaction) -> Vec<nssa::AccountId> {
use common::transaction::NSSATransaction;
match tx {
@ -52,15 +53,15 @@ pub struct FuzzAccount {
/// has a shape controlled by the fuzzer rather than fixed at compile time.
pub fn arbitrary_fuzz_state(u: &mut Unstructured<'_>) -> arbitrary::Result<Vec<FuzzAccount>> {
let n = ((u8::arbitrary(u)? as usize) % 8) + 1; // 1..=8
(0..n)
.map(|_| {
Ok(FuzzAccount {
account_id: ArbAccountId::arbitrary(u)?.0,
balance: u128::arbitrary(u)?,
private_key: ArbPrivateKey::arbitrary(u)?.0,
})
std::iter::repeat_with(|| {
Ok(FuzzAccount {
account_id: ArbAccountId::arbitrary(u)?.0,
balance: u128::arbitrary(u)?,
private_key: ArbPrivateKey::arbitrary(u)?.0,
})
.collect()
})
.take(n)
.collect()
}
/// Generate a native-transfer [`NSSATransaction`] between two accounts chosen
@ -115,8 +116,8 @@ prop_compose! {
)(
from_idx in 0..accounts.len(),
to_idx in 0..accounts.len(),
nonce in 0u128..1_000u128,
amount in 0u128..10_000u128,
nonce in 0_u128..1_000_u128,
amount in 0_u128..10_000_u128,
) -> NSSATransaction {
let (from_id, from_key) = &accounts[from_idx];
let (to_id, _) = &accounts[to_idx];
@ -127,6 +128,7 @@ prop_compose! {
}
/// Return the test accounts from `testnet_initial_state` as `(AccountId, PrivateKey)` pairs.
#[must_use]
pub fn test_accounts() -> Vec<(AccountId, PrivateKey)> {
initial_pub_accounts_private_keys()
.into_iter()
@ -168,9 +170,9 @@ prop_compose! {
/// the state is left unchanged on rejection (StateIsolationOnFailure).
pub fn arb_invalid_account_state_tx()(
// Use a random 32-byte seed as a "phantom" account id not in genesis
phantom_id_bytes in proptest::array::uniform32(0u8..),
phantom_id_bytes in proptest::array::uniform32(0_u8..),
amount in (u128::MAX / 2)..u128::MAX, // overflow-inducing amount
nonce in 0u128..10u128,
nonce in 0_u128..10_u128,
) -> NSSATransaction {
let phantom_id = nssa::AccountId::new(phantom_id_bytes);
// Attempt to sign with a key that has no matching on-chain account
@ -216,14 +218,14 @@ pub fn arb_duplicate_tx_sequence() -> impl Strategy<Value = Vec<NSSATransaction>
pub fn arb_pathological_sequence() -> impl Strategy<Value = Vec<NSSATransaction>> {
let accounts = test_accounts();
let n = accounts.len();
proptest::collection::vec((0..n, 0..n, 0u128..5u128, any::<bool>()), 1..8_usize).prop_map(
proptest::collection::vec((0..n, 0..n, 0_u128..5_u128, any::<bool>()), 1..8_usize).prop_map(
move |params| {
params
.into_iter()
.map(|(from_idx, to_idx, nonce, zero_amount)| {
let (from_id, from_key) = &accounts[from_idx];
let (to_id, _) = &accounts[to_idx];
let amount = if zero_amount { 0u128 } else { u128::MAX }; // 0 or overflow
let amount = if zero_amount { 0_u128 } else { u128::MAX }; // 0 or overflow
common::test_utils::create_transaction_native_token_transfer(
*from_id, nonce, *to_id, amount, from_key,
)

View File

@ -9,7 +9,7 @@ pub struct BalanceSnapshot(pub std::collections::HashMap<nssa::AccountId, u128>)
impl BalanceSnapshot {
/// Capture current total balance over all known accounts.
pub fn total(&self) -> u128 {
self.0.values().copied().fold(0u128, u128::saturating_add)
self.0.values().copied().fold(0_u128, u128::saturating_add)
}
}
@ -72,9 +72,8 @@ impl ProtocolInvariant for StateIsolationOnFailure {
return Some(InvariantViolation {
invariant: self.name(),
message: format!(
"balance changed despite tx rejection: account {:?} had \
"balance changed despite tx rejection: account {acc_id:?} had \
{expected_balance} before, {actual_balance} after",
acc_id,
),
});
}
@ -106,7 +105,7 @@ impl ProtocolInvariant for BalanceConservation {
.0
.keys()
.map(|&id| ctx.state_after.get_account_by_id(id).balance)
.fold(0u128, u128::saturating_add);
.fold(0_u128, u128::saturating_add);
if total_before != total_after {
return Some(InvariantViolation {
invariant: self.name(),
@ -142,10 +141,9 @@ impl ProtocolInvariant for FailedTxNonceStability {
return Some(InvariantViolation {
invariant: self.name(),
message: format!(
"nonce changed despite tx rejection: account {:?} nonce was \
{:?} before, {:?} after \
(griefing attack victim nonce permanently burned on failed tx)",
acc_id, expected_nonce, actual_nonce,
"nonce changed despite tx rejection: account {acc_id:?} nonce was \
{expected_nonce:?} before, {actual_nonce:?} after \
(griefing attack \u{2014} victim nonce permanently burned on failed tx)",
),
});
}
@ -241,7 +239,7 @@ pub fn assert_replay_rejection(
let replay = applied_tx.execute_check_on_state(state, next_block_id, next_timestamp);
assert!(
replay.is_err(),
"INVARIANT VIOLATION [ReplayRejection]: transaction accepted a second time \
"INVARIANT VIOLATION [ReplayRejection]: transaction accepted a second time \u{2014} \
nonce replay not prevented (replay block_id={next_block_id}, \
replay timestamp={next_timestamp})",
);
@ -298,15 +296,14 @@ pub fn assert_nonce_increment_correctness(
nonce_before
.0
.checked_add(1)
.expect("nonce overflow signer nonce at u128::MAX"),
.expect("nonce overflow \u{2014} signer nonce at u128::MAX"),
);
assert_eq!(
nonce_after, expected,
"INVARIANT VIOLATION [NonceIncrementCorrectness]: signer account {:?} nonce \
"INVARIANT VIOLATION [NonceIncrementCorrectness]: signer account {id:?} nonce \
not incremented by 1 after successful transaction \
before={:?}, expected={:?}, got={:?} \
\u{2014} before={nonce_before:?}, expected={expected:?}, got={nonce_after:?} \
(apply_state_diff failed to increment nonce exactly once)",
id, nonce_before, expected, nonce_after,
);
}
}
@ -389,13 +386,13 @@ mod tests {
#[test]
fn balance_conservation_catches_inflation_on_success() {
// Arrange: one account with balance 100.
let acc_id = nssa::AccountId::new([1u8; 32]);
let acc_id = nssa::AccountId::new([1_u8; 32]);
let state_before = V03State::new_with_genesis_accounts(&[(acc_id, 100)], vec![], 0);
// Simulate execution that inflated the balance to 200.
let state_after = V03State::new_with_genesis_accounts(&[(acc_id, 200)], vec![], 0);
let mut balances = std::collections::HashMap::new();
balances.insert(acc_id, 100u128);
balances.insert(acc_id, 100_u128);
let ctx = InvariantCtx {
state_before: &state_before,
@ -419,7 +416,7 @@ mod tests {
#[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 acc_id = nssa::AccountId::new([9_u8; 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);
@ -429,7 +426,7 @@ mod tests {
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 acc_id = nssa::AccountId::new([3_u8; 32]);
let state = V03State::new_with_genesis_accounts(&[], vec![], 0);
let mut nonces = std::collections::HashMap::new();
@ -443,7 +440,7 @@ mod tests {
#[test]
fn failed_tx_nonce_stability_catches_nonce_mutation() {
let acc_id = nssa::AccountId::new([2u8; 32]);
let acc_id = nssa::AccountId::new([2_u8; 32]);
// before: nonce 5; after: nonce 6 (should not happen on failure)
let state_before = V03State::new_with_genesis_accounts(&[(acc_id, 100)], vec![], 0);
let state_after = V03State::new_with_genesis_accounts(&[(acc_id, 100)], vec![], 0);
@ -455,7 +452,7 @@ mod tests {
nonces.insert(acc_id, Nonce(1));
let mut balances = std::collections::HashMap::new();
balances.insert(acc_id, 100u128);
balances.insert(acc_id, 100_u128);
let ctx = InvariantCtx {
state_before: &state_before,
@ -493,36 +490,33 @@ mod replay_proptest {
let accounts = test_accounts();
let init_accs: Vec<(nssa::AccountId, u128)> = accounts
.iter()
.map(|(id, _)| (*id, 1_000_000u128))
.map(|(id, _)| (*id, 1_000_000_u128))
.collect();
V03State::new_with_genesis_accounts(&init_accs, vec![], 0)
}
proptest! {
/// **ReplayRejection** a transaction accepted in block N must be
/// **ReplayRejection** \u{2014} a transaction accepted in block N must be
/// rejected when replayed in block N+1, because the nonce is consumed
/// on first acceptance.
#[test]
fn replay_rejection_proptest(tx in arb_native_transfer_tx(test_accounts())) {
let mut state = make_test_state();
// Stateless gate skip structurally invalid transactions (e.g. those
// Stateless gate \u{2014} skip structurally invalid transactions (e.g. those
// whose public key does not match the declared sender).
let validated_tx = match tx.transaction_stateless_check() {
Ok(v) => v,
Err(_) => return Ok(()),
};
let Ok(validated_tx) = tx.transaction_stateless_check() else { return Ok(()) };
// First application may fail for state-level reasons (e.g. sender
// First application \u{2014} may fail for state-level reasons (e.g. sender
// has insufficient balance, wrong nonce). In that case there is
// nothing to replay.
let first_result = validated_tx.execute_check_on_state(&mut state, 1, 0);
if let Ok(validated_tx) = first_result {
if let Ok(applied_tx) = first_result {
// Use the shared framework function. assert_replay_rejection uses
// assert!() rather than prop_assert!(); for structured proptest
// inputs the framework-level panic is equivalent.
super::assert_replay_rejection(validated_tx, &mut state, 2, 1);
super::assert_replay_rejection(applied_tx, &mut state, 2, 1);
}
}
}

View File

@ -1,6 +1,65 @@
//! Fuzzing property library: invariant framework + input generators.
#![allow(clippy::missing_docs_in_private_items)]
#![allow(
clippy::missing_docs_in_private_items,
reason = "fuzz/test library; internal docs omitted for brevity"
)]
#![allow(
clippy::single_char_lifetime_names,
reason = "the `Arbitrary` trait uses `'a` and our impls must match its signature"
)]
#![allow(
clippy::exhaustive_structs,
reason = "fuzz-library newtype wrappers and test helpers; non_exhaustive would only add noise"
)]
#![allow(
clippy::missing_inline_in_public_items,
reason = "fuzz/test library; inlining hints have negligible effect here"
)]
#![allow(
clippy::question_mark_used,
reason = "`?` is the idiomatic Rust error-propagation operator in `Arbitrary` implementations"
)]
#![allow(
clippy::as_conversions,
reason = "u8 → usize for index arithmetic is safe and bounded in arbitrary contexts"
)]
#![allow(
clippy::integer_division_remainder_used,
reason = "modulo is the natural way to bound arbitrary u8 values to a range"
)]
#![allow(
clippy::arbitrary_source_item_ordering,
reason = "items are grouped logically rather than alphabetically for readability"
)]
#![allow(
clippy::iter_over_hash_type,
reason = "invariant checks iterate over all accounts; iteration order does not affect correctness"
)]
#![allow(
clippy::arithmetic_side_effects,
reason = "arithmetic is bounded by construction in test/fuzz helpers"
)]
#![allow(
clippy::integer_division,
reason = "u128::MAX / 2 is intentional for generating overflow-inducing test values"
)]
#![allow(
clippy::module_name_repetitions,
reason = "assert_invariants is the canonical, self-documenting name for this function"
)]
#![allow(
clippy::unused_trait_names,
reason = "named `Arbitrary` import needed to disambiguate from `proptest::arbitrary::Arbitrary` in generators.rs"
)]
#![allow(
clippy::let_underscore_must_use,
reason = "seed-generation IO errors are intentionally ignored in tests"
)]
#![allow(
clippy::let_underscore_untyped,
reason = "seed-generation IO errors are intentionally ignored in tests"
)]
pub mod arbitrary_types;
pub mod generators;
@ -51,9 +110,9 @@ mod seed_gen {
for rel in &targets {
let p = workspace_root.join(rel);
if let Some(parent) = p.parent() {
fs::create_dir_all(parent).ok();
let _ = fs::create_dir_all(parent);
}
fs::write(&p, &bytes).ok();
let _ = fs::write(&p, &bytes);
}
}
}