Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions scripts/test_validate_mainnet_roles.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#!/usr/bin/env bash
#
# Tests for validate_mainnet_roles.sh
# Runs entirely offline: the `stellar` CLI is replaced by a shell stub whose
# per-method output is driven by environment variables.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TARGET="$SCRIPT_DIR/validate_mainnet_roles.sh"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT

PASS=0
FAIL=0

GOOD_ADDR="GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
GOOD_ADDR2="GB7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDB"

make_stub() {
cat > "$TMP/stellar" <<'STUB'
#!/usr/bin/env bash
method=""
seen_dashdash=0
for a in "$@"; do
if [ "$seen_dashdash" = "1" ] && [ -z "$method" ]; then
method="$a"
fi
if [ "$a" = "--" ]; then
seen_dashdash=1
fi
done
case "$method" in
get_admin) printf '%s' "${STUB_ADMIN-}" ;;
get_admin_quorum_weight) printf '%s' "${STUB_QUORUM-}" ;;
get_multisig_owners) printf '%s' "${STUB_OWNERS-}" ;;
get_multisig_threshold) printf '%s' "${STUB_THRESHOLD-}" ;;
get_pending_dao_rotation) printf '%s' "${STUB_DAO_PENDING-}" ;;
*) echo "unknown method: $method" >&2; exit 1 ;;
esac
STUB
chmod +x "$TMP/stellar"
}

run_validator() {
PATH="$TMP:$PATH" bash "$TARGET" \
--contract-id CDUMMYCONTRACTIDCDUMMYCONTRACTIDCDUMMYCONTRACTIDCDUMY \
--source "$GOOD_ADDR" \
--network local \
> "$TMP/out.txt" 2>&1 && echo 0 || echo $?
}

assert_eq() {
if [ "$1" = "$2" ]; then
PASS=$((PASS + 1)); echo " ok: $3"
else
FAIL=$((FAIL + 1)); echo " FAIL: $3 (expected '$1', got '$2')"
echo "----- validator output -----"; cat "$TMP/out.txt"; echo "----------------------------"
fi
}

assert_contains() {
if grep -q "$1" "$TMP/out.txt"; then
PASS=$((PASS + 1)); echo " ok: output contains '$1'"
else
FAIL=$((FAIL + 1)); echo " FAIL: output missing '$1'"
echo "----- validator output -----"; cat "$TMP/out.txt"; echo "----------------------------"
fi
}

reset_defaults() {
export STUB_ADMIN="\"$GOOD_ADDR\""
export STUB_QUORUM='3'
export STUB_OWNERS="[\"$GOOD_ADDR\",\"$GOOD_ADDR2\"]"
export STUB_THRESHOLD='2'
export STUB_DAO_PENDING='null'
}

make_stub

reset_defaults
echo "test: all checks pass"
assert_eq 0 "$(run_validator)" "exit 0 on healthy contract"
assert_contains "RESULT: PASS"

reset_defaults
export STUB_ADMIN='""'
echo "test: null admin fails"
assert_eq 1 "$(run_validator)" "exit 1 on null admin"

reset_defaults
export STUB_ADMIN='"NOTANADDRESS"'
echo "test: malformed admin fails"
assert_eq 1 "$(run_validator)" "exit 1 on malformed admin"

reset_defaults
export STUB_QUORUM='0'
echo "test: zero quorum weight fails"
assert_eq 1 "$(run_validator)" "exit 1 on zero quorum"

reset_defaults
export STUB_OWNERS='[]'
echo "test: empty multisig owners fails"
assert_eq 1 "$(run_validator)" "exit 1 on empty owners"

reset_defaults
export STUB_THRESHOLD='9'
echo "test: threshold exceeds owner count fails"
assert_eq 1 "$(run_validator)" "exit 1 on threshold > owners"

reset_defaults
export STUB_DAO_PENDING='{"new_dao":"'"$GOOD_ADDR"'"}'
echo "test: pending DAO rotation fails"
assert_eq 1 "$(run_validator)" "exit 1 on pending DAO rotation"

reset_defaults
echo "test: ROLE_* parser finds all four known roles"
ROLES_OUT="$(grep -oE 'pub const (ROLE_[A-Z_]+)' "$SCRIPT_DIR/../contracts/attestation/src/access_control.rs" | awk '{print $3}' | grep -vE 'ROLE_VALID_MASK' | sort -u | tr '\n' ' ')"
for r in ROLE_ADMIN ROLE_ATTESTOR ROLE_BUSINESS ROLE_OPERATOR; do
case " $ROLES_OUT " in
*" $r "*) PASS=$((PASS+1)); echo " ok: parser found $r" ;;
*) FAIL=$((FAIL+1)); echo " FAIL: parser missed $r" ;;
esac
done

echo
echo "=============================================="
echo " Results: $PASS passed, $FAIL failed"
echo "=============================================="
[ "$FAIL" -eq 0 ] || exit 1
205 changes: 205 additions & 0 deletions scripts/validate_mainnet_roles.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
#!/usr/bin/env bash
#
# validate_mainnet_roles.sh
#
# Post-deployment hardening check for the Veritasor attestation contract.
# Queries a freshly deployed contract and asserts that privileged roles and
# governance bindings are set, so an ownerless or half-configured contract
# cannot silently go live.
#
# Checks performed (each prints PASS/FAIL; any FAIL => non-zero exit):
# 1. ROLE coverage - every ROLE_* constant in access_control.rs is
# accounted for by this validator.
# 2. Admin bound - get_admin() returns a real, non-null address.
# 3. Admin quorum - get_admin_quorum_weight() > 0.
# 4. Multisig initialized - get_multisig_owners() non-empty AND
# 1 <= get_multisig_threshold() <= owner count.
# 5. DAO wiring - get_pending_dao_rotation() is None.
#
# Known limitation: the contract exposes no read-only getter for the ACTIVE
# DAO controller address. This script validates the absence of a pending
# rotation and reports the gap.
#
# Usage:
# scripts/validate_mainnet_roles.sh \
# --contract-id <CONTRACT_ID> \
# --source <SIGNING_ACCOUNT> \
# [--network mainnet] \
# [--access-control <path to access_control.rs>]
#
# Exit codes: 0 all passed | 1 a check failed | 2 usage/environment error

set -euo pipefail

NETWORK="mainnet"
CONTRACT_ID=""
SOURCE=""
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ACCESS_CONTROL="$SCRIPT_DIR/../contracts/attestation/src/access_control.rs"

while [ $# -gt 0 ]; do
case "$1" in
--network) NETWORK="$2"; shift 2 ;;
--contract-id) CONTRACT_ID="$2"; shift 2 ;;
--source) SOURCE="$2"; shift 2 ;;
--access-control) ACCESS_CONTROL="$2"; shift 2 ;;
-h|--help) sed -n '2,40p' "${BASH_SOURCE[0]}"; exit 0 ;;
*) echo "unrecognized argument: $1" >&2; exit 2 ;;
esac
done

if [ -z "$CONTRACT_ID" ]; then
echo "error: --contract-id is required" >&2; exit 2
fi
if [ -z "$SOURCE" ]; then
echo "error: --source is required" >&2; exit 2
fi
if ! command -v stellar >/dev/null 2>&1; then
echo "error: the 'stellar' CLI is required" >&2
exit 2
fi
if [ ! -f "$ACCESS_CONTROL" ]; then
echo "error: access_control.rs not found at: $ACCESS_CONTROL" >&2; exit 2
fi

FAILURES=0
REPORT=()

record() {
local status="$1"; shift
REPORT+=("$status $*")
if [ "$status" = "FAIL" ]; then
FAILURES=$((FAILURES + 1))
fi
}

invoke() {
local method="$1"; shift
stellar contract invoke \
--id "$CONTRACT_ID" \
--source "$SOURCE" \
--network "$NETWORK" \
-- "$method" "$@"
}

parse_role_constants() {
grep -oE 'pub const (ROLE_[A-Z_]+)' "$ACCESS_CONTROL" \
| awk '{print $3}' \
| grep -vE 'ROLE_VALID_MASK' \
| sort -u
}

KNOWN_ROLES="ROLE_ADMIN ROLE_ATTESTOR ROLE_BUSINESS ROLE_OPERATOR"

check_role_coverage() {
local uncovered=""
local role
while IFS= read -r role; do
[ -z "$role" ] && continue
case " $KNOWN_ROLES " in
*" $role "*) : ;;
*) uncovered="$uncovered $role" ;;
esac
done < <(parse_role_constants)

if [ -n "$uncovered" ]; then
record FAIL "ROLE coverage: unhandled role constant(s):$uncovered"
else
record PASS "ROLE coverage: all ROLE_* constants accounted for"
fi
}

check_admin_bound() {
local admin
if ! admin="$(invoke get_admin 2>/dev/null)"; then
record FAIL "Admin bound: get_admin() call failed"
return
fi
admin="$(printf '%s' "$admin" | tr -d '"[:space:]')"
if [ -z "$admin" ] || ! printf '%s' "$admin" | grep -qE '^[GC][A-Z0-9]{55}$'; then
record FAIL "Admin bound: admin address is null or malformed ('$admin')"
else
record PASS "Admin bound: $admin"
fi
}

check_admin_quorum() {
local weight
if ! weight="$(invoke get_admin_quorum_weight 2>/dev/null)"; then
record FAIL "Admin quorum: get_admin_quorum_weight() call failed"
return
fi
weight="$(printf '%s' "$weight" | tr -d '"[:space:]')"
if ! printf '%s' "$weight" | grep -qE '^[0-9]+$'; then
record FAIL "Admin quorum: non-numeric weight ('$weight')"
elif [ "$weight" -le 0 ]; then
record FAIL "Admin quorum: weight is zero (no active admins)"
else
record PASS "Admin quorum: weight = $weight"
fi
}

check_multisig() {
local owners_raw threshold owner_count
if ! owners_raw="$(invoke get_multisig_owners 2>/dev/null)"; then
record FAIL "Multisig: get_multisig_owners() call failed"
return
fi
owner_count="$(printf '%s' "$owners_raw" | grep -oE '[GC][A-Z0-9]{55}' | wc -l | tr -d '[:space:]')"

if [ "$owner_count" -eq 0 ]; then
record FAIL "Multisig: owner set is empty (not initialized)"
return
fi

if ! threshold="$(invoke get_multisig_threshold 2>/dev/null)"; then
record FAIL "Multisig: get_multisig_threshold() call failed"
return
fi
threshold="$(printf '%s' "$threshold" | tr -d '"[:space:]')"
if ! printf '%s' "$threshold" | grep -qE '^[0-9]+$'; then
record FAIL "Multisig: non-numeric threshold ('$threshold')"
elif [ "$threshold" -lt 1 ]; then
record FAIL "Multisig: threshold < 1"
elif [ "$threshold" -gt "$owner_count" ]; then
record FAIL "Multisig: threshold ($threshold) exceeds owner count ($owner_count)"
else
record PASS "Multisig: $owner_count owner(s), threshold $threshold"
fi
}

check_dao_wiring() {
local pending
if ! pending="$(invoke get_pending_dao_rotation 2>/dev/null)"; then
record FAIL "DAO wiring: get_pending_dao_rotation() call failed"
return
fi
pending="$(printf '%s' "$pending" | tr -d '[:space:]')"
if [ "$pending" = "null" ] || [ "$pending" = "void" ] || [ -z "$pending" ]; then
record PASS "DAO wiring: no pending DAO rotation (active-DAO getter not exposed; see notes)"
else
record FAIL "DAO wiring: a DAO rotation is pending on a fresh deploy ('$pending')"
fi
}

check_role_coverage
check_admin_bound
check_admin_quorum
check_multisig
check_dao_wiring

echo "=============================================="
echo " Mainnet role validation: $CONTRACT_ID"
echo " Network: $NETWORK"
echo "=============================================="
for line in "${REPORT[@]}"; do
echo " $line"
done
echo "=============================================="

if [ "$FAILURES" -gt 0 ]; then
echo "RESULT: FAIL ($FAILURES check(s) failed)"
exit 1
fi
echo "RESULT: PASS (all checks passed)"
exit 0