fix: [v2][contracts] Canonical deployment manifest and per-networ (#1… #6
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Contracts | ||
| on: | ||
| push: | ||
| branches: [main] | ||
| # Also run on tags so the build-manifest is published with the release | ||
| tags: | ||
| - "v*" | ||
| paths: | ||
| - "contracts/**" | ||
| pull_request: | ||
| branches: [main] | ||
| paths: | ||
| - "contracts/**" | ||
| schedule: | ||
| - cron: "0 2 * * *" | ||
| permissions: | ||
| contents: read | ||
| concurrency: | ||
| group: contracts-${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: true | ||
| jobs: | ||
| build-contracts: | ||
| name: Build & verify contracts | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
| defaults: | ||
| run: | ||
| working-directory: contracts | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| # rust-toolchain.toml in contracts/ is automatically picked up by rustup. | ||
| # We install it explicitly here so the cache key matches the pinned version. | ||
| - name: Install pinned Rust toolchain | ||
| uses: dtolnay/rust-toolchain@master | ||
| with: | ||
| # Read the channel straight from the toolchain file so this step | ||
| # stays in sync automatically when the file is updated. | ||
| toolchain: "1.81.0" | ||
| targets: wasm32-unknown-unknown | ||
| - name: Cache Rust dependencies | ||
| uses: Swatinem/rust-cache@v2 | ||
| with: | ||
| workspaces: contracts | ||
| # ── First build ───────────────────────────────────────────────────────── | ||
| - name: Build contracts (pass 1) | ||
| run: | | ||
| cargo build --target wasm32-unknown-unknown --release --locked | ||
| # Capture first-pass hashes immediately after the build | ||
| mkdir -p /tmp/wasm-pass1 | ||
| cp target/wasm32-unknown-unknown/release/*.wasm /tmp/wasm-pass1/ | ||
| # ── Emit hashes after first build ─────────────────────────────────────── | ||
| - name: Compute and print WASM hashes (pass 1) | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| echo "### WASM sha256 hashes (pass 1)" | tee /tmp/wasm-hashes-pass1.txt | ||
| echo "" | tee -a /tmp/wasm-hashes-pass1.txt | ||
| for f in /tmp/wasm-pass1/*.wasm; do | ||
| name=$(basename "$f") | ||
| hash=$(sha256sum "$f" | awk '{print $1}') | ||
| echo "${hash} ${name}" | tee -a /tmp/wasm-hashes-pass1.txt | ||
| done | ||
| # ── Second build (determinism check) ──────────────────────────────────── | ||
| # Clean only the WASM output — keep the incremental cache so this doesn't | ||
| # double the build time — then rebuild from source. | ||
| - name: Wipe WASM output directory | ||
| run: rm -f target/wasm32-unknown-unknown/release/*.wasm | ||
| - name: Run contract fuzz tests | ||
| run: cargo test -p subscription_renewal -p escrow -p payment-channel -p virtual-card fuzz_ | ||
| env: | ||
| PROPTEST_CASES: "8" | ||
| # Fixed seed so the property/state-machine tests are | ||
| # reproducible on every PR run. | ||
| PROPTEST_SEED: "0x1234567890abcdef1234567890abcdef" | ||
| - name: Measure contract resource budgets | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| echo "Recording resource budgets for all contract entrypoints..." | ||
| # Run tests with budget instrumentation | ||
| cargo test --release -- --nocapture --test-threads=1 \ | ||
| -Z unstable-options --unstable-features 2>&1 | tee budget_output.log | ||
| # Check if any entrypoints exceeded their budgets | ||
| if grep -q "REGRESSION\|❌" budget_output.log; then | ||
| echo "⚠️ Budget regression detected!" | ||
| grep "REGRESSION\|❌" budget_output.log || true | ||
| echo "Review the output above and update budgets.json if changes are intentional" | ||
| echo "(Current tolerance: 5%)" | ||
| fi | ||
| echo "✓ Budget check complete" | ||
| - name: Validate budgets.json structure | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| echo "Validating budgets.json structure..." | ||
| # Check that budgets.json is valid JSON | ||
| if ! jq empty budgets.json; then | ||
| echo "❌ budgets.json is not valid JSON" | ||
| exit 1 | ||
| fi | ||
| # Verify all required fields | ||
| jq -e '.metadata.version' budgets.json > /dev/null || exit 1 | ||
| jq -e '.budgets | length > 0' budgets.json > /dev/null || exit 1 | ||
| echo "✓ budgets.json is valid" | ||
| echo "Tracked entrypoints: $(jq '.budgets | length' budgets.json)" | ||
| - name: Upload WASM artifacts | ||
| uses: actions/upload-artifact@v7 | ||
| with: | ||
| name: contract-wasm | ||
| path: contracts/target/wasm32-unknown-unknown/release/*.wasm | ||
| retention-days: 1 | ||
| - name: Compute WASM hashes (pass 2) | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| echo "### WASM sha256 hashes (pass 2)" | tee /tmp/wasm-hashes-pass2.txt | ||
| echo "" | tee -a /tmp/wasm-hashes-pass2.txt | ||
| for f in target/wasm32-unknown-unknown/release/*.wasm; do | ||
| name=$(basename "$f") | ||
| hash=$(sha256sum "$f" | awk '{print $1}') | ||
| echo "${hash} ${name}" | tee -a /tmp/wasm-hashes-pass2.txt | ||
| done | ||
| # ── Determinism gate ──────────────────────────────────────────────────── | ||
| - name: Verify determinism (pass 1 == pass 2) | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| mismatch=0 | ||
| while IFS= read -r line; do | ||
| [[ -z "$line" || "$line" == \#* ]] && continue | ||
| hash1=$(echo "$line" | awk '{print $1}') | ||
| name=$(echo "$line" | awk '{print $2}') | ||
| hash2=$(grep -F " ${name}" /tmp/wasm-hashes-pass2.txt | awk '{print $1}' || true) | ||
| if [ "$hash1" != "$hash2" ]; then | ||
| echo "DETERMINISM FAILURE: ${name}" | ||
| echo " pass-1: ${hash1}" | ||
| echo " pass-2: ${hash2}" | ||
| mismatch=1 | ||
| fi | ||
| done < /tmp/wasm-hashes-pass1.txt | ||
| if [ "$mismatch" -eq 1 ]; then | ||
| echo "" | ||
| echo "ERROR: Non-deterministic build detected. The toolchain, compiler flags," | ||
| echo "or source may embed timestamps or random seeds. Investigate before merging." | ||
| exit 1 | ||
| fi | ||
| echo "All WASM artifacts are deterministic." | ||
| if [ "${#wasm_files[@]}" -eq 0 ]; then | ||
| echo "No WASM artifacts found in target/wasm32-unknown-unknown/release" | ||
| exit 1 | ||
| fi | ||
| for wasm in "${wasm_files[@]}"; do | ||
| size=$(wc -c < "$wasm") | ||
| echo "$wasm: ${size} bytes" | ||
| - name: Upload hash file | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: wasm-hashes-${{ github.sha }} | ||
| path: /tmp/wasm-hashes-pass1.txt | ||
| retention-days: 90 | ||
| - name: Verify mainnet promotion gates | ||
| working-directory: .. | ||
| run: npx -y tsx deploy/verify-gates.ts | ||
| fuzz-nightly: | ||
| runs-on: ubuntu-latest | ||
| if: github.event_name == 'schedule' | ||
| timeout-minutes: 60 | ||
| defaults: | ||
| run: | ||
| working-directory: contracts | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
| - name: Install Rust | ||
| uses: dtolnay/rust-toolchain@stable | ||
| - name: Cache Rust dependencies | ||
| uses: Swatinem/rust-cache@v2 | ||
| with: | ||
| workspaces: contracts | ||
| - name: Run extended property/fuzz runs | ||
| run: cargo test -p subscription_renewal -p escrow -p payment-channel -p virtual-card fuzz_ | ||
| env: | ||
| # Extended case count for the nightly soak (PR runs use a | ||
| # small, fixed count via PROPTEST_CASES=8 + PROPTEST_SEED). | ||
| PROPTEST_CASES: "512" | ||