Source mirror · commit 369512f · 2026-10-26 · MIT · canonical repo: gitlab.com/jiapunk/upgrade-airlock
git clone https://upgrade-airlock.pages.dev/source/upgrade-airlock.git
upgrade-airlock / scripts / sepolia / sweep.sh
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).
21source "$(dirname "$0")/lib.sh"
22[ $# = 0 ] || die "usage: [FORK=<port>] $0"
23MIN_AGE=${SWEEP_MIN_AGE:-3}
24[[ "$MIN_AGE" =~ ^[0-9]+$ ]] || die "SWEEP_MIN_AGE must be a number of blocks"
25
26LOCK=$STATE/sweep.lock
27if ! 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"
34fi
35echo $$ >"$LOCK/pid"
36cleanup() {
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}
42trap cleanup EXIT
43trap 'exit 130' INT TERM
44
45check_rpc
46GA=$(dep airlockAdmin)
47workflow_config
48WINDOW=$(jq -r '.sweepWindow // 20' "$CONFIG")
49NOW=$(block_now)
50NEXT=$(cast call "$GA" 'nextId()(uint256)' --rpc-url "$RPC")
51FIRST=$((NEXT - WINDOW))
52[ "$FIRST" -ge 1 ] || FIRST=1
53WAITING=() YOUNG=()
54for ((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
61done
62STAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
63HEAD="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[*]}}"
64if [ ${#WAITING[@]} = 0 ] && [ -z "${SWEEP_ALWAYS:-}" ]; then
65 say "$HEAD"
66 exit 0
67fi
68
69sim_bin
70RX=$(dep receiver)
71ATTESTER_PK=$(read_key attester)
72PAYER=sender
73[ -r "$KEY_DIR/sweeper.key" ] && PAYER=sweeper
74SENDER_PK=$(read_key "$PAYER")
75SBAL=$(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)"
77ATTESTER=$(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()"
79wasm_build
80
81LOG=$STATE/sweep.log
82if [ -f "$LOG" ] && [ "$(wc -c <"$LOG" | tr -d ' ')" -gt 5000000 ]; then mv -f "$LOG" "$LOG.1"; fi
83RUNLOG=$(mktemp "$STATE/sweep-run.XXXXXX")
84say "== $HEAD" | tee -a "$LOG"
85T0=$(now_s)
86cd "$ROOT/cre"
87set +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 &
94SPID=$!
95unset ATTESTER_PK SENDER_PK
96wait "$SPID"
97RC=$?
98SPID=
99set -e
100tee -a "$LOG" <"$RUNLOG"
101
102# What the run changed, read from the chain.
103OUT=()
104for 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")
109done
110say "== sweep done in $(fmt_secs $(($(now_s) - T0))) (simulator exit $RC): ${OUT[*]:-nothing waited}" | tee -a "$LOG"
111exit "$RC"