ubt 0.4.1

Unified Binary Tree implementation based on EIP-7864
Documentation
name: Fuzzing

on:
  workflow_dispatch:
    inputs:
      target:
        description: 'Fuzz target to run'
        required: true
        default: 'fuzz_tree_ops'
        type: choice
        options:
          - fuzz_tree_ops
          - fuzz_proof_decode
          - fuzz_code_chunk
          - fuzz_basic_data
          - fuzz_streaming
          - fuzz_embedding
          - all
      duration:
        description: 'Duration in seconds (18000 = 5 hours). If target=all, must be >= number of targets.'
        required: true
        default: '18000'
        type: string

jobs:
  fuzz:
    runs-on: ubuntu-latest

    env:
      # Keep in sync with `workflow_dispatch.inputs.target.options`.
      FUZZ_TARGETS: fuzz_tree_ops fuzz_proof_decode fuzz_code_chunk fuzz_basic_data fuzz_streaming fuzz_embedding
    
    steps:
      # Triggered via `workflow_dispatch`. Fuzz failures currently fail the job so crashes and build
      # regressions are obvious; artifact upload steps below use `if: always()` to run on failure.
      - name: Checkout repository
        uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
        with:
          submodules: recursive

      - name: Install Rust nightly
        uses: dtolnay/rust-toolchain@881ba7bf39a41cda34ac9e123fb41b44ed08232f # nightly
        with:
          toolchain: nightly-2026-02-01

      - name: Install cargo-fuzz
        run: cargo +nightly-2026-02-01 install cargo-fuzz --version 0.13.1 --locked --force

      - name: Validate fuzz inputs (single target)
        if: inputs.target != 'all'
        env:
          TARGET: ${{ inputs.target }}
          DURATION: ${{ inputs.duration }}
        run: |
          set -euo pipefail
          set -f

          if ! [[ "$DURATION" =~ ^[0-9]+$ ]]; then
            echo "::error::duration must be an integer number of seconds (got: $DURATION)"
            exit 1
          fi
          DURATION_INT=$((10#$DURATION))
          if (( DURATION_INT < 1 )); then
            echo "::error::duration must be >= 1 second (got: $DURATION_INT)"
            exit 1
          fi

          read -r -a targets <<<"$FUZZ_TARGETS"
          num_targets=${#targets[@]}
          if (( num_targets == 0 )); then
            echo "::error::FUZZ_TARGETS is empty"
            exit 1
          fi
          valid_target=false
          for t in "${targets[@]}"; do
            if [[ "$t" == "$TARGET" ]]; then
              valid_target=true
              break
            fi
          done
          if ! $valid_target; then
            echo "::error::invalid fuzz target: $TARGET"
            exit 1
          fi

          printf '%s=%s\n' "FUZZ_TARGET" "$TARGET" >>"$GITHUB_ENV"
          printf '%s=%s\n' "FUZZ_DURATION_INT" "$DURATION_INT" >>"$GITHUB_ENV"

      - name: Run fuzz (single target)
        if: inputs.target != 'all'
        run: |
          set -euo pipefail

          echo "Fuzzing $FUZZ_TARGET for ${FUZZ_DURATION_INT} seconds"
          cargo +nightly-2026-02-01 fuzz run "$FUZZ_TARGET" -- \
            -max_total_time="$FUZZ_DURATION_INT" \
            -print_final_stats=1

      - name: Validate fuzz inputs (all targets)
        if: inputs.target == 'all'
        env:
          DURATION: ${{ inputs.duration }}
        run: |
          set -euo pipefail
          set -f

          if ! [[ "$DURATION" =~ ^[0-9]+$ ]]; then
            echo "::error::duration must be an integer number of seconds (got: $DURATION)"
            exit 1
          fi
          DURATION_INT=$((10#$DURATION))
          if (( DURATION_INT < 1 )); then
            echo "::error::duration must be >= 1 second (got: $DURATION_INT)"
            exit 1
          fi

          read -r -a targets <<<"$FUZZ_TARGETS"
          num_targets=${#targets[@]}
          if (( num_targets == 0 )); then
            echo "::error::FUZZ_TARGETS is empty"
            exit 1
          fi

          if (( DURATION_INT < num_targets )); then
            echo "::error::duration must be >= ${num_targets} seconds when target=all (got: $DURATION_INT)"
            exit 1
          fi

          DURATION_PER_TARGET=$(( DURATION_INT / num_targets ))
          if (( DURATION_PER_TARGET < 1 )); then
            DURATION_PER_TARGET=1
          fi
          printf '%s=%s\n' "FUZZ_DURATION_PER_TARGET" "$DURATION_PER_TARGET" >>"$GITHUB_ENV"

      - name: Run fuzz (all targets)
        if: inputs.target == 'all'
        run: |
          set -euo pipefail
          set -f

          read -r -a targets <<<"$FUZZ_TARGETS"
          failed=0
          echo "Running all targets, ${FUZZ_DURATION_PER_TARGET}s each"

          for target in "${targets[@]}"; do
            echo "=== Fuzzing $target for ${FUZZ_DURATION_PER_TARGET}s ==="
            if ! cargo +nightly-2026-02-01 fuzz run "$target" -- \
              -max_total_time="$FUZZ_DURATION_PER_TARGET" \
              -print_final_stats=1; then
              echo "::warning::fuzz run failed for target: $target"
              failed=1
            fi
          done

          exit "$failed"

      - name: Upload crash artifacts
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
        if: always()
        with:
          name: fuzz-crashes-${{ inputs.target }}
          path: |
            fuzz/artifacts/
          retention-days: 30
          if-no-files-found: ignore

      - name: Upload corpus
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
        if: always()
        with:
          name: fuzz-corpus-${{ inputs.target }}
          path: |
            fuzz/corpus/
          retention-days: 7
          if-no-files-found: ignore

      - name: Summary
        if: always()
        run: |
          echo "## Fuzzing Summary" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "| Setting | Value |" >> $GITHUB_STEP_SUMMARY
          echo "|---------|-------|" >> $GITHUB_STEP_SUMMARY
          echo "| Target | ${{ inputs.target }} |" >> $GITHUB_STEP_SUMMARY
          echo "| Duration | ${{ inputs.duration }}s |" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          
          if [ -d "fuzz/artifacts" ] && [ "$(ls -A fuzz/artifacts 2>/dev/null)" ]; then
            echo "### [!] Crashes Found" >> $GITHUB_STEP_SUMMARY
            echo "Check the uploaded artifacts for crash inputs." >> $GITHUB_STEP_SUMMARY
            find fuzz/artifacts -type f | head -20 >> $GITHUB_STEP_SUMMARY
          else
            echo "### [OK] No crashes found" >> $GITHUB_STEP_SUMMARY
          fi