|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 5 | +FHEVM_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" |
| 6 | +REPO_ROOT="$(cd "${FHEVM_DIR}/../.." && pwd)" |
| 7 | +ENGINE_DIR="${REPO_ROOT}/coprocessor/fhevm-engine" |
| 8 | + |
| 9 | +COMPOSE_PROJECT="fhevm" |
| 10 | +COMPOSE_ENV="${FHEVM_DIR}/env/staging/.env.coprocessor.local" |
| 11 | +COMPOSE_FILE="${FHEVM_DIR}/docker-compose/coprocessor-docker-compose.yml" |
| 12 | + |
| 13 | +RUN_INTEGRATION=true |
| 14 | +RUN_STACK=true |
| 15 | +CAP=1 |
| 16 | +LOAD_TEST="operators" |
| 17 | +BOOTSTRAP_TIMEOUT_SECONDS=180 |
| 18 | + |
| 19 | +RED='\033[0;31m' |
| 20 | +GREEN='\033[0;32m' |
| 21 | +YELLOW='\033[0;33m' |
| 22 | +BLUE='\033[0;34m' |
| 23 | +RESET='\033[0m' |
| 24 | + |
| 25 | +override_file="" |
| 26 | +listeners_overridden=false |
| 27 | + |
| 28 | +usage() { |
| 29 | + cat <<EOF |
| 30 | +Usage: $(basename "$0") [options] |
| 31 | +
|
| 32 | +Options: |
| 33 | + --integration-only Run only deterministic Rust integration assertions. |
| 34 | + --stack-only Run only local-stack multi-listener SQL assertions. |
| 35 | + --cap N Slow-lane cap for stack scenario (default: 1). |
| 36 | + --load-test NAME fhevm-cli test to generate load (default: operators). |
| 37 | + --bootstrap-timeout SEC Timeout for key-bootstrap gate (default: 180). |
| 38 | + -h, --help Show this help. |
| 39 | +EOF |
| 40 | +} |
| 41 | + |
| 42 | +log() { |
| 43 | + printf "${BLUE}[slow-lane-validate]${RESET} %s\n" "$*" |
| 44 | +} |
| 45 | + |
| 46 | +warn() { |
| 47 | + printf "${YELLOW}[slow-lane-validate]${RESET} %s\n" "$*" |
| 48 | +} |
| 49 | + |
| 50 | +die() { |
| 51 | + printf "${RED}[slow-lane-validate] %s${RESET}\n" "$*" >&2 |
| 52 | + exit 1 |
| 53 | +} |
| 54 | + |
| 55 | +compose() { |
| 56 | + docker compose -p "${COMPOSE_PROJECT}" \ |
| 57 | + --env-file "${COMPOSE_ENV}" \ |
| 58 | + -f "${COMPOSE_FILE}" \ |
| 59 | + "$@" |
| 60 | +} |
| 61 | + |
| 62 | +compose_with_override() { |
| 63 | + local file="$1" |
| 64 | + shift |
| 65 | + docker compose -p "${COMPOSE_PROJECT}" \ |
| 66 | + --env-file "${COMPOSE_ENV}" \ |
| 67 | + -f "${COMPOSE_FILE}" \ |
| 68 | + -f "${file}" \ |
| 69 | + "$@" |
| 70 | +} |
| 71 | + |
| 72 | +db_query() { |
| 73 | + local sql="$1" |
| 74 | + docker exec -i coprocessor-and-kms-db \ |
| 75 | + psql -U postgres -d coprocessor -v ON_ERROR_STOP=1 -At -c "${sql}" |
| 76 | +} |
| 77 | + |
| 78 | +cleanup() { |
| 79 | + if [[ "${listeners_overridden}" == "true" ]]; then |
| 80 | + warn "Restoring listener defaults (no slow-lane override)" |
| 81 | + compose up -d --force-recreate \ |
| 82 | + coprocessor-host-listener \ |
| 83 | + coprocessor-host-listener-poller >/dev/null |
| 84 | + fi |
| 85 | + |
| 86 | + if [[ -n "${override_file}" && -f "${override_file}" ]]; then |
| 87 | + rm -f "${override_file}" |
| 88 | + fi |
| 89 | +} |
| 90 | + |
| 91 | +trap cleanup EXIT |
| 92 | + |
| 93 | +wait_for_bootstrap() { |
| 94 | + local deadline=$((SECONDS + BOOTSTRAP_TIMEOUT_SECONDS)) |
| 95 | + while (( SECONDS < deadline )); do |
| 96 | + local has_activate_key |
| 97 | + local has_fetched_keyset |
| 98 | + local has_key_material |
| 99 | + |
| 100 | + has_activate_key="$(docker logs --since=20m coprocessor-gw-listener 2>&1 | rg -c "ActivateKey event successful" || true)" |
| 101 | + has_fetched_keyset="$(docker logs --since=20m coprocessor-sns-worker 2>&1 | rg -c "Fetched keyset" || true)" |
| 102 | + has_key_material="$(db_query "SELECT COALESCE(bool_and(COALESCE(octet_length(lo_get(sns_pk)), 0) > 0), false) FROM tenants;")" |
| 103 | + |
| 104 | + if [[ "${has_activate_key}" -gt 0 && "${has_fetched_keyset}" -gt 0 && "${has_key_material}" == "t" ]]; then |
| 105 | + log "Bootstrap gate passed (ActivateKey + keyset + non-empty sns_pk)" |
| 106 | + return 0 |
| 107 | + fi |
| 108 | + sleep 3 |
| 109 | + done |
| 110 | + |
| 111 | + warn "Bootstrap gate timed out, restarting gw-listener once" |
| 112 | + compose up -d --no-deps coprocessor-gw-listener >/dev/null |
| 113 | + |
| 114 | + local retry_deadline=$((SECONDS + BOOTSTRAP_TIMEOUT_SECONDS)) |
| 115 | + while (( SECONDS < retry_deadline )); do |
| 116 | + local has_fetched_keyset |
| 117 | + has_fetched_keyset="$(docker logs --since=20m coprocessor-sns-worker 2>&1 | rg -c "Fetched keyset" || true)" |
| 118 | + if [[ "${has_fetched_keyset}" -gt 0 ]]; then |
| 119 | + log "Bootstrap recovered after gw-listener restart" |
| 120 | + return 0 |
| 121 | + fi |
| 122 | + sleep 3 |
| 123 | + done |
| 124 | + |
| 125 | + die "Bootstrap gate failed: sns-worker did not fetch keyset" |
| 126 | +} |
| 127 | + |
| 128 | +apply_listener_cap_override() { |
| 129 | + local cap="$1" |
| 130 | + override_file="$(mktemp)" |
| 131 | + cat >"${override_file}" <<EOF |
| 132 | +services: |
| 133 | + coprocessor-host-listener: |
| 134 | + command: |
| 135 | + - host_listener |
| 136 | + - --database-url=\${DATABASE_URL} |
| 137 | + - --coprocessor-api-key=\${TENANT_API_KEY} |
| 138 | + - --acl-contract-address=\${ACL_CONTRACT_ADDRESS} |
| 139 | + - --tfhe-contract-address=\${FHEVM_EXECUTOR_CONTRACT_ADDRESS} |
| 140 | + - --url=\${RPC_WS_URL} |
| 141 | + - --initial-block-time=1 |
| 142 | + - --dependent-ops-max-per-chain=${cap} |
| 143 | +
|
| 144 | + coprocessor-host-listener-poller: |
| 145 | + command: |
| 146 | + - host_listener_poller |
| 147 | + - --database-url=\${DATABASE_URL} |
| 148 | + - --coprocessor-api-key=\${TENANT_API_KEY} |
| 149 | + - --acl-contract-address=\${ACL_CONTRACT_ADDRESS} |
| 150 | + - --tfhe-contract-address=\${FHEVM_EXECUTOR_CONTRACT_ADDRESS} |
| 151 | + - --url=\${RPC_HTTP_URL} |
| 152 | + - --dependent-ops-max-per-chain=${cap} |
| 153 | +EOF |
| 154 | + |
| 155 | + compose_with_override "${override_file}" up -d --force-recreate \ |
| 156 | + coprocessor-host-listener \ |
| 157 | + coprocessor-host-listener-poller >/dev/null |
| 158 | + listeners_overridden=true |
| 159 | + log "Applied listener override with --dependent-ops-max-per-chain=${cap}" |
| 160 | +} |
| 161 | + |
| 162 | +run_integration_assertions() { |
| 163 | + log "Running deterministic integration assertions" |
| 164 | + ( |
| 165 | + cd "${ENGINE_DIR}" |
| 166 | + cargo +1.91.1 test -p host-listener --test host_listener_integration_tests \ |
| 167 | + test_slow_lane_threshold_matrix_locally -- --nocapture |
| 168 | + cargo +1.91.1 test -p host-listener --test host_listener_integration_tests \ |
| 169 | + test_slow_lane_cross_block_sustained_below_cap_stays_fast_locally -- --nocapture |
| 170 | + cargo +1.91.1 test -p host-listener --test host_listener_integration_tests \ |
| 171 | + test_slow_lane_off_mode_promotes_seen_chain_locally -- --nocapture |
| 172 | + ) |
| 173 | + printf "${GREEN}[slow-lane-validate] Integration assertions passed${RESET}\n" |
| 174 | +} |
| 175 | + |
| 176 | +run_stack_assertions() { |
| 177 | + log "Running local-stack assertions (cap=${CAP}, load=${LOAD_TEST})" |
| 178 | + wait_for_bootstrap |
| 179 | + |
| 180 | + local before_block_height |
| 181 | + before_block_height="$(db_query "SELECT COALESCE(MAX(block_height), 0) FROM dependence_chain;")" |
| 182 | + log "Baseline block_height=${before_block_height}" |
| 183 | + |
| 184 | + apply_listener_cap_override "${CAP}" |
| 185 | + |
| 186 | + ( |
| 187 | + cd "${FHEVM_DIR}" |
| 188 | + ./fhevm-cli test "${LOAD_TEST}" |
| 189 | + ) |
| 190 | + |
| 191 | + local counts |
| 192 | + counts="$(db_query " |
| 193 | + SELECT |
| 194 | + COUNT(*) FILTER (WHERE schedule_priority = 0), |
| 195 | + COUNT(*) FILTER (WHERE schedule_priority = 1), |
| 196 | + COUNT(*) |
| 197 | + FROM dependence_chain |
| 198 | + WHERE block_height > ${before_block_height}; |
| 199 | + ")" |
| 200 | + IFS='|' read -r fast_count slow_count total_count <<<"${counts}" |
| 201 | + |
| 202 | + log "Observed chains after baseline: total=${total_count}, fast=${fast_count}, slow=${slow_count}" |
| 203 | + |
| 204 | + [[ "${total_count}" -gt 0 ]] || die "No new dependence chains were ingested" |
| 205 | + [[ "${fast_count}" -gt 0 ]] || die "Expected at least one fast chain" |
| 206 | + [[ "${slow_count}" -gt 0 ]] || die "Expected at least one slow chain (raise load or lower cap)" |
| 207 | + |
| 208 | + local schedulable_order_head |
| 209 | + schedulable_order_head="$(db_query " |
| 210 | + SELECT schedule_priority |
| 211 | + FROM dependence_chain |
| 212 | + WHERE status = 'updated' |
| 213 | + AND worker_id IS NULL |
| 214 | + AND dependency_count = 0 |
| 215 | + AND block_height > ${before_block_height} |
| 216 | + ORDER BY schedule_priority ASC, last_updated_at ASC |
| 217 | + LIMIT 1; |
| 218 | + ")" |
| 219 | + |
| 220 | + if [[ -n "${schedulable_order_head}" && "${schedulable_order_head}" != "0" ]]; then |
| 221 | + die "Expected fast-lane first in schedulable ordering, got schedule_priority=${schedulable_order_head}" |
| 222 | + fi |
| 223 | + |
| 224 | + printf "${GREEN}[slow-lane-validate] Stack assertions passed${RESET}\n" |
| 225 | +} |
| 226 | + |
| 227 | +while (( "$#" )); do |
| 228 | + case "$1" in |
| 229 | + --integration-only) |
| 230 | + RUN_INTEGRATION=true |
| 231 | + RUN_STACK=false |
| 232 | + shift |
| 233 | + ;; |
| 234 | + --stack-only) |
| 235 | + RUN_INTEGRATION=false |
| 236 | + RUN_STACK=true |
| 237 | + shift |
| 238 | + ;; |
| 239 | + --cap) |
| 240 | + [[ $# -ge 2 ]] || die "--cap requires a value" |
| 241 | + CAP="$2" |
| 242 | + shift 2 |
| 243 | + ;; |
| 244 | + --load-test) |
| 245 | + [[ $# -ge 2 ]] || die "--load-test requires a value" |
| 246 | + LOAD_TEST="$2" |
| 247 | + shift 2 |
| 248 | + ;; |
| 249 | + --bootstrap-timeout) |
| 250 | + [[ $# -ge 2 ]] || die "--bootstrap-timeout requires a value" |
| 251 | + BOOTSTRAP_TIMEOUT_SECONDS="$2" |
| 252 | + shift 2 |
| 253 | + ;; |
| 254 | + -h|--help) |
| 255 | + usage |
| 256 | + exit 0 |
| 257 | + ;; |
| 258 | + *) |
| 259 | + die "Unknown argument: $1" |
| 260 | + ;; |
| 261 | + esac |
| 262 | +done |
| 263 | + |
| 264 | +if [[ "${RUN_INTEGRATION}" == "true" ]]; then |
| 265 | + run_integration_assertions |
| 266 | +fi |
| 267 | + |
| 268 | +if [[ "${RUN_STACK}" == "true" ]]; then |
| 269 | + run_stack_assertions |
| 270 | +fi |
| 271 | + |
| 272 | +printf "${GREEN}[slow-lane-validate] All selected checks passed${RESET}\n" |
0 commit comments