scripts/sepolia/sweep.sh111 lines · 5033 bytes
| 1 | #!/usr/bin/env bash |
| 2 | # One sweeper run: the CRE workflow's cron handler (trigger 1), run once. It reads nextId, looks at the newest |
| 3 | # proposal ids (the config's sweep window), and judges the ones that are still Pending without a CRE verdict |
| 4 | # (AirlockAdmin.oracleJudged false): proposals the listener missed because it was down, whose Proposed log was too |
| 5 | # big to deliver, or that the approver cleared by hand (a CRE veto still wins over that clearance). Each judgement |
| 6 | # ends on chain like the listener's (Clear or Veto report). At most one upgrade simulation per run (the CRE HTTP |
| 7 | # budget); the next run takes the next one. |
| 8 | # |
| 9 | # scripts/sepolia/sweep.sh # live Sepolia |
| 10 | # FORK=19631 scripts/sepolia/sweep.sh # local fork |
| 11 | # SWEEP_ALWAYS=1 scripts/sepolia/sweep.sh # run the workflow even when the pre-check finds nothing to judge |
| 12 | # |
| 13 | # systemd/airlock-sweep.timer runs it every 60 s. Before it starts the simulator it checks the same window itself |
| 14 | # (a few eth_calls) and exits at once when no proposal waits for a verdict, so an idle minute costs no simulator run. |
| 15 | # Proposals younger than SWEEP_MIN_AGE blocks (default 3, ~36 s) are left to the listener, which is normally |
| 16 | # judging them at that moment; set SWEEP_MIN_AGE=0 when no listener runs. One sweep at a time (a lock in the state |
| 17 | # dir). Output: stdout and <state dir>/sweep.log (appended, rotated at 5 MB). |
| 18 | # The reports are paid by sweeper.key when KEY_DIR has one (so the sweeper and the listener never race for one |
| 19 | # nonce), else by sender.key like the listener's. |
| 20 | # Env: CRE_SIM, KEY_DIR, SEPOLIA_RPC, AIRLOCK_STATE, WORKFLOW_CONFIG, WASM_CACHE (see airlock.sh). |
| 21 | source "$(dirname "$0")/lib.sh" |
| 22 | [ $# = 0 ] || die "usage: [FORK=<port>] $0" |
| 23 | MIN_AGE=${SWEEP_MIN_AGE:-3} |
| 24 | [[ "$MIN_AGE" =~ ^[0-9]+$ ]] || die "SWEEP_MIN_AGE must be a number of blocks" |
| 25 | |
| 26 | LOCK=$STATE/sweep.lock |
| 27 | if ! mkdir "$LOCK" 2>/dev/null; then |
| 28 | if [ -f "$LOCK/pid" ] && kill -0 "$(cat "$LOCK/pid")" 2>/dev/null; then |
| 29 | say "sweep: another sweep is running (pid $(cat "$LOCK/pid")); skipping this one" |
| 30 | exit 0 |
| 31 | fi |
| 32 | rm -rf "$LOCK" |
| 33 | mkdir "$LOCK" || die "cannot take the sweep lock $LOCK" |
| 34 | fi |
| 35 | echo $$ >"$LOCK/pid" |
| 36 | cleanup() { |
| 37 | if [ -n "${SPID:-}" ]; then kill "$SPID" 2>/dev/null || true; fi |
| 38 | rm -rf "$LOCK" |
| 39 | [ -n "${RUNLOG:-}" ] && rm -f "$RUNLOG" |
| 40 | return 0 |
| 41 | } |
| 42 | trap cleanup EXIT |
| 43 | trap 'exit 130' INT TERM |
| 44 | |
| 45 | check_rpc |
| 46 | GA=$(dep airlockAdmin) |
| 47 | workflow_config |
| 48 | WINDOW=$(jq -r '.sweepWindow // 20' "$CONFIG") |
| 49 | NOW=$(block_now) |
| 50 | NEXT=$(cast call "$GA" 'nextId()(uint256)' --rpc-url "$RPC") |
| 51 | FIRST=$((NEXT - WINDOW)) |
| 52 | [ "$FIRST" -ge 1 ] || FIRST=1 |
| 53 | WAITING=() YOUNG=() |
| 54 | for ((id = FIRST; id < NEXT; id++)); do |
| 55 | read -r _ ST _ _ _ PB < <(proposal "$id") |
| 56 | [ "$ST" = 1 ] || continue |
| 57 | # the workflow's own filter: Pending and not judged by the oracle (a deployment without oracleJudged: not cleared) |
| 58 | J=$(oracle_judged "$id") |
| 59 | if [ "$J" = '?' ]; then [ "$(clearance "$id")" = "$ZERO32" ] || continue; elif [ "$J" = true ]; then continue; fi |
| 60 | if [ $((NOW - PB)) -ge "$MIN_AGE" ]; then WAITING+=("$id"); else YOUNG+=("$id"); fi |
| 61 | done |
| 62 | STAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ) |
| 63 | HEAD="sweep $STAMP block $NOW: ids $FIRST..$((NEXT - 1)) checked; waiting for a verdict: ${WAITING[*]:-none}${YOUNG[*]:+; left to the listener (younger than $MIN_AGE blocks): ${YOUNG[*]}}" |
| 64 | if [ ${#WAITING[@]} = 0 ] && [ -z "${SWEEP_ALWAYS:-}" ]; then |
| 65 | say "$HEAD" |
| 66 | exit 0 |
| 67 | fi |
| 68 | |
| 69 | sim_bin |
| 70 | RX=$(dep receiver) |
| 71 | ATTESTER_PK=$(read_key attester) |
| 72 | PAYER=sender |
| 73 | [ -r "$KEY_DIR/sweeper.key" ] && PAYER=sweeper |
| 74 | SENDER_PK=$(read_key "$PAYER") |
| 75 | SBAL=$(cast balance "$(cast wallet address --private-key "$SENDER_PK")" --rpc-url "$RPC") |
| 76 | [ "$SBAL" != 0 ] || die "$PAYER.key has no ETH on $NET (fund-sender.sh <eth> $PAYER)" |
| 77 | ATTESTER=$(cast wallet address --private-key "$ATTESTER_PK") |
| 78 | [ "$(cast call "$RX" 'attester()(address)' --rpc-url "$RPC")" = "$ATTESTER" ] || die "attester.key does not match AirlockReceiver.attester()" |
| 79 | wasm_build |
| 80 | |
| 81 | LOG=$STATE/sweep.log |
| 82 | if [ -f "$LOG" ] && [ "$(wc -c <"$LOG" | tr -d ' ')" -gt 5000000 ]; then mv -f "$LOG" "$LOG.1"; fi |
| 83 | RUNLOG=$(mktemp "$STATE/sweep-run.XXXXXX") |
| 84 | say "== $HEAD" | tee -a "$LOG" |
| 85 | T0=$(now_s) |
| 86 | cd "$ROOT/cre" |
| 87 | set +e |
| 88 | # the keys reach the simulator through its environment only (exported by the builtin, never in any argv) |
| 89 | ( |
| 90 | export "$(attester_env)=$ATTESTER_PK" CRE_ETH_PRIVATE_KEY="$SENDER_PK" |
| 91 | exec "$SIM" workflow simulate ./veto -T "$TARGET" --config "$CONFIG" ${WASM_ARGS[@]+"${WASM_ARGS[@]}"} \ |
| 92 | --non-interactive --trigger-index 1 --broadcast |
| 93 | ) >"$RUNLOG" 2>&1 & |
| 94 | SPID=$! |
| 95 | unset ATTESTER_PK SENDER_PK |
| 96 | wait "$SPID" |
| 97 | RC=$? |
| 98 | SPID= |
| 99 | set -e |
| 100 | tee -a "$LOG" <"$RUNLOG" |
| 101 | |
| 102 | # What the run changed, read from the chain. |
| 103 | OUT=() |
| 104 | for id in ${WAITING[@]+"${WAITING[@]}"}; do |
| 105 | read -r _ ST _ _ _ _ < <(proposal "$id") |
| 106 | NOTE= |
| 107 | [ "$(oracle_judged "$id")" != false ] || NOTE=' (no CRE verdict yet)' |
| 108 | OUT+=("#$id $(state_name "$ST" "$(clearance "$id")")$NOTE") |
| 109 | done |
| 110 | say "== sweep done in $(fmt_secs $(($(now_s) - T0))) (simulator exit $RC): ${OUT[*]:-nothing waited}" | tee -a "$LOG" |
| 111 | exit "$RC" |