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 / propose.sh
scripts/sepolia/propose.sh121 lines · 7837 bytes
1#!/usr/bin/env bash
2# Propose one of the demo cards as the owner, then wait for the airlock's verdict on chain.
3#
4# scripts/sepolia/propose.sh v2 # the honest upgrade -> expect Cleared (it may execute after the cooldown)
5# scripts/sepolia/propose.sh evil-mint # anyone can mint -> expect a Veto, R1_TOTAL_SUPPLY
6# scripts/sepolia/propose.sh evil-owner # anyone can take owner -> expect a Veto, R2_OWNER
7# scripts/sepolia/propose.sh evil-fee # transfers skim a fee -> expect a Veto, R3_TRANSFER_CONSERVATION
8# scripts/sepolia/propose.sh evil-basefee # honest while simulated (basefee 0), anyone mints on a real block
9# # -> expect a Veto, R9_NEW_OPCODES (BASEFEE)
10# scripts/sepolia/propose.sh evil-drain # v1 plus rescue(): anyone moves a holder's tokens without allowance
11# # -> expect a Veto, R3B_THIRD_PARTY_FUNDS
12# scripts/sepolia/propose.sh remove-receiver # drop the CRE receiver from the guardians
13# # -> expect a Veto, R7_CONFIG_GUARD
14# FORK=19631 scripts/sepolia/propose.sh ... # same on the local fork
15#
16# Env: WAIT=0 returns right after the proposal is mined; WAIT_SECS (default 180 live, 120 fork) bounds the wait.
17# The verdict is read from the chain: AirlockAdmin.clearanceOf(id) (Cleared) or the proposal status (Vetoed), with
18# the rule, the evidence hash and the report transaction from the events. The listener's log lines for the
19# proposal are shown when its log is in the same state dir.
20# The card's expected outcome is only compared after the chain's verdict is in (a projected terminal then never
21# shows a verdict before CRE gives one); SHOW_EXPECT=1 also prints it right after the proposal, for rehearsals.
22# Reads are retried on transient RPC errors (RPC_TRIES, default 3) and a failed read inside the wait loop is tried
23# again 2 s later; the proposal transaction itself is sent once and never retried. If the script stops before it
24# prints "proposed #N", look at the war room or status.sh before running it again: the transaction may have landed.
25# Exit status: 0 when the outcome is the expected one, 1 otherwise.
26source "$(dirname "$0")/lib.sh"
27CARD=${1:-}
28case "$CARD" in
29 v2 | honest) KEY=v2 EXPECT=clear ;;
30 evil-mint | evilMint) KEY=evilMint EXPECT=R1_TOTAL_SUPPLY ;;
31 evil-owner | evilOwner) KEY=evilOwner EXPECT=R2_OWNER ;;
32 evil-fee | evilFee) KEY=evilFee EXPECT=R3_TRANSFER_CONSERVATION ;;
33 evil-basefee | evilBaseFee) KEY=evilBaseFee EXPECT=R9_NEW_OPCODES ;;
34 evil-drain | evilDrain) KEY=evilDrain EXPECT=R3B_THIRD_PARTY_FUNDS ;;
35 remove-receiver) KEY=receiver EXPECT=R7_CONFIG_GUARD ;;
36 *) die "usage: [FORK=<port>] $0 v2|evil-mint|evil-owner|evil-fee|evil-basefee|evil-drain|remove-receiver" ;;
37esac
38check_rpc
39GA=$(dep airlockAdmin)
40[ -n "$(jq -r --arg k "$KEY" '.[$k] // empty' "$DEPLOY_JSON")" ] || die "the deployment has no $KEY (it predates that card; deploy again with deploy.sh)"
41TARGET_ADDR=$(dep "$KEY")
42ALOG=$STATE/airlock.log
43LOGLINES=0
44[ -f "$ALOG" ] && LOGLINES=$(wc -l <"$ALOG" | tr -d ' ')
45if [ -f "$STATE/airlock.pid" ] && kill -0 "$(cat "$STATE/airlock.pid")" 2>/dev/null; then :; else
46 say "note: no airlock.sh listener in this state dir ($STATE); the proposal is judged by the sweeper (sweep.sh / the systemd timer) or not at all"
47fi
48
49T0=$(now_s)
50if [ "$KEY" = receiver ]; then
51 [ "$(cast call "$GA" 'isGuardian(address)(bool)' "$TARGET_ADDR" --rpc-url "$RPC")" = true ] || say "note: the receiver is not a guardian any more"
52 say "proposing $CARD (setGuardian receiver $TARGET_ADDR -> off) on $NET ..."
53 RCPT=$(send_as owner "$GA" 'proposeSetGuardian(address,bool)' "$TARGET_ADDR" false)
54else
55 say "proposing $CARD (upgrade to $KEY $TARGET_ADDR) on $NET ..."
56 RCPT=$(send_as owner "$GA" 'proposeUpgrade(address,bytes)' "$TARGET_ADDR" 0x)
57fi
58TX=$(jq -r .transactionHash <<<"$RCPT")
59[ "$(jq -r .status <<<"$RCPT")" = 0x1 ] || die "proposal transaction failed: $(tx_ref "$TX")"
60TOPIC=$(cast keccak 'Proposed(uint256,uint8,address,uint256,bytes,bytes32,uint64)')
61LOGJ=$(jq --arg ga "$(lc "$GA")" --arg t "$TOPIC" -c '[.logs[] | select((.address|ascii_downcase)==$ga and .topics[0]==$t)][0]' <<<"$RCPT")
62[ "$LOGJ" != null ] || die "no Proposed log in $(tx_ref "$TX")"
63ID=$(cast to-dec "$(jq -r '.topics[1]' <<<"$LOGJ")")
64PBLOCK=$(cast to-dec "$(jq -r .blockNumber <<<"$RCPT")")
65EXPECT_TEXT=$([ "$EXPECT" = clear ] && echo 'Cleared by the CRE receiver' || echo "Vetoed by the CRE receiver with $EXPECT")
66READY=$(proposal "$ID" | awk '{print $4}') || READY=
67if [[ "$READY" =~ ^[0-9]+$ ]]; then
68 DELAY=$((READY - PBLOCK))
69 say "proposed #$ID in block $PBLOCK ($(fmt_secs $(($(now_s) - T0))) to mine); executable from block $READY (+$DELAY blocks, ~$(fmt_secs $((DELAY * SEPOLIA_BLOCK_SECS))) at 12 s blocks) once cleared"
70else
71 say "proposed #$ID in block $PBLOCK ($(fmt_secs $(($(now_s) - T0))) to mine); executable after the cooldown once cleared"
72fi
73say " tx $(tx_ref "$TX")"
74[ "${SHOW_EXPECT:-0}" = 1 ] && say " expected: $EXPECT_TEXT"
75[ "${WAIT:-1}" = 1 ] || exit 0
76
77listener_lines() {
78 [ -f "$ALOG" ] || return 0
79 tail -n +"$((LOGLINES + 1))" "$ALOG" | grep -E "\[USER LOG\].*#$ID[^0-9]" | sed -E 's/^.*\[USER LOG\] / airlock: /' | cut -c1-220 || true
80}
81LIMIT=${WAIT_SECS:-$([ "$MODE" = live ] && echo 180 || echo 120)}
82PTS=$(block_ts "$PBLOCK") || PTS=
83chain_secs() { local t; if [ -n "$PTS" ] && t=$(block_ts "$1"); then say "$((t - PTS))s"; else say "?s"; fi; }
84while :; do
85 # a read the RPC does not answer (after lib.sh's retries) is tried again on the next pass, never fatal
86 ST=$(proposal "$ID" | awk '{print $2}') || ST=
87 CL=$(clearance "$ID") || CL=
88 [[ "$ST" =~ ^[0-3]$ ]] || ST=
89 [[ "$CL" =~ ^0x[0-9a-fA-F]{64}$ ]] || CL=
90 if [ -n "$ST" ] && [ -n "$CL" ] && { [ "$ST" = 2 ] || [ "$CL" != "$ZERO32" ]; }; then
91 EV=$(events_since "$PBLOCK")
92 # an RPC behind a load balancer may answer the status before its log index has the event: read again
93 if [ -z "$(first_event "$([ "$ST" = 2 ] && echo vetoed || echo cleared)" "$ID" "$EV")" ]; then sleep 3; EV=$(events_since "$PBLOCK"); fi
94 if [ "$ST" = 2 ]; then
95 read -r _ _ RH WHO VTX VB < <(first_event vetoed "$ID" "$EV") || { RH=$ZERO32 WHO=unknown VTX=unknown VB=$PBLOCK; }
96 RULE=$(rule_name "$RH")
97 read -r _ _ EVID _ _ _ < <(first_event reason "$ID" "$EV") || EVID=-
98 say "VETOED #$ID by $(who_name "$WHO"): $RULE, in block $VB (+$((VB - PBLOCK)) blocks, $(chain_secs "$VB") of chain time after the proposal; $(fmt_secs $(($(now_s) - T0))) wall clock)"
99 say " evidence $EVID"
100 say " report tx $(tx_ref "$VTX")"
101 GOT=$RULE
102 else
103 read -r _ _ EVID WHO CTX CB < <(first_event cleared "$ID" "$EV") || { WHO=unknown CTX=unknown CB=$PBLOCK; }
104 say "CLEARED #$ID by $(who_name "$WHO") in block $CB (+$((CB - PBLOCK)) blocks, $(chain_secs "$CB") of chain time after the proposal; $(fmt_secs $(($(now_s) - T0))) wall clock)"
105 say " evidence $CL"
106 say " report tx $(tx_ref "$CTX")"
107 say " executable from block ${READY:-<its ready block>}: ${FORK:+FORK=$FORK }scripts/sepolia/execute.sh $ID"
108 GOT=clear
109 fi
110 listener_lines
111 if [ "$GOT" = "$EXPECT" ]; then say " matches this card's expected outcome ($EXPECT_TEXT)"; exit 0; fi
112 say "MISMATCH: expected $([ "$EXPECT" = clear ] && echo 'a clearance' || echo "a veto with $EXPECT")"
113 exit 1
114 fi
115 if [ $(($(now_s) - T0)) -ge "$LIMIT" ]; then
116 say "no verdict after $(fmt_secs "$LIMIT"): #$ID is ${ST:+${STATUS_NAMES[$ST]} and }not cleared (is airlock.sh running? see $ALOG; or run sweep.sh)"
117 listener_lines
118 exit 1
119 fi
120 sleep 2
121done