Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
01b3dc0
Split proptest and fuzz workflows into separate files
shahan-khatchadourian-anchorage Mar 14, 2026
37126d9
Add reusable post-failure-comment action tagging @copilot
shahan-khatchadourian-anchorage Mar 14, 2026
ff591ec
Allow triggering CI on any PR via 'ci' label
shahan-khatchadourian-anchorage Mar 24, 2026
53d07fe
Fix fuzz build and simplify CI workflows
shahan-khatchadourian-anchorage Mar 25, 2026
4d9e954
Add failure labels to fuzz and proptest workflows
shahan-khatchadourian-anchorage Mar 25, 2026
8eb03cd
Harden label steps in fuzz and proptest workflows
shahan-khatchadourian-anchorage Mar 25, 2026
303064d
Address Copilot review: pin nightly, lock cargo-fuzz, fix cache key
shahan-khatchadourian-anchorage Mar 25, 2026
e16444e
Address code review: Default::default(), env var for PR number
shahan-khatchadourian-anchorage Mar 25, 2026
82e2439
test: add real-IDL structural validation tests
shahan-khatchadourian-anchorage Mar 26, 2026
cdaf441
Address code review: Solana-scope workflows, pin toolchain, extract n…
shahan-khatchadourian-anchorage Apr 1, 2026
4d09685
ci: add parser_cli to StageX build matrix
shahan-khatchadourian-anchorage Apr 2, 2026
05d335f
ci: label parser_cli build as Solana-specific in stagex matrix
shahan-khatchadourian-anchorage Apr 10, 2026
f994ed1
Address code review: use short Idl type, fix doc comment, require dis…
shahan-khatchadourian-anchorage Apr 10, 2026
2bf5b2c
ci: label-triggered stagex builds and cache/credential fixes
shahan-khatchadourian-anchorage Apr 11, 2026
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
1 change: 1 addition & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0
with:
components: clippy, rustfmt
cache: false

- name: Cache Rust dependencies
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
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: 11 additions & 7 deletions .github/workflows/stagex.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,15 @@ on:
- "images/**"
- "Makefile"
pull_request:
paths:
- ".github/**"
- "src/**"
- "images/**"
- "Makefile"
types: [labeled]
workflow_dispatch: # Allows manual invocation

jobs:
parser:
name: Build parser images
name: Build ${{ matrix.target.label || matrix.target.name }}
if: >-
github.event_name != 'pull_request' ||
github.event.label.name == 'stagex'
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
Expand All @@ -37,6 +36,8 @@ jobs:
matrix:
target:
- name: parser_app
- name: parser_cli
label: "parser_cli (Solana)"
- name: parser_gateway
steps:
- name: Checkout sources
Expand All @@ -47,7 +48,6 @@ jobs:
uses: ./.github/actions/docker-setup
with:
dockerHub: ${{ secrets.DOCKERHUB_API_KEY }}
ghcr: ${{ secrets.GITHUB_TOKEN }}

- name: Build ${{ matrix.target.name }}
shell: bash
Expand All @@ -58,6 +58,10 @@ jobs:
run: |
env -C out/${{ matrix.target.name }} tar -cf - . | docker load

- name: Login to GHCR
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin

- name: Upload to GHCR
run: |
for tag in ${tags}; do
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ out/parser_app/index.json: \
$(shell git ls-files images/parser_app src)
$(call build,parser_app)

out/parser_cli/index.json: \
$(shell git ls-files images/parser_cli src)
$(call build,parser_cli)

out/parser_gateway/index.json: \
$(shell git ls-files images/parser_gateway src)
$(call build,parser_gateway)
Expand Down
28 changes: 28 additions & 0 deletions images/parser_cli/Containerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
FROM stagex/pallet-rust:1.88.0@sha256:b9021d2b75eac64fe8b931d96dde63ef11792e5023cee77c3471ccc34a95a377 AS build

# Rust configuration
ENV RUSTFLAGS='-C target-feature=+crt-static'
ENV CARGOFLAGS='--target x86_64-unknown-linux-musl --no-default-features --locked --release'

# Directory for Rust artifacts
ENV RELEASE_DIR=/src/target/x86_64-unknown-linux-musl/release

# Load Rust sources
ADD src /src
WORKDIR /src/

# pre-fetch all workspace deps; we need them to build with `--network=none` later
RUN cargo fetch

WORKDIR /src/parser/cli
RUN --network=none <<-EOF
set -eu
cargo test ${CARGOFLAGS} --features solana
cargo build ${CARGOFLAGS} --features solana
mkdir -p /rootfs/usr/local/bin
mv ${RELEASE_DIR}/parser_cli /rootfs/usr/local/bin/
EOF

# Use busybox as a base so we can easily cp the pivot binary if needed
FROM stagex/core-busybox:1.36.1@sha256:cac5d773db1c69b832d022c469ccf5f52daf223b91166e6866d42d6983a3b374 AS package
COPY --from=build /rootfs/. .
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 skipped in CI (decode failures are logged to stderr).
pub fn load_idl_from_env() -> Option<(String, 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
88 changes: 88 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,88 @@
//! 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 {
let disc = inst
.discriminator
.as_ref()
.unwrap_or_else(|| panic!("instruction '{}' has no discriminator", inst.name));
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