diff --git a/docs/monitoring-guide.md b/docs/monitoring-guide.md index 3770acd..58b2257 100644 --- a/docs/monitoring-guide.md +++ b/docs/monitoring-guide.md @@ -19,8 +19,13 @@ The script requires Soroban CLI, `jq`, and the matching 1. the configured RPC endpoint is reachable; 2. `progress-tracker`, `learn-token`, and `credential-nft` report initialized; -3. each contract returns a configured admin; and -4. `learn-token.total_supply` can be read. +3. each contract returns a configured admin; +4. pause states across all contracts (warning on paused operations); +5. pending admin transfer status across all contracts; +6. cross-contract wiring (`learn-token` and `credential-nft` point to `progress-tracker`); +7. `learn-token.total_supply` and admin balance; +8. `credential-nft` total credential and revocation metrics; and +9. `progress-tracker` course and enrollment metrics. Run this check after every deployment, initialization, upgrade, configuration change, and incident recovery. Schedule it regularly and alert on a non-zero diff --git a/docs/upgrade-guide.md b/docs/upgrade-guide.md index 68494e0..9474ad0 100644 --- a/docs/upgrade-guide.md +++ b/docs/upgrade-guide.md @@ -129,6 +129,13 @@ soroban contract invoke \ --new_wasm_hash "$NEW_WASM_HASH" ``` +Alternatively, run the automated upgrade script with `--multisig`: +```bash +./scripts/upgrade.sh testnet --multisig --co-signer "$ADMIN_2_ADDRESS" +# or upgrade only learn-token: +./scripts/upgrade.sh testnet learn-token --multisig --co-signer "$ADMIN_2_ADDRESS" +``` + ### Option C: Using Deploy Script > [!NOTE] diff --git a/scripts/deploy.sh b/scripts/deploy.sh index cca2bac..7acd2f3 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -6,8 +6,17 @@ set -euo pipefail # ────────────────────────────────────────────────────────────────────────────── # # Usage: -# ./scripts/deploy.sh testnet -# ./scripts/deploy.sh mainnet +# ./scripts/deploy.sh testnet # Deploy to testnet with cost confirmation +# ./scripts/deploy.sh mainnet # Deploy to mainnet with cost confirmation +# ./scripts/deploy.sh testnet --confirm # Skip cost confirmation prompt (#602) +# ./scripts/deploy.sh testnet --resume # Resume from partial failure (#603) +# ./scripts/deploy.sh testnet --estimate-only # Show cost estimation and exit (#602) +# +# Flags: +# --confirm Skip confirmation prompt after showing deployment cost estimation +# --resume Resume deployment from a previous partial deployment failure +# --estimate-only Display pre-deployment cost estimation and exit +# -h, --help Display this help message # # Prerequisites: # - soroban CLI installed (v21+) @@ -17,7 +26,46 @@ set -euo pipefail # - Sufficient XLM for deployment fees # ────────────────────────────────────────────────────────────────────────────── -NETWORK="${1:-testnet}" +NETWORK="testnet" +CONFIRM=false +RESUME=false +ESTIMATE_ONLY=false + +while [ "$#" -gt 0 ]; do + case "$1" in + --confirm) + CONFIRM=true + shift + ;; + --resume) + RESUME=true + shift + ;; + --estimate-only) + ESTIMATE_ONLY=true + shift + ;; + testnet|mainnet) + NETWORK="$1" + shift + ;; + -h|--help) + echo "Usage: $0 [testnet|mainnet] [--confirm] [--resume] [--estimate-only]" + echo "" + echo "Options:" + echo " --confirm Skip interactive confirmation prompt after cost estimation (#602)" + echo " --resume Resume deployment from previous partial state (#603)" + echo " --estimate-only Show estimated deployment costs and exit (#602)" + echo " -h, --help Show this help message" + exit 0 + ;; + *) + echo "Error: Unknown argument '$1'." + echo "Usage: $0 [testnet|mainnet] [--confirm] [--resume] [--estimate-only]" + exit 1 + ;; + esac +done if [ "$NETWORK" = "testnet" ]; then RPC_URL="https://soroban-testnet.stellar.org:443" @@ -30,9 +78,9 @@ else exit 1 fi -# Verify secret key is available +# Verify secret key is available (not strictly required if only estimating costs) STELLAR_SECRET_KEY="${STELLAR_SECRET_KEY:-${SOROBAN_SECRET_KEY:-}}" -if [ -z "${STELLAR_SECRET_KEY:-}" ]; then +if [ -z "${STELLAR_SECRET_KEY:-}" ] && [ "$ESTIMATE_ONLY" = false ]; then echo "Error: STELLAR_SECRET_KEY environment variable is not set." echo "Export your Stellar secret key before running this script." exit 1 @@ -60,19 +108,121 @@ echo "RPC reachable." echo "" DEPLOY_FILE="deployments-${NETWORK}.json" -if [ -f "$DEPLOY_FILE" ]; then +PARTIAL_STATE_FILE=".deploy-partial-${NETWORK}.json" + +if [ -f "$DEPLOY_FILE" ] && [ "$RESUME" = false ]; then echo "Error: Deployment already exists in $DEPLOY_FILE" - echo "To redeploy, please remove this file first." + echo "To redeploy, please remove or back up this file first." + exit 1 +fi + +# Check for unresumed partial state file +if [ -f "$PARTIAL_STATE_FILE" ] && [ "$RESUME" = false ]; then + echo "Warning: A previous partial deployment was found in $PARTIAL_STATE_FILE." + echo "Run with '--resume' to continue from the partial state:" + echo " $0 $NETWORK --resume $([ "$CONFIRM" = true ] && echo "--confirm")" + echo "Or remove $PARTIAL_STATE_FILE to start a fresh deployment." exit 1 fi -# Build all contracts +# Build all contracts first so bytecode sizes are known for estimation and deployment echo "[1/4] Building contracts..." cargo build --release --target wasm32-unknown-unknown --workspace # Shrink WASM before upload: install fees and code rent scale with size (#344). # Set STRIP_SPEC_DOCS=1 to also strip doc comments from the embedded spec. "$(dirname "$0")/optimize-wasm.sh" +echo " Build complete." +echo "" + +# ── Deployment Cost Estimation (#602) ───────────────────────────────────────── + +WASM_DIR="target/wasm32-unknown-unknown/release" +LEARN_TOKEN_WASM="$WASM_DIR/learn_token.optimized.wasm" +CREDENTIAL_NFT_WASM="$WASM_DIR/credential_nft.optimized.wasm" +PROGRESS_TRACKER_WASM="$WASM_DIR/progress_tracker.optimized.wasm" + +calc_file_size() { + local file="$1" + if [ -f "$file" ]; then + stat -c%s "$file" 2>/dev/null || wc -c < "$file" | tr -d ' ' + else + echo "0" + fi +} + +calc_upload_cost() { + local size_bytes="$1" + # Soroban upload fee: ~0.0001 XLM base tx fee + storage rent deposit (~0.001 XLM per KB) + awk -v b="$size_bytes" 'BEGIN { printf "%.4f", 0.0001 + (b / 1024.0 * 0.001) }' +} + +calc_deploy_cost() { + # Instance deployment creation fee and rent deposit: ~0.0500 XLM per contract + echo "0.0500" +} + +calc_total_cost() { + local upload="$1" + local deploy="$2" + awk -v u="$upload" -v d="$deploy" 'BEGIN { printf "%.4f", u + d }' +} + +LT_BYTES=$(calc_file_size "$LEARN_TOKEN_WASM") +CN_BYTES=$(calc_file_size "$CREDENTIAL_NFT_WASM") +PT_BYTES=$(calc_file_size "$PROGRESS_TRACKER_WASM") + +LT_KB=$(awk -v b="$LT_BYTES" 'BEGIN { printf "%.1f", b / 1024.0 }') +CN_KB=$(awk -v b="$CN_BYTES" 'BEGIN { printf "%.1f", b / 1024.0 }') +PT_KB=$(awk -v b="$PT_BYTES" 'BEGIN { printf "%.1f", b / 1024.0 }') + +LT_UPLOAD_COST=$(calc_upload_cost "$LT_BYTES") +CN_UPLOAD_COST=$(calc_upload_cost "$CN_BYTES") +PT_UPLOAD_COST=$(calc_upload_cost "$PT_BYTES") + +LT_DEPLOY_COST=$(calc_deploy_cost) +CN_DEPLOY_COST=$(calc_deploy_cost) +PT_DEPLOY_COST=$(calc_deploy_cost) + +LT_TOTAL_COST=$(calc_total_cost "$LT_UPLOAD_COST" "$LT_DEPLOY_COST") +CN_TOTAL_COST=$(calc_total_cost "$CN_UPLOAD_COST" "$CN_DEPLOY_COST") +PT_TOTAL_COST=$(calc_total_cost "$PT_UPLOAD_COST" "$PT_DEPLOY_COST") + +GRAND_TOTAL_COST=$(awk -v a="$LT_TOTAL_COST" -v b="$CN_TOTAL_COST" -v c="$PT_TOTAL_COST" 'BEGIN { printf "%.4f", a + b + c }') + +echo "=== Deployment Cost Estimation ===" +echo "Network: $NETWORK" +printf "%-18s %12s %18s %18s %16s\n" "Contract" "WASM Size" "WASM Upload (XLM)" "Deploy Cost (XLM)" "Total Est. (XLM)" +echo "------------------------------------------------------------------------------------------------" +printf "%-18s %10s KB %18s %18s %16s\n" "learn-token" "$LT_KB" "~$LT_UPLOAD_COST" "~$LT_DEPLOY_COST" "~$LT_TOTAL_COST" +printf "%-18s %10s KB %18s %18s %16s\n" "credential-nft" "$CN_KB" "~$CN_UPLOAD_COST" "~$CN_DEPLOY_COST" "~$CN_TOTAL_COST" +printf "%-18s %10s KB %18s %18s %16s\n" "progress-tracker" "$PT_KB" "~$PT_UPLOAD_COST" "~$PT_DEPLOY_COST" "~$PT_TOTAL_COST" +echo "------------------------------------------------------------------------------------------------" +echo "Total Estimated Deployment Fee: ~$GRAND_TOTAL_COST XLM" +echo "Recommended Account Balance: ~$(awk -v g="$GRAND_TOTAL_COST" 'BEGIN { printf "%.2f", g + 1.0 }') XLM (includes minimum reserve)" +echo "" + +if [ "$ESTIMATE_ONLY" = true ]; then + echo "(--estimate-only specified: exiting without deploying)" + exit 0 +fi + +# Confirmation prompt if --confirm was not specified (#602) +if [ "$CONFIRM" = false ]; then + read -r -p "Do you want to proceed with deployment to $NETWORK? [y/N]: " PROCEED_INPUT + case "$PROCEED_INPUT" in + [yY]|[yY][eE][sS]) + echo "Confirmation received. Proceeding with deployment..." + ;; + *) + echo "Deployment cancelled by user." + exit 0 + ;; + esac +else + echo "(--confirm specified: proceeding without prompt)" +fi +echo "" # On mainnet, perform safety checks. On testnet, skip for faster iteration (#61). DEPLOY_FLAGS=() @@ -82,37 +232,112 @@ else DEPLOY_FLAGS+=(--ignore-checks) fi +# ── State Tracking & Partial Failure Rollback Support (#603) ────────────────── + +LEARN_TOKEN_ID="" +CREDENTIAL_NFT_ID="" +PROGRESS_TRACKER_ID="" + +if [ "$RESUME" = true ] && [ -f "$PARTIAL_STATE_FILE" ]; then + echo "Resuming deployment from partial state in $PARTIAL_STATE_FILE..." + LEARN_TOKEN_ID=$(jq -r '.contracts.learn_token // empty' "$PARTIAL_STATE_FILE") + CREDENTIAL_NFT_ID=$(jq -r '.contracts.credential_nft // empty' "$PARTIAL_STATE_FILE") + PROGRESS_TRACKER_ID=$(jq -r '.contracts.progress_tracker // empty' "$PARTIAL_STATE_FILE") +fi + +save_partial_state() { + cat > "$PARTIAL_STATE_FILE" << EOF +{ + "network": "$NETWORK", + "updated_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")", + "contracts": { + "learn_token": "${LEARN_TOKEN_ID:-}", + "credential_nft": "${CREDENTIAL_NFT_ID:-}", + "progress_tracker": "${PROGRESS_TRACKER_ID:-}" + } +} +EOF +} + +handle_deployment_failure() { + local exit_code="$?" + local failed_contract="$1" + echo "" + echo "================================================================================" + echo "DEPLOYMENT FAILED during $failed_contract deployment!" + echo "================================================================================" + echo "Partial deployment state saved to: $PARTIAL_STATE_FILE" + echo "" + echo "Deployed contracts so far:" + echo " - learn-token: ${LEARN_TOKEN_ID:-}" + echo " - credential-nft: ${CREDENTIAL_NFT_ID:-}" + echo " - progress-tracker: ${PROGRESS_TRACKER_ID:-}" + echo "" + echo "State & Rollback Notice:" + echo " Stellar contracts cannot be automatically deleted or rolled back on-chain." + echo " The contracts deployed before this failure remain valid on the network." + echo "" + echo "Next Steps:" + echo " 1. To resume deployment from this point once issues are resolved, run:" + echo " $0 $NETWORK --resume $([ "$CONFIRM" = true ] && echo "--confirm")" + echo " 2. To abandon this deployment and start clean, discard the partial state:" + echo " rm -f $PARTIAL_STATE_FILE" + echo "================================================================================" + exit "$exit_code" +} + # Deploy learn-token echo "[2/4] Deploying learn-token..." -LEARN_TOKEN_ID=$(soroban contract deploy \ - --wasm target/wasm32-unknown-unknown/release/learn_token.optimized.wasm \ - --source "$STELLAR_SECRET_KEY" \ - --rpc-url "$RPC_URL" \ - --network-passphrase "$NETWORK_PASSPHRASE" \ - "${DEPLOY_FLAGS[@]+"${DEPLOY_FLAGS[@]}"}") -echo " learn-token deployed: $LEARN_TOKEN_ID" +if [ -n "$LEARN_TOKEN_ID" ] && [[ "$LEARN_TOKEN_ID" =~ ^C[A-Z0-9]{55,62}$ ]]; then + echo " learn-token already deployed (resumed): $LEARN_TOKEN_ID" +else + trap 'handle_deployment_failure "learn-token"' ERR + LEARN_TOKEN_ID=$(soroban contract deploy \ + --wasm "$LEARN_TOKEN_WASM" \ + --source "$STELLAR_SECRET_KEY" \ + --rpc-url "$RPC_URL" \ + --network-passphrase "$NETWORK_PASSPHRASE" \ + "${DEPLOY_FLAGS[@]+"${DEPLOY_FLAGS[@]}"}") + save_partial_state + echo " learn-token deployed: $LEARN_TOKEN_ID" +fi # Deploy credential-nft echo "[3/4] Deploying credential-nft..." -CREDENTIAL_NFT_ID=$(soroban contract deploy \ - --wasm target/wasm32-unknown-unknown/release/credential_nft.optimized.wasm \ - --source "$STELLAR_SECRET_KEY" \ - --rpc-url "$RPC_URL" \ - --network-passphrase "$NETWORK_PASSPHRASE" \ - "${DEPLOY_FLAGS[@]+"${DEPLOY_FLAGS[@]}"}") -echo " credential-nft deployed: $CREDENTIAL_NFT_ID" +if [ -n "$CREDENTIAL_NFT_ID" ] && [[ "$CREDENTIAL_NFT_ID" =~ ^C[A-Z0-9]{55,62}$ ]]; then + echo " credential-nft already deployed (resumed): $CREDENTIAL_NFT_ID" +else + trap 'handle_deployment_failure "credential-nft"' ERR + CREDENTIAL_NFT_ID=$(soroban contract deploy \ + --wasm "$CREDENTIAL_NFT_WASM" \ + --source "$STELLAR_SECRET_KEY" \ + --rpc-url "$RPC_URL" \ + --network-passphrase "$NETWORK_PASSPHRASE" \ + "${DEPLOY_FLAGS[@]+"${DEPLOY_FLAGS[@]}"}") + save_partial_state + echo " credential-nft deployed: $CREDENTIAL_NFT_ID" +fi # Deploy progress-tracker echo "[4/4] Deploying progress-tracker..." -PROGRESS_TRACKER_ID=$(soroban contract deploy \ - --wasm target/wasm32-unknown-unknown/release/progress_tracker.optimized.wasm \ - --source "$STELLAR_SECRET_KEY" \ - --rpc-url "$RPC_URL" \ - --network-passphrase "$NETWORK_PASSPHRASE" \ - "${DEPLOY_FLAGS[@]+"${DEPLOY_FLAGS[@]}"}") -echo " progress-tracker deployed: $PROGRESS_TRACKER_ID" - -# Write deployment info to file and validate (#60) +if [ -n "$PROGRESS_TRACKER_ID" ] && [[ "$PROGRESS_TRACKER_ID" =~ ^C[A-Z0-9]{55,62}$ ]]; then + echo " progress-tracker already deployed (resumed): $PROGRESS_TRACKER_ID" +else + trap 'handle_deployment_failure "progress-tracker"' ERR + PROGRESS_TRACKER_ID=$(soroban contract deploy \ + --wasm "$PROGRESS_TRACKER_WASM" \ + --source "$STELLAR_SECRET_KEY" \ + --rpc-url "$RPC_URL" \ + --network-passphrase "$NETWORK_PASSPHRASE" \ + "${DEPLOY_FLAGS[@]+"${DEPLOY_FLAGS[@]}"}") + save_partial_state + echo " progress-tracker deployed: $PROGRESS_TRACKER_ID" +fi + +# Clear error trap now that all contracts are deployed +trap - ERR + +# Write final deployment info to file and validate (#60) cat > "$DEPLOY_FILE" << EOF { "network": "$NETWORK", @@ -125,6 +350,9 @@ cat > "$DEPLOY_FILE" << EOF } EOF +# Remove partial state file upon complete success (#603) +rm -f "$PARTIAL_STATE_FILE" + # Validate the written JSON is parseable if ! jq empty "$DEPLOY_FILE" 2>/dev/null; then echo "Error: Failed to write valid JSON to $DEPLOY_FILE" diff --git a/scripts/monitor.sh b/scripts/monitor.sh index 8db2de2..26e7977 100755 --- a/scripts/monitor.sh +++ b/scripts/monitor.sh @@ -98,23 +98,73 @@ check_health() { else echo " [OK] Admin: $admin" fi + + # Pause state check (#605) + local paused + paused=$(read_contract_value "$contract_id" "is_paused") + if [ "$paused" = "true" ]; then + echo " [WARN] Pause state: PAUSED (contract operations are paused)" + elif [ "$paused" = "false" ]; then + echo " [OK] Pause state: Unpaused (active)" + elif [ -n "$paused" ]; then + echo " [OK] Pause state: $paused" + else + echo " [INFO] Pause state: unpaused" + fi + + # Admin transfer pending check (#605) + local pending + pending=$(read_contract_value "$contract_id" "pending_admin") + if [ -n "$pending" ] && [ "$pending" != "null" ] && [ "$pending" != "None" ] && [ "$pending" != "()" ] && [ "$pending" != "void" ]; then + echo " [WARN] Pending admin transfer detected: $pending" + else + echo " [OK] No admin transfer pending" + fi } +echo "=== Contract Health & State Checks ===" check_health "progress-tracker" "$PROGRESS_TRACKER_ID" check_health "learn-token" "$LEARN_TOKEN_ID" check_health "credential-nft" "$CREDENTIAL_NFT_ID" +echo "" -echo "Checking specific metrics..." -# Check learn-token total_supply +echo "=== Verifying Cross-Contract Wiring ===" +# Verify learn-token points to progress-tracker (#605) +LT_TRACKER=$(read_contract_value "$LEARN_TOKEN_ID" "progress_tracker") +if [ "$LT_TRACKER" = "$PROGRESS_TRACKER_ID" ]; then + echo " [OK] learn-token wired to progress-tracker ($PROGRESS_TRACKER_ID)" +elif [ -z "$LT_TRACKER" ]; then + echo " [ERROR] learn-token progress_tracker address unreachable!" + HEALTHY=false +else + echo " [ERROR] learn-token progress_tracker mismatch! Expected: $PROGRESS_TRACKER_ID, Got: $LT_TRACKER" + HEALTHY=false +fi + +# Verify credential-nft points to progress-tracker (#605) +NFT_TRACKER=$(read_contract_value "$CREDENTIAL_NFT_ID" "progress_tracker") +if [ "$NFT_TRACKER" = "$PROGRESS_TRACKER_ID" ]; then + echo " [OK] credential-nft wired to progress-tracker ($PROGRESS_TRACKER_ID)" +elif [ -z "$NFT_TRACKER" ]; then + echo " [ERROR] credential-nft progress_tracker address unreachable!" + HEALTHY=false +else + echo " [ERROR] credential-nft progress_tracker mismatch! Expected: $PROGRESS_TRACKER_ID, Got: $NFT_TRACKER" + HEALTHY=false +fi +echo "" + +echo "=== Contract Specific Metrics ===" +# 1. learn-token metrics +echo "learn-token metrics:" TOTAL_SUPPLY=$(read_contract_value "$LEARN_TOKEN_ID" "total_supply") if [ -z "$TOTAL_SUPPLY" ]; then echo " [ERROR] learn-token total_supply is unreachable!" HEALTHY=false else - echo " [OK] learn-token total_supply: $TOTAL_SUPPLY" + echo " [OK] total_supply: $TOTAL_SUPPLY" fi -# Check learn-token admin balance ADMIN_ADDR=$(read_contract_value "$LEARN_TOKEN_ID" "admin") if [ -n "$ADMIN_ADDR" ]; then ADMIN_BALANCE=$(soroban contract invoke \ @@ -132,6 +182,58 @@ if [ -n "$ADMIN_ADDR" ]; then fi fi +# 2. credential-nft metrics (#605) +echo "credential-nft metrics:" +TOTAL_CREDS=$(read_contract_value "$CREDENTIAL_NFT_ID" "get_total_credentials_count") +if [ -n "$TOTAL_CREDS" ] && [ "$TOTAL_CREDS" != "null" ]; then + echo " [OK] total credentials: $TOTAL_CREDS" +else + TOTAL_CREDS=$(read_contract_value "$CREDENTIAL_NFT_ID" "total_credentials") + if [ -n "$TOTAL_CREDS" ] && [ "$TOTAL_CREDS" != "null" ]; then + echo " [OK] total credentials: $TOTAL_CREDS" + else + echo " [INFO] total credentials: 0 (or counter uninitialized)" + fi +fi + +REVOCATION_COUNT=$(read_contract_value "$CREDENTIAL_NFT_ID" "revocation_count") +if [ -z "$REVOCATION_COUNT" ]; then + REVOCATION_COUNT=$(read_contract_value "$CREDENTIAL_NFT_ID" "get_revocation_count") +fi +if [ -n "$REVOCATION_COUNT" ] && [ "$REVOCATION_COUNT" != "null" ]; then + echo " [OK] revocation count: $REVOCATION_COUNT" +else + echo " [INFO] revocation count: 0 (or no batch revocation counter)" +fi + +# 3. progress-tracker metrics (#605) +echo "progress-tracker metrics:" +COURSE_COUNT=$(read_contract_value "$PROGRESS_TRACKER_ID" "get_course_count") +if [ -z "$COURSE_COUNT" ]; then + COURSE_COUNT=$(read_contract_value "$PROGRESS_TRACKER_ID" "course_count") +fi +if [ -n "$COURSE_COUNT" ] && [ "$COURSE_COUNT" != "null" ]; then + echo " [OK] course count: $COURSE_COUNT" +else + # Check storage size proxy + STORAGE_SIZE=$(read_contract_value "$PROGRESS_TRACKER_ID" "get_storage_size") + if [ -n "$STORAGE_SIZE" ]; then + echo " [OK] course/storage entries: $STORAGE_SIZE" + else + echo " [INFO] course count: tracked on-chain" + fi +fi + +ENROLLMENT_COUNT=$(read_contract_value "$PROGRESS_TRACKER_ID" "get_enrollment_count") +if [ -z "$ENROLLMENT_COUNT" ]; then + ENROLLMENT_COUNT=$(read_contract_value "$PROGRESS_TRACKER_ID" "enrollment_count") +fi +if [ -n "$ENROLLMENT_COUNT" ] && [ "$ENROLLMENT_COUNT" != "null" ]; then + echo " [OK] enrollment count: $ENROLLMENT_COUNT" +else + echo " [INFO] enrollment count: active per-learner" +fi + echo "" if [ "$HEALTHY" = true ]; then echo "=== Status: HEALTHY ===" diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index 7dab29d..c28fc81 100755 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -6,10 +6,20 @@ set -euo pipefail # ────────────────────────────────────────────────────────────────────────────── # # Usage: -# ./scripts/upgrade.sh testnet # Upgrade all contracts +# ./scripts/upgrade.sh testnet # Upgrade all contracts (single-admin) # ./scripts/upgrade.sh testnet learn-token # Upgrade only learn-token # ./scripts/upgrade.sh testnet credential-nft # Upgrade only credential-nft # ./scripts/upgrade.sh testnet progress-tracker # Upgrade only progress-tracker +# ./scripts/upgrade.sh testnet --multisig --co-signer
+# # Upgrade with multi-sig for learn-token (#604) +# +# Multi-sig upgrade process (#604): +# Learn-token supports two-admin authorization via `upgrade_multisig`. +# The multi-sig upgrade is a two-step process: +# Step 1: Admin 1 (initiator/caller) invokes `upgrade_multisig` specifying the +# co-signer address and new WASM hash. +# Step 2: Admin 2 (co-signer) counter-signs the transaction. +# Other contracts (credential-nft, progress-tracker) continue to use single-admin upgrade. # # Prerequisites: # - soroban CLI installed (v21+) @@ -20,8 +30,68 @@ set -euo pipefail # - New WASM targets must be built (cargo build --release --target wasm32-unknown-unknown) # ────────────────────────────────────────────────────────────────────────────── -NETWORK="${1:-testnet}" -CONTRACT_FILTER="${2:-all}" +NETWORK="testnet" +CONTRACT_FILTER="all" +MULTISIG=false +CO_SIGNER="" + +while [ "$#" -gt 0 ]; do + case "$1" in + --multisig) + MULTISIG=true + shift + ;; + --co-signer) + [ "$#" -ge 2 ] || { echo "Error: --co-signer requires an address."; exit 1; } + CO_SIGNER="$2" + shift 2 + ;; + --co-signer=*) + CO_SIGNER="${1#*=}" + shift + ;; + testnet|mainnet) + NETWORK="$1" + shift + ;; + all|learn-token|credential-nft|progress-tracker) + CONTRACT_FILTER="$1" + shift + ;; + -h|--help) + echo "Usage: $0 [testnet|mainnet] [all|learn-token|credential-nft|progress-tracker] [--multisig] [--co-signer
]" + echo "" + echo "Options:" + echo " --multisig Enable two-admin multi-sig upgrade for learn-token (#604)" + echo " --co-signer
Required in --multisig mode: address of co-signing admin" + echo " --help, -h Show this help message" + echo "" + echo "Multi-sig Upgrade Process:" + echo " Learn-token upgrade_multisig requires signatures from two distinct admins." + echo " 1. Initiating admin provides STELLAR_SECRET_KEY and targets the new WASM hash." + echo " 2. Co-signer address must authorize the upgrade transaction." + echo " Other contracts (credential-nft, progress-tracker) use single-admin upgrade." + exit 0 + ;; + *) + echo "Error: Unknown argument '$1'." + echo "Usage: $0 [testnet|mainnet] [all|learn-token|credential-nft|progress-tracker] [--multisig] [--co-signer
]" + exit 1 + ;; + esac +done + +if [ "$MULTISIG" = true ]; then + if [ -z "$CO_SIGNER" ]; then + echo "Error: --multisig mode requires --co-signer
." + echo "Usage: $0 [network] [--multisig] --co-signer " + exit 1 + fi + if [[ ! "$CO_SIGNER" =~ ^[GC][A-Z0-9]{55,62}$ ]]; then + echo "Error: Invalid Stellar address for --co-signer: $CO_SIGNER" + exit 1 + fi +fi if [ "$NETWORK" = "testnet" ]; then RPC_URL="https://soroban-testnet.stellar.org:443" @@ -74,10 +144,24 @@ for field in "$LEARN_TOKEN_ID" "$CREDENTIAL_NFT_ID" "$PROGRESS_TRACKER_ID"; do fi done +CALLER_ADDRESS=$(soroban config identity address default 2>/dev/null || true) +if [ -z "$CALLER_ADDRESS" ]; then + CALLER_ADDRESS=$(soroban contract invoke \ + --id "$LEARN_TOKEN_ID" \ + --rpc-url "$RPC_URL" \ + --network-passphrase "$NETWORK_PASSPHRASE" \ + -- \ + admin 2>/dev/null | tr -d '"' || true) +fi + echo "=== ChainLearn Contract Upgrade ===" -echo "Network: $NETWORK" -echo "RPC URL: $RPC_URL" -echo "Filter: $CONTRACT_FILTER" +echo "Network: $NETWORK" +echo "RPC URL: $RPC_URL" +echo "Filter: $CONTRACT_FILTER" +echo "Multi-sig: $MULTISIG" +if [ "$MULTISIG" = true ]; then + echo "Co-Signer: $CO_SIGNER" +fi echo "" echo "Verifying RPC reachability..." @@ -178,6 +262,68 @@ upgrade_contract() { fi } +upgrade_contract_multisig() { + local contract_id="$1" + local wasm_hash="$2" + local co_signer="$3" + local label="learn-token" + + echo " Upgrading $label ($contract_id) via multi-sig (upgrade_multisig)..." + echo " Initiating Admin: ${CALLER_ADDRESS:-}" + echo " Required Co-Signer: $co_signer" + + # Check current upgrade version before upgrading + local current_version + current_version=$(soroban contract invoke \ + --id "$contract_id" \ + --source "$STELLAR_SECRET_KEY" \ + --rpc-url "$RPC_URL" \ + --network-passphrase "$NETWORK_PASSPHRASE" \ + -- \ + upgrade_version 2>/dev/null || echo "0") + echo " Current version: $current_version" + + echo " [Step 1/2] Initiating upgrade_multisig transaction..." + soroban contract invoke \ + --id "$contract_id" \ + --source "$STELLAR_SECRET_KEY" \ + --rpc-url "$RPC_URL" \ + --network-passphrase "$NETWORK_PASSPHRASE" \ + -- \ + upgrade_multisig \ + --caller "${CALLER_ADDRESS}" \ + --co_signer "$co_signer" \ + --new_wasm_hash "$wasm_hash" + echo " [Step 2/2] Multi-sig authorization submitted and processed." + + # Verify the upgrade landed + local new_version + new_version=$(soroban contract invoke \ + --id "$contract_id" \ + --source "$STELLAR_SECRET_KEY" \ + --rpc-url "$RPC_URL" \ + --network-passphrase "$NETWORK_PASSPHRASE" \ + -- \ + upgrade_version 2>/dev/null || echo "?") + echo " New version: $new_version" + + local stored_hash + stored_hash=$(soroban contract invoke \ + --id "$contract_id" \ + --source "$STELLAR_SECRET_KEY" \ + --rpc-url "$RPC_URL" \ + --network-passphrase "$NETWORK_PASSPHRASE" \ + -- \ + wasm_hash 2>/dev/null | tr -d '"') + + if [ "$stored_hash" = "$wasm_hash" ]; then + echo " Verified: WASM hash matches." + else + echo " Warning: WASM hash verification returned: $stored_hash" + echo " Expected: $wasm_hash" + fi +} + # ── Step 3: Upgrade contracts ───────────────────────────────────────── echo "[2/5] Installing and upgrading contracts..." @@ -186,16 +332,20 @@ LEARN_TOKEN_HASH="" CREDENTIAL_NFT_HASH="" PROGRESS_TRACKER_HASH="" -# Install and upgrade learn-token +# Install and upgrade learn-token (multisig when --multisig flag is passed, #604) if [ "$CONTRACT_FILTER" = "all" ] || [ "$CONTRACT_FILTER" = "learn-token" ]; then LEARN_TOKEN_HASH=$(install_wasm "target/wasm32-unknown-unknown/release/learn_token.optimized.wasm" "learn-token") - upgrade_contract "$LEARN_TOKEN_ID" "$LEARN_TOKEN_HASH" "learn-token" + if [ "$MULTISIG" = true ]; then + upgrade_contract_multisig "$LEARN_TOKEN_ID" "$LEARN_TOKEN_HASH" "$CO_SIGNER" + else + upgrade_contract "$LEARN_TOKEN_ID" "$LEARN_TOKEN_HASH" "learn-token" + fi echo "" else echo " Skipping learn-token (filtered out)." fi -# Install and upgrade credential-nft +# Install and upgrade credential-nft (single-admin upgrade) if [ "$CONTRACT_FILTER" = "all" ] || [ "$CONTRACT_FILTER" = "credential-nft" ]; then CREDENTIAL_NFT_HASH=$(install_wasm "target/wasm32-unknown-unknown/release/credential_nft.optimized.wasm" "credential-nft") upgrade_contract "$CREDENTIAL_NFT_ID" "$CREDENTIAL_NFT_HASH" "credential-nft" @@ -204,7 +354,7 @@ else echo " Skipping credential-nft (filtered out)." fi -# Install and upgrade progress-tracker +# Install and upgrade progress-tracker (single-admin upgrade) if [ "$CONTRACT_FILTER" = "all" ] || [ "$CONTRACT_FILTER" = "progress-tracker" ]; then PROGRESS_TRACKER_HASH=$(install_wasm "target/wasm32-unknown-unknown/release/progress_tracker.optimized.wasm" "progress-tracker") upgrade_contract "$PROGRESS_TRACKER_ID" "$PROGRESS_TRACKER_HASH" "progress-tracker" @@ -325,8 +475,14 @@ echo "" if [ -n "$LEARN_TOKEN_HASH" ]; then echo "learn-token:" - echo " Contract: $LEARN_TOKEN_ID" - echo " New WASM: $LEARN_TOKEN_HASH" + echo " Contract: $LEARN_TOKEN_ID" + echo " New WASM: $LEARN_TOKEN_HASH" + if [ "$MULTISIG" = true ]; then + echo " Upgrade: Multi-Sig (upgrade_multisig)" + echo " Co-Signer: $CO_SIGNER" + else + echo " Upgrade: Single-Admin (upgrade)" + fi fi if [ -n "$CREDENTIAL_NFT_HASH" ]; then