name: zap-audit

# LP-023 — audit gates for zap_native invariants.
#
# Job 1 (R6-6): AddressList.At() must remain non-production until
# owner-model migration / executor-side equivalence is proven. This
# workflow fails the PR if any new production caller is introduced.
#
# Job 2 (R7V8): Every tx type that EMBEDS a ChainsList in its per-tx
# Verify() body MUST call .MustVerify() on it. The receiver-name rename
# (Verify → MustVerify, batch 5 v3.6) makes the gate grep-able. Without
# this gate, a future Verify() author can silently skip the per-entry
# FxIDsLen + reserved-bytes walk and ship a wire-fork primitive.
#
# Allowed callers (Job 1):
#   - *_test.go         — tests
#   - tx_verify.go      — executor-side SyntacticVerify boundary
#   - owner.go          — the type's own implementation
#   - audit_test.go     — the mirroring local test that reproduces this gate
#
# To LEGITIMATELY introduce a production caller (Job 1):
#   1. Land the owner-model migration that retires AddressList.At() OR
#   2. Add the caller's file to the allowlist below AND document the
#      equivalence proof in vms/platformvm/txs/zap_native/owner.go.
#
# Local reproduction:
#   cd vms/platformvm/txs/zap_native && go test -run TestAuditGate -v

on:
  pull_request:
    paths:
      - "vms/platformvm/txs/zap_native/**"
      - ".github/workflows/zap-audit.yml"
  push:
    branches: [main, dev]
    paths:
      - "vms/platformvm/txs/zap_native/**"
      - ".github/workflows/zap-audit.yml"

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  addresslist-production-gate:
    name: AddressList.At() — no production consumers
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Grep for production consumers
        shell: bash
        run: |
          set -euo pipefail
          hits=$(grep -rn 'AddressList\.At(' --include='*.go' vms/platformvm/txs/zap_native/ \
            | grep -vE '(_test\.go|tx_verify\.go|owner\.go)' \
            || true)
          if [ -n "$hits" ]; then
            echo "ERROR: New production consumer(s) of AddressList.At() found."
            echo "AddressList is forward-looking — see"
            echo "  vms/platformvm/txs/zap_native/owner.go"
            echo "for the consumer-safety contract."
            echo ""
            echo "If you have a legitimate production use, either:"
            echo "  1. Land the owner-model migration that retires AddressList.At, OR"
            echo "  2. Add the file to the allowlist in"
            echo "     .github/workflows/zap-audit.yml AND"
            echo "     vms/platformvm/txs/zap_native/audit_test.go"
            echo "     with the equivalence proof recorded in owner.go."
            echo ""
            echo "Offenders:"
            echo "$hits"
            exit 1
          fi
          echo "Audit clean: zero AddressList.At() production consumers"

  chainslist-verify-gate:
    name: ChainsList embedders — MustVerify() required
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Ensure ChainsList embedders call MustVerify()
        shell: bash
        run: |
          set -euo pipefail
          cd vms/platformvm/txs/zap_native
          # Step 1: enumerate tx types that EMBED ChainsList. A tx type
          # T is an embedder if it has an accessor like
          #   func (t T) Chains() ChainsListView
          # defined in a production file.
          embedders=$(grep -rEoh 'func \(t [A-Z][A-Za-z0-9_]+\) Chains\(\) (ChainsList|ChainsListView|BoundChainsList)' \
            --include='*.go' . \
            | grep -vE '(_test\.go)' \
            | sed -E 's/^func \(t ([A-Za-z0-9_]+)\) Chains.*/\1/' \
            | sort -u || true)
          if [ -z "$embedders" ]; then
            echo "Audit clean: zero ChainsList embedder tx types"
            exit 0
          fi
          # Step 2: for each embedder T, confirm tx_verify.go has a
          # Verify() method for T AND calls .MustVerify(). Centralized
          # in tx_verify.go by convention; embedder file (type
          # definition) and Verify file (gate body) are decoupled.
          offenders=""
          for T in $embedders; do
            if grep -qE "func \([a-z]+ ${T}\) Verify\(\)" tx_verify.go; then
              if ! grep -q '\.MustVerify(' tx_verify.go; then
                offenders="${offenders}\n${T} (tx_verify.go has Verify() for ${T} but no .MustVerify() call)"
              fi
            fi
          done
          if [ -n "$offenders" ]; then
            echo "ERROR: ChainsList embedder tx type(s) missing .MustVerify() call."
            echo "Every tx type T with a Chains() accessor AND a Verify()"
            echo "method MUST call list.MustVerify() inside that Verify()"
            echo "to enforce FxIDsLen + reserved-bytes invariants"
            echo "(R6-4 / R6V5 / R7V8)."
            echo ""
            echo "Either wire the gate or document why it's safe to skip"
            echo "inline with a clear comment."
            echo ""
            echo -e "Offenders:${offenders}"
            exit 1
          fi
          echo "Audit clean: all ChainsList embedders call MustVerify()"

  validatorslist-mustverify-gate:
    name: ValidatorsList embedders — MustVerify() required
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Ensure ValidatorsList embedders call MustVerify()
        shell: bash
        run: |
          set -euo pipefail
          cd vms/platformvm/txs/zap_native
          # Step 1: enumerate tx types that EMBED ValidatorsList. A tx
          # type T is an embedder if it has an accessor like
          #   func (t T) Validators() ValidatorsList
          # defined in a production file.
          embedders=$(grep -rEoh 'func \(t [A-Z][A-Za-z0-9_]+\) Validators\(\) ValidatorsList' \
            --include='*.go' . \
            | grep -vE '(_test\.go)' \
            | sed -E 's/^func \(t ([A-Za-z0-9_]+)\) Validators.*/\1/' \
            | sort -u || true)
          if [ -z "$embedders" ]; then
            echo "Audit clean: zero ValidatorsList embedder tx types"
            exit 0
          fi
          # Step 2: for each embedder T, locate the bounded Verify()
          # body in tx_verify.go and confirm it contains a MustVerify(
          # token. Body delimited by matching braces from the opening
          # `{` of the function header.
          offenders=""
          for T in $embedders; do
            if ! grep -qE "func \(t ${T}\) Verify\(\) error \{" tx_verify.go; then
              # No Verify() — covered by other audits; skip here.
              continue
            fi
            body=$(awk -v T="${T}" '
              BEGIN { in_fn=0; depth=0 }
              {
                if (in_fn == 0) {
                  if ($0 ~ ("func \\(t " T "\\) Verify\\(\\) error \\{$")) {
                    in_fn=1
                    depth=1
                    next
                  }
                } else {
                  for (i=1; i<=length($0); i++) {
                    c = substr($0,i,1)
                    if (c == "{") depth++
                    else if (c == "}") {
                      depth--
                      if (depth == 0) { exit }
                    }
                  }
                  print $0
                }
              }
            ' tx_verify.go)
            if ! echo "$body" | grep -q '\.MustVerify('; then
              offenders="${offenders}\n${T} (Verify() body does not call validators.MustVerify())"
            fi
          done
          if [ -n "$offenders" ]; then
            echo "ERROR: ValidatorsList embedder tx type(s) missing .MustVerify() call."
            echo "Every tx type T with a Validators() accessor AND a Verify()"
            echo "method MUST call list.MustVerify() inside that Verify()"
            echo "to enforce the 5 structural floor invariants"
            echo "(cap, weight, BLS-non-zero, expiry-non-zero)."
            echo ""
            echo -e "Offenders:${offenders}"
            exit 1
          fi
          echo "Audit clean: all ValidatorsList embedders call MustVerify()"

  owner-bearing-syntacticverify-gate:
    name: Owner-bearing tx — SyntacticVerify() required
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Ensure Owner-bearing tx types call SyntacticVerify()
        shell: bash
        run: |
          set -euo pipefail
          cd vms/platformvm/txs/zap_native
          # Step 1: enumerate Owner-bearing tx types. Match any of the
          # named owner accessors that return the (uint32, uint64,
          # ids.ShortID) tuple. TransferChainOwnershipTx uses split
          # accessors (OwnerThreshold/Locktime/Address) so a separate
          # check picks it up.
          embedders=$(grep -rEoh 'func \(t [A-Z][A-Za-z0-9_]+\) (Owner|RewardsOwner|ValidationRewardsOwner|DelegationRewardsOwner)\(\) \(uint32, uint64, ids\.ShortID\)' \
            --include='*.go' . \
            | grep -vE '(_test\.go)' \
            | sed -E 's/^func \(t ([A-Za-z0-9_]+)\).*/\1/' \
            | sort -u || true)
          if grep -qE 'func \(t TransferChainOwnershipTx\) (OwnerThreshold|OwnerLocktime|OwnerAddress)\(\)' \
              --include='*.go' -r . ; then
            embedders="${embedders}
TransferChainOwnershipTx"
          fi
          embedders=$(echo "${embedders}" | sort -u | grep -v '^$' || true)
          if [ -z "$embedders" ]; then
            echo "Audit clean: zero Owner-bearing tx types"
            exit 0
          fi
          # Step 2: for each embedder T, locate the bounded Verify() body
          # in tx_verify.go and confirm it contains a SyntacticVerify
          # token. The body is delimited by matching braces from the
          # opening `{` of the function header.
          offenders=""
          for T in $embedders; do
            if ! grep -qE "func \(t ${T}\) Verify\(\) error \{" tx_verify.go; then
              offenders="${offenders}\n${T} (no Verify() method in tx_verify.go — Owner consumed without gate)"
              continue
            fi
            # Extract the bounded body via awk with brace depth tracking.
            body=$(awk -v T="${T}" '
              BEGIN { in_fn=0; depth=0 }
              {
                if (in_fn == 0) {
                  if ($0 ~ ("func \\(t " T "\\) Verify\\(\\) error \\{$")) {
                    in_fn=1
                    depth=1
                    next
                  }
                } else {
                  for (i=1; i<=length($0); i++) {
                    c = substr($0,i,1)
                    if (c == "{") depth++
                    else if (c == "}") {
                      depth--
                      if (depth == 0) { exit }
                    }
                  }
                  print $0
                }
              }
            ' tx_verify.go)
            if ! echo "$body" | grep -q 'SyntacticVerify'; then
              offenders="${offenders}\n${T} (Verify() body does not call SyntacticVerify)"
            fi
          done
          if [ -n "$offenders" ]; then
            echo "ERROR: Owner-bearing tx type(s) missing SyntacticVerify call."
            echo "Every tx type T with an Owner accessor MUST call"
            echo "  stubFromTuple(...).SyntacticVerify()  OR"
            echo "  OwnerView(...).SyntacticVerify()"
            echo "inside its per-tx Verify() body."
            echo ""
            echo "Skipping the gate makes a threshold=0 wire-encoded Owner a"
            echo "silent authorization bypass (R4V7)."
            echo ""
            echo -e "Offenders:${offenders}"
            exit 1
          fi
          echo "Audit clean: all Owner-bearing tx types call SyntacticVerify()"
