Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions .github/workflows/fuzz.yml → .github/workflows/fuzz-solana.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
name: Fuzz Testing
name: "Fuzz Testing: Solana"

on:
pull_request:
types: [opened, synchronize, reopened, labeled]

env:
NIGHTLY_VERSION: nightly-2026-03-13

jobs:
fuzz:
if: contains(github.event.pull_request.labels.*.name, 'fuzz')
Expand All @@ -18,7 +21,7 @@ jobs:
- name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0
with:
toolchain: nightly-2026-03-13
toolchain: ${{ env.NIGHTLY_VERSION }}
- name: Install cargo-fuzz
run: cargo install cargo-fuzz --locked
- name: Cache Rust dependencies
Expand Down Expand Up @@ -50,12 +53,12 @@ jobs:
- name: Fuzz fuzz_transaction_string (30s)
id: fuzz_transaction_string
continue-on-error: true
run: cargo +nightly-2026-03-13 fuzz run fuzz_transaction_string -- -max_total_time=30
run: cargo +${{ env.NIGHTLY_VERSION }} fuzz run fuzz_transaction_string -- -max_total_time=30
working-directory: src/chain_parsers/visualsign-solana/fuzz
- name: Fuzz fuzz_versioned_transaction (30s)
id: fuzz_versioned_transaction
continue-on-error: true
run: cargo +nightly-2026-03-13 fuzz run fuzz_versioned_transaction -- -max_total_time=30
run: cargo +${{ env.NIGHTLY_VERSION }} fuzz run fuzz_versioned_transaction -- -max_total_time=30
working-directory: src/chain_parsers/visualsign-solana/fuzz
- name: Label PR on fuzz failure
env:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Property Tests
name: "Property Tests: Solana"

on:
pull_request:
Expand Down
18 changes: 18 additions & 0 deletions src/chain_parsers/visualsign-solana/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,21 @@ pub fn find_text(fields: &[AnnotatedPayloadField], label: &str) -> Option<String
None
})
}

// ── IDL loading helpers ───────────────────────────────────────────────────────

/// Load a real IDL from the path in the `IDL_FILE` environment variable.
///
/// Returns `None` when `IDL_FILE` is unset or the IDL fails validation,
/// allowing `real_idl_*` tests to be silently skipped in CI.
pub fn load_idl_from_env() -> Option<(String, solana_parser::solana::structs::Idl)> {
let path = std::env::var("IDL_FILE").ok()?;
let json = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("IDL_FILE={path}: {e}"));
match decode_idl_data(&json) {
Ok(idl) => Some((json, idl)),
Err(e) => {
eprintln!("IDL_FILE={path}: skipping — decode failed: {e}");
None
}
}
}
16 changes: 3 additions & 13 deletions src/chain_parsers/visualsign-solana/tests/fuzz_idl_parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ use solana_parser::{decode_idl_data, parse_instruction_with_idl};
use solana_parser_fuzz_core::proptest as arb;
use std::sync::Arc;

mod common;

// parse_instruction_with_idl ignores the program_id parameter (_program_id);
// use an obviously fake value to avoid confusion with real known programs.
const TEST_PROGRAM_ID: &str = "deadbeef1234deadbeef5678deadbeef";
Expand Down Expand Up @@ -849,19 +851,7 @@ fn size_guard_vec_u64_over_budget() {
//
// See scripts/fuzz_all_idls.sh to run against all embedded IDLs in one pass.

fn load_idl_from_env() -> Option<(String, solana_parser::solana::structs::Idl)> {
let path = std::env::var("IDL_FILE").ok()?;
let json = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("IDL_FILE={path}: {e}"));
match decode_idl_data(&json) {
Ok(idl) => Some((json, idl)),
Err(e) => {
// IDL failed validation (e.g. duplicate type names, cyclic references).
// Skip these tests — they are not valid inputs for real_idl_* tests.
eprintln!("IDL_FILE={path}: skipping — decode failed: {e}");
None
}
}
}
use common::load_idl_from_env;

/// Crash-safety test against a real IDL loaded from IDL_FILE.
///
Expand Down
86 changes: 86 additions & 0 deletions src/chain_parsers/visualsign-solana/tests/real_idl_validation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//! Structural invariant tests for real production IDLs.
//!
//! These tests assert properties of the decoded IDL structure itself —
//! no random input generation, no proptest. They verify that `decode_idl_data`
//! produces well-formed IDLs from production JSON files.
//!
//! Run: `IDL_FILE=/path/to/idl.json cargo test --test real_idl_validation`
//! All IDLs: `scripts/fuzz_all_idls.sh`

mod common;

use common::load_idl_from_env;

/// Every instruction in the decoded IDL must have a discriminator computed by
/// decode_idl_data (either provided explicitly or derived via Anchor's SHA256
/// scheme). A missing discriminator means the instruction is unreachable.
#[test]
fn real_idl_all_instructions_have_discriminators() {
let Some((_, idl)) = load_idl_from_env() else {
return;
};
for inst in &idl.instructions {
let disc = inst
.discriminator
.as_ref()
.unwrap_or_else(|| panic!("instruction '{}' has no discriminator", inst.name));
assert_eq!(
disc.len(),
8,
"instruction '{}' discriminator must be 8 bytes, got {}",
inst.name,
disc.len()
);
}
}

/// No two instructions in the IDL may share a discriminator — a collision would
/// make them indistinguishable at parse time.
#[test]
fn real_idl_discriminators_are_unique() {
let Some((_, idl)) = load_idl_from_env() else {
return;
};
let mut seen: std::collections::HashMap<Vec<u8>, &str> = std::collections::HashMap::new();
for inst in &idl.instructions {
if let Some(disc) = &inst.discriminator {
if let Some(existing) = seen.get(disc) {
panic!(
"discriminator collision between '{}' and '{}': {:?}",
existing, inst.name, disc
);
}
seen.insert(disc.clone(), &inst.name);
}
}
}

/// No two instructions may share a name — duplicate names make dispatch results
/// ambiguous and hint at an IDL construction error.
#[test]
fn real_idl_instruction_names_are_unique() {
let Some((_, idl)) = load_idl_from_env() else {
return;
};
let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
for inst in &idl.instructions {
assert!(
seen.insert(inst.name.as_str()),
"duplicate instruction name: '{}'",
inst.name
);
}
}

/// compute_idl_hash must be deterministic — the same JSON must produce the
/// same hash on every call.
#[test]
fn real_idl_idl_hash_is_stable() {
let Some((json, _)) = load_idl_from_env() else {
return;
};
let h1 = solana_parser::compute_idl_hash(&json);
let h2 = solana_parser::compute_idl_hash(&json);
assert_eq!(h1, h2, "IDL hash must be deterministic");
assert!(!h1.is_empty(), "IDL hash must not be empty");
}
2 changes: 1 addition & 1 deletion src/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[toolchain]
channel = "1.88"
channel = "1.88.0"
components = [ "rustfmt", "cargo", "clippy" ]
profile = "minimal"
Loading