Skip to content

use canonical version in interface metadata test #746

use canonical version in interface metadata test

use canonical version in interface metadata test #746

Workflow file for this run

name: Contract CI
on:
push:
branches: [main]
pull_request:
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
jobs:
deploy-script:
name: Deployment script checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run deploy.sh manifest and drift tests
run: bash tests/deploy/test_deploy.sh
fmt:
name: Format check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.81.0"
components: rustfmt
- uses: Swatinem/rust-cache@v2
- run: cargo fmt --all -- --check
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.81.0"
components: clippy
- uses: Swatinem/rust-cache@v2
- run: cargo clippy --all-targets -- -D warnings
contract-build-lints:
name: Contract-only dead-code check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.81.0"
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- run: tools/check_contract_build_lints.sh
test:
name: Unit tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.81.0"
- uses: Swatinem/rust-cache@v2
- run: cargo test --workspace
production-acceptance:
name: Production acceptance report
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.81.0"
components: rustfmt, clippy
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- name: Run production acceptance suite
run: scripts/production-acceptance.sh --output target/production-readiness-report.md
- name: Upload production readiness report
uses: actions/upload-artifact@v4
with:
name: production-readiness-report
path: target/production-readiness-report.md
retention-days: 14
error-discriminants:
name: Error discriminant stability
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check Error enum discriminants are append-only
run: tools/check_error_discriminants.sh "${{ github.event.pull_request.base.sha }}" HEAD
build:
name: WASM build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.81.0"
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- run: cargo build --target wasm32-unknown-unknown --release -p ledgerlens-score --locked
- name: Record WASM byte-size evidence
run: |
mkdir -p target
{
echo "# WASM Size Evidence"
echo ""
echo "Path: target/wasm32-unknown-unknown/release/ledgerlens_score.wasm"
echo "Bytes: $(wc -c < target/wasm32-unknown-unknown/release/ledgerlens_score.wasm | tr -d ' ')"
echo "SHA-256: $(sha256sum target/wasm32-unknown-unknown/release/ledgerlens_score.wasm | awk '{print $1}')"
} > target/wasm-size-report.md
- name: Upload wasm artifact
uses: actions/upload-artifact@v4
with:
name: ledgerlens-score-wasm
path: target/wasm32-unknown-unknown/release/ledgerlens_score.wasm
retention-days: 7
- name: Upload WASM size report artifact
uses: actions/upload-artifact@v4
with:
name: ledgerlens-score-wasm-size-report
path: target/wasm-size-report.md
retention-days: 7
deploy-script-tests:
name: Deploy script failure-mode tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: tests/deploy_script_rpc_failures.sh
audit:
name: Security audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
# cargo-audit 0.22 requires Rust 1.88+ and understands the CVSS 4.0
# records now present in the live RustSec advisory database. This job
# only inspects Cargo.lock; contract builds remain pinned to Rust 1.81.
toolchain: "1.88.0"
- uses: Swatinem/rust-cache@v2
- name: Install cargo-audit
run: cargo +1.88.0 install cargo-audit --version 0.22.2 --locked
- name: Run security audit
# RUSTSEC-2026-0009 affects time's RFC parsing. time is only pulled in
# through serde_with -> stellar-xdr; the contract does not invoke that
# parsing path. The fixed time release requires Edition 2024 and cannot
# be parsed by the Rust 1.81 toolchain required for deployable Soroban
# WASM. Keep this exception narrow and remove it when Soroban permits a
# newer release toolchain.
run: cargo +1.88.0 audit --ignore RUSTSEC-2026-0009
# ── Supply-chain integrity checks ───────────────────────────────────────────
#
# Verifies that:
# 1. Every crate in Cargo.lock originates from a pinned, expected registry
# (crates.io or git with an exact rev) — no path-only or unpinned
# sources that could silently change between runs.
# 2. The release WASM artifact is signed with a detached SHA-256 manifest
# so downstream consumers can verify provenance.
# 3. The toolchain pin in rust-toolchain.toml matches what CI actually uses.
#
# Failure modes that cause this job to fail (acceptance criteria):
# • Any Cargo.lock entry whose `source` field is absent (local path crates
# that have been accidentally published) or points to an unexpected registry.
# • The WASM artifact SHA-256 in the signed manifest does not match the
# freshly built binary.
# • The active rustc version does not match rust-toolchain.toml.
#
# See docs/supply-chain.md for the full policy and manual audit procedure.
# ─────────────────────────────────────────────────────────────────────────────
supply-chain:
name: Supply-chain integrity
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@fcf085fcb917fba9e37c9eaa60e2e5dc4d0cd05f # 1.81.0
with:
toolchain: "1.81.0"
targets: wasm32-unknown-unknown
# ── Step 1: Verify the active toolchain matches rust-toolchain.toml ──────
- name: Verify toolchain pin
run: |
EXPECTED=$(grep '^channel' rust-toolchain.toml | sed 's/.*= *"\(.*\)"/\1/')
ACTUAL=$(rustc --version | awk '{print $2}')
echo "rust-toolchain.toml channel : $EXPECTED"
echo "Active rustc version : $ACTUAL"
if [ "$EXPECTED" != "$ACTUAL" ]; then
echo "ERROR: toolchain mismatch — CI used $ACTUAL but rust-toolchain.toml pins $EXPECTED"
exit 1
fi
echo "OK: toolchain pin verified."
# ── Step 2: Verify Cargo.lock registry sources ────────────────────────────
# Each non-workspace crate in Cargo.lock must declare a `source` that is
# either:
# (a) "registry+https://github.com/rust-lang/crates.io-index"
# (b) a git source with an explicit `rev` lock
# Path-only entries (workspace members) are explicitly allowed.
# Any entry from an unknown registry causes the job to fail.
- name: Verify Cargo.lock registry sources
run: |
python3 - <<'EOF'
import re, sys
with open("Cargo.lock") as f:
content = f.read()
ALLOWED_REGISTRY = "registry+https://github.com/rust-lang/crates.io-index"
# Parse [[package]] blocks
blocks = re.split(r'\n\[\[package\]\]', content)
violations = []
for block in blocks:
name_m = re.search(r'name\s*=\s*"([^"]+)"', block)
src_m = re.search(r'source\s*=\s*"([^"]+)"', block)
if not name_m:
continue
name = name_m.group(1)
if not src_m:
# No source = workspace path crate; allowed.
continue
src = src_m.group(1)
if src == ALLOWED_REGISTRY:
continue
if src.startswith("git+") and "rev=" in src:
continue # pinned git source
violations.append(f" {name}: {src}")
if violations:
print("ERROR: Cargo.lock contains unexpected or unpinned registry sources:")
for v in violations:
print(v)
print("")
print("Only crates.io and git sources with a pinned rev are permitted.")
print("See docs/supply-chain.md for the policy.")
sys.exit(1)
print(f"OK: all Cargo.lock registry sources verified.")
EOF
# ── Step 3: Build WASM and produce a signed SHA-256 manifest ─────────────
- name: Build release WASM
run: cargo build --target wasm32-unknown-unknown --release -p ledgerlens-score --locked
- name: Sign WASM artifact (SHA-256 manifest)
run: |
WASM=target/wasm32-unknown-unknown/release/ledgerlens_score.wasm
MANIFEST=target/ledgerlens_score.wasm.sha256
sha256sum "$WASM" > "$MANIFEST"
echo "Manifest written:"
cat "$MANIFEST"
- name: Verify WASM artifact against manifest
run: |
MANIFEST=target/ledgerlens_score.wasm.sha256
sha256sum --check "$MANIFEST"
echo "OK: WASM artifact integrity verified."
- name: Upload WASM SHA-256 manifest
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.1
with:
name: ledgerlens-score-wasm-sha256-manifest
path: target/ledgerlens_score.wasm.sha256
retention-days: 90
#
# Builds the release WASM twice in two completely independent containers and
# asserts the resulting files are byte-identical by comparing their SHA-256
# digests. A mismatch means something in the toolchain or build inputs is
# non-deterministic — the job fails and surfaces the differing hashes.
#
# Both jobs must complete before the comparison step, so they run in parallel
# first and the compare step depends on both via `needs`.
#
# See docs/reproducible-builds.md for the local verification procedure.
# ─────────────────────────────────────────────────────────────────────────────
repro-build-1:
name: Reproducible build (run 1)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.81.0"
targets: wasm32-unknown-unknown
# Deliberately no Swatinem/rust-cache — each run must be a cold,
# independent build to prove there is no cache-based non-determinism.
- name: Build WASM (run 1)
run: cargo build --target wasm32-unknown-unknown --release -p ledgerlens-score --locked
- name: Compute SHA-256 (run 1)
id: hash1
run: |
SHA=$(sha256sum target/wasm32-unknown-unknown/release/ledgerlens_score.wasm | awk '{print $1}')
echo "sha256=$SHA" >> "$GITHUB_OUTPUT"
echo "Run 1 SHA-256: $SHA"
- name: Upload WASM artifact (run 1)
uses: actions/upload-artifact@v4
with:
name: wasm-run-1
path: target/wasm32-unknown-unknown/release/ledgerlens_score.wasm
retention-days: 1
repro-build-2:
name: Reproducible build (run 2)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.81.0"
targets: wasm32-unknown-unknown
# No cache — independent cold build, same as run 1.
- name: Build WASM (run 2)
run: cargo build --target wasm32-unknown-unknown --release -p ledgerlens-score --locked
- name: Compute SHA-256 (run 2)
id: hash2
run: |
SHA=$(sha256sum target/wasm32-unknown-unknown/release/ledgerlens_score.wasm | awk '{print $1}')
echo "sha256=$SHA" >> "$GITHUB_OUTPUT"
echo "Run 2 SHA-256: $SHA"
- name: Upload WASM artifact (run 2)
uses: actions/upload-artifact@v4
with:
name: wasm-run-2
path: target/wasm32-unknown-unknown/release/ledgerlens_score.wasm
retention-days: 1
repro-verify:
name: Verify WASM reproducibility
needs: [repro-build-1, repro-build-2]
runs-on: ubuntu-latest
steps:
- name: Download WASM artifacts
uses: actions/download-artifact@v4
with:
path: wasm-artifacts
- name: Compare SHA-256 digests
run: |
SHA1=$(sha256sum wasm-artifacts/wasm-run-1/ledgerlens_score.wasm | awk '{print $1}')
SHA2=$(sha256sum wasm-artifacts/wasm-run-2/ledgerlens_score.wasm | awk '{print $1}')
echo "Run 1 SHA-256: $SHA1"
echo "Run 2 SHA-256: $SHA2"
if [ "$SHA1" != "$SHA2" ]; then
echo ""
echo "ERROR: WASM output is NOT reproducible."
echo " Run 1: $SHA1"
echo " Run 2: $SHA2"
echo ""
echo "The two independent builds produced different bytecode."
echo "Check for non-deterministic build inputs (timestamps, paths,"
echo "env vars, or codegen flags). See docs/reproducible-builds.md."
exit 1
fi
echo ""
echo "SUCCESS: Both builds produced identical WASM output."
echo " SHA-256: $SHA1"