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. |
| 26 | source "$(dirname "$0")/lib.sh" |
| 27 | CARD=${1:-} |
| 28 | case "$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" ;; |
| 37 | esac |
| 38 | check_rpc |
| 39 | GA=$(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)" |
| 41 | TARGET_ADDR=$(dep "$KEY") |
| 42 | ALOG=$STATE/airlock.log |
| 43 | LOGLINES=0 |
| 44 | [ -f "$ALOG" ] && LOGLINES=$(wc -l <"$ALOG" | tr -d ' ') |
| 45 | if [ -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" |
| 47 | fi |
| 48 | |
| 49 | T0=$(now_s) |
| 50 | if [ "$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) |
| 54 | else |
| 55 | say "proposing $CARD (upgrade to $KEY $TARGET_ADDR) on $NET ..." |
| 56 | RCPT=$(send_as owner "$GA" 'proposeUpgrade(address,bytes)' "$TARGET_ADDR" 0x) |
| 57 | fi |
| 58 | TX=$(jq -r .transactionHash <<<"$RCPT") |
| 59 | [ "$(jq -r .status <<<"$RCPT")" = 0x1 ] || die "proposal transaction failed: $(tx_ref "$TX")" |
| 60 | TOPIC=$(cast keccak 'Proposed(uint256,uint8,address,uint256,bytes,bytes32,uint64)') |
| 61 | LOGJ=$(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")" |
| 63 | ID=$(cast to-dec "$(jq -r '.topics[1]' <<<"$LOGJ")") |
| 64 | PBLOCK=$(cast to-dec "$(jq -r .blockNumber <<<"$RCPT")") |
| 65 | EXPECT_TEXT=$([ "$EXPECT" = clear ] && echo 'Cleared by the CRE receiver' || echo "Vetoed by the CRE receiver with $EXPECT") |
| 66 | READY=$(proposal "$ID" | awk '{print $4}') || READY= |
| 67 | if [[ "$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" |
| 70 | else |
| 71 | say "proposed #$ID in block $PBLOCK ($(fmt_secs $(($(now_s) - T0))) to mine); executable after the cooldown once cleared" |
| 72 | fi |
| 73 | say " tx $(tx_ref "$TX")" |
| 74 | [ "${SHOW_EXPECT:-0}" = 1 ] && say " expected: $EXPECT_TEXT" |
| 75 | [ "${WAIT:-1}" = 1 ] || exit 0 |
| 76 | |
| 77 | listener_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 | } |
| 81 | LIMIT=${WAIT_SECS:-$([ "$MODE" = live ] && echo 180 || echo 120)} |
| 82 | PTS=$(block_ts "$PBLOCK") || PTS= |
| 83 | chain_secs() { local t; if [ -n "$PTS" ] && t=$(block_ts "$1"); then say "$((t - PTS))s"; else say "?s"; fi; } |
| 84 | while :; 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 |
| 121 | done |