Skip to content

Instantly share code, notes, and snippets.

@ZenGround0
Created May 11, 2026 21:29
Show Gist options
  • Select an option

  • Save ZenGround0/00a0fa704922f51dfa5395f15ddf8380 to your computer and use it in GitHub Desktop.

Select an option

Save ZenGround0/00a0fa704922f51dfa5395f15ddf8380 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
# Exercise the SectorStatusChecker EVM contract against a live miner+sector.
#
# Usage:
# check-sector-status.sh <contract_eth_addr> <miner_addr> <sector_number>
#
# - <contract_eth_addr>: 0x... address of deployed SectorStatusChecker
# - <miner_addr>: Filecoin miner address (e.g. t0180698 or f0...)
# - <sector_number>: u64 sector number
#
# Env overrides:
# ETH_RPC default http://127.0.0.1:4444/rpc/v1
# LOTUS default "lotus" (in PATH)
#
# Steps:
# 1. Calls GenerateSectorLocation off-chain (lotus state call) to get
# {status, aux_data} for the sector. Aux_data is the CBOR-encoded
# SectorLocation = [deadline:i64, partition:i64]; sentinel (-1, -1)
# means "NO_DEADLINE/NO_PARTITION" (sector not tracked anywhere —
# either compacted-terminated or never committed).
# 2. Calls validateSectorStatus on the contract via eth_call, asserting
# the contract agrees the sector is in the reported status.
# 3. Calls getNominalSectorExpiration on the contract via eth_call, and
# compares against the expiration in `lotus state sector` output.
# Skipped when status is Dead (sector is not in the Sectors AMT, so
# no nominal expiration exists by design).
set -euo pipefail
ETH_RPC="${ETH_RPC:-http://127.0.0.1:4444/rpc/v1}"
LOTUS="${LOTUS:-lotus}"
if [[ $# -ne 3 ]]; then
echo "usage: $0 <contract_eth_addr> <miner_addr> <sector_number>" >&2
exit 2
fi
CONTRACT="$1"
MINER="$2"
SECTOR="$3"
# FRC-42 method number for miner GenerateSectorLocation.
GENERATE_SECTOR_LOCATION_METHOD=1321604665
# CBOR encoding of SectorLocation { deadline: -1, partition: -1 }, base64-encoded:
# 0x82 (array of 2) 0x20 (i64 -1) 0x20 (i64 -1) ==> base64 "giAg"
AUX_NONE_B64="giAg"
AUX_NONE_HEX="0x822020"
# Sanity check args.
[[ "$CONTRACT" =~ ^0x[0-9a-fA-F]{40}$ ]] || { echo "contract must be 0x-prefixed eth address" >&2; exit 2; }
[[ "$SECTOR" =~ ^[0-9]+$ ]] || { echo "sector must be a non-negative integer" >&2; exit 2; }
# Resolve miner to its f0/t0 ID form (uint64 form) for the contract.
# `lotus state lookup <addr>` works for both ID (t0..) and robust (t2..) forms.
MINER_ID_ADDR="$("$LOTUS" state lookup "$MINER" 2>/dev/null || true)"
[[ -n "$MINER_ID_ADDR" ]] || { echo "could not resolve miner to ID address: $MINER" >&2; exit 1; }
MINER_ID="${MINER_ID_ADDR#?0}" # strip leading 't0' or 'f0'
[[ "$MINER_ID" =~ ^[0-9]+$ ]] || { echo "unexpected ID address shape: $MINER_ID_ADDR" >&2; exit 1; }
echo "== Inputs =="
echo " contract: $CONTRACT"
echo " miner: $MINER (id=$MINER_ID, resolved=$MINER_ID_ADDR)"
echo " sector: $SECTOR"
echo " eth-rpc: $ETH_RPC"
echo
# Tiny pure-stdlib CBOR decoder for our SectorLocation = [i64, i64] case.
# Reads aux_data base64 from stdin and prints "<deadline> <partition>".
decode_aux() {
python3 - "$1" <<'PY'
import sys, base64
b = base64.b64decode(sys.argv[1])
def rd(b, i):
major, info = b[i] >> 5, b[i] & 0x1f
i += 1
if info < 24:
v = info
elif info == 24:
v = b[i]; i += 1
elif info == 25:
v = int.from_bytes(b[i:i+2], 'big'); i += 2
elif info == 26:
v = int.from_bytes(b[i:i+4], 'big'); i += 4
elif info == 27:
v = int.from_bytes(b[i:i+8], 'big'); i += 8
else:
raise ValueError(f"unsupported additional info {info}")
if major == 1: # negative integer
v = -1 - v
elif major != 0: # unsigned integer
raise ValueError(f"unexpected CBOR major {major}")
return v, i
if not b or b[0] != 0x82:
raise SystemExit(f"expected CBOR array(2), got 0x{b[0]:02x}" if b else "empty aux_data")
i = 1
d, i = rd(b, i)
p, i = rd(b, i)
print(f"{d} {p}")
PY
}
# Pretty-print the location based on (deadline, partition).
fmt_location() {
local d="$1" p="$2"
if [[ "$d" == "-1" && "$p" == "-1" ]]; then
echo "NO_DEADLINE NO_PARTITION"
else
echo "deadline=$d partition=$p"
fi
}
# Pretty-print status code.
fmt_status() {
case "$1" in
0) echo "Dead";;
1) echo "Active";;
2) echo "Faulty";;
*) echo "Unknown($1)";;
esac
}
# ---- 1. Off-chain GenerateSectorLocation via lotus state call ----
echo "== 1. GenerateSectorLocation (off-chain) =="
PARAMS_B64="$("$LOTUS" chain encode params "$MINER_ID_ADDR" "$GENERATE_SECTOR_LOCATION_METHOD" "{\"SectorNumber\":$SECTOR}")"
# Run the call; capture stdout *and* allow a failure (e.g. older actor version
# might raise instead of returning Dead/(-1,-1) for a missing sector).
if CALL_OUT="$("$LOTUS" state call --encoding base64 "$MINER_ID_ADDR" "$GENERATE_SECTOR_LOCATION_METHOD" "$PARAMS_B64" 2>&1)"; then
CALL_OK=1
else
CALL_OK=0
fi
# Parse "Exit code: X" if present.
EXIT_CODE="$(grep -E '^Exit code:' <<<"$CALL_OUT" | awk '{print $3}')"
EXIT_CODE="${EXIT_CODE:-?}"
if [[ "$CALL_OK" != "1" || "$EXIT_CODE" != "0" ]]; then
# Look for the actor-side "sector ... not found" signal in either the lotus
# error text or the call output. Treat it as Dead with NO_DEADLINE/NO_PARTITION.
if grep -qE "sector .* not found|not_found|USR_NOT_FOUND" <<<"$CALL_OUT"; then
echo " WARN: GenerateSectorLocation reported sector not found; falling back to status=Dead, location=NO_DEADLINE/NO_PARTITION"
STATUS=0
AUX_B64="$AUX_NONE_B64"
AUX_HEX="$AUX_NONE_HEX"
else
echo "GenerateSectorLocation failed (exit=$EXIT_CODE):" >&2
echo "$CALL_OUT" >&2
exit 1
fi
else
# Extract Status + AuxData from the JSON Return block.
RETURN_JSON="$(awk '/^Return:/{flag=1; next} flag' <<<"$CALL_OUT")"
if command -v jq >/dev/null 2>&1; then
STATUS=$(echo "$RETURN_JSON" | jq -r '.Status')
AUX_B64=$(echo "$RETURN_JSON" | jq -r '.AuxData')
else
STATUS=$(echo "$RETURN_JSON" | sed -n 's/.*"Status": *\([0-9]*\).*/\1/p')
AUX_B64=$(echo "$RETURN_JSON" | sed -n 's/.*"AuxData": *"\([^"]*\)".*/\1/p')
fi
AUX_HEX="0x$(echo "$AUX_B64" | base64 -d | od -An -tx1 -v | tr -d ' \n')"
fi
# Decode + pretty-print.
read -r LOC_DEADLINE LOC_PARTITION < <(decode_aux "$AUX_B64")
echo " status: $STATUS ($(fmt_status "$STATUS"))"
echo " location: $(fmt_location "$LOC_DEADLINE" "$LOC_PARTITION")"
echo " aux_data: $AUX_B64 ($AUX_HEX)"
echo
# ---- 2. validateSectorStatus via contract eth_call ----
echo "== 2. validateSectorStatus (contract) =="
VS_CALL=$(cast calldata "validateSectorStatus(uint64,uint64,uint8,bytes)" "$MINER_ID" "$SECTOR" "$STATUS" "$AUX_HEX")
VS_RET=$(cast call --rpc-url "$ETH_RPC" "$CONTRACT" "$VS_CALL")
VS_VALID=$(cast --to-dec "$VS_RET")
echo " raw: $VS_RET"
echo " valid: $VS_VALID"
if [[ "$VS_VALID" != "1" ]]; then
echo "FAIL: contract reports sector is NOT in status=$STATUS ($(fmt_status "$STATUS"))" >&2
exit 1
fi
echo " PASS: contract agrees sector is $(fmt_status "$STATUS")"
echo
# ---- 3. getNominalSectorExpiration via contract eth_call ----
echo "== 3. getNominalSectorExpiration (contract) =="
if [[ "$STATUS" == "0" ]]; then
echo " SKIP: sector is Dead — not in Sectors AMT, no nominal expiration to compare"
echo
echo "All checks passed."
exit 0
fi
NE_CALL=$(cast calldata "getNominalSectorExpiration(uint64,uint64)" "$MINER_ID" "$SECTOR")
NE_RET=$(cast call --rpc-url "$ETH_RPC" "$CONTRACT" "$NE_CALL")
CONTRACT_EXP=$(cast --to-dec "$NE_RET")
echo " contract expiration: $CONTRACT_EXP"
# Compare against lotus state sector.
LOTUS_EXP=$("$LOTUS" state sector "$MINER_ID_ADDR" "$SECTOR" 2>/dev/null \
| awk '/^Expiration:/ {print $2; exit}')
echo " lotus expiration: ${LOTUS_EXP:-<none>}"
if [[ -z "$LOTUS_EXP" ]]; then
echo " WARN: could not read expiration from 'lotus state sector' (sector may not be in on-chain SectorInfo array — e.g. terminated/compacted)."
elif [[ "$CONTRACT_EXP" == "$LOTUS_EXP" ]]; then
echo " PASS: expirations match"
else
echo " FAIL: expirations differ" >&2
exit 1
fi
echo
echo "All checks passed."
@ZenGround0
Copy link
Copy Markdown
Author

Terminated and compacted sector

./check-sector-status.sh 0x0d8826fceed35075ee34e2f150a350a823982ad6 t0185851 6
== Inputs ==
  contract:  0x0d8826fceed35075ee34e2f150a350a823982ad6
  miner:     t0185851 (id=185851, resolved=t0185851)
  sector:    6
  eth-rpc:   http://127.0.0.1:4444/rpc/v1

== 1. GenerateSectorLocation (off-chain) ==
  status:   0 (Dead)
  location: NO_DEADLINE NO_PARTITION
  aux_data: giAg  (0x822020)

== 2. validateSectorStatus (contract) ==
  raw:    0x0000000000000000000000000000000000000000000000000000000000000001
  valid:  1
  PASS: contract agrees sector is Dead

== 3. getNominalSectorExpiration (contract) ==
  SKIP: sector is Dead — not in Sectors AMT, no nominal expiration to compare

All checks passed.

@ZenGround0
Copy link
Copy Markdown
Author

Termianted and uncompacted sector

./check-sector-status.sh 0x0d8826fceed35075ee34e2f150a350a823982ad6 t0181521 120
== Inputs ==
  contract:  0x0d8826fceed35075ee34e2f150a350a823982ad6
  miner:     t0181521 (id=181521, resolved=t0181521)
  sector:    120
  eth-rpc:   http://127.0.0.1:4444/rpc/v1

== 1. GenerateSectorLocation (off-chain) ==
  status:   0 (Dead)
  location: deadline=0 partition=0
  aux_data: ggAA  (0x820000)

== 2. validateSectorStatus (contract) ==
  raw:    0x0000000000000000000000000000000000000000000000000000000000000001
  valid:  1
  PASS: contract agrees sector is Dead

== 3. getNominalSectorExpiration (contract) ==
  SKIP: sector is Dead — not in Sectors AMT, no nominal expiration to compare

All checks passed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment