diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02ef1c5..3324c53 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -304,6 +304,9 @@ jobs: exit 1 fi + - name: Test validate-deployment script (mocked CLI) + run: bash scripts/validate-deployment.test.sh + stellar-kani: needs: changes if: needs.changes.outputs.stellar == 'true' @@ -350,7 +353,6 @@ jobs: defaults: run: working-directory: stellar - continue-on-error: true steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -388,16 +390,48 @@ jobs: RPC_URL: https://rpc-futurenet.stellar.org IDENTITY_NAME: wraith-dryrun-ci run: | + set -euo pipefail ./scripts/deploy-dryrun.sh 2>&1 | tee deploy-dryrun-output.txt + exit_code=${PIPESTATUS[0]} + if [ $exit_code -ne 0 ]; then + echo "::error::Deploy dry-run script failed with exit code $exit_code" + exit $exit_code + fi + + - name: Validate deployment output + run: | + chmod +x ./scripts/validate-deployment.sh + ./scripts/validate-deployment.sh deploy-dryrun-output.txt futurenet - name: Parse and save contract IDs run: | cat deploy-dryrun-output.txt | grep -E '(stealth-announcer|stealth-registry|stealth-sender|wraith-names)\s' | \ - awk '{print $1, $2}' > contract-ids.txt || true + awk '{print $1, $2}' > contract-ids.txt + + # Verify contract-ids.txt is not empty + if [ ! -s contract-ids.txt ]; then + echo "::error::contract-ids.txt is empty - failed to parse contract IDs" + exit 1 + fi + + # Count lines to verify all 4 contracts are present + contract_count=$(wc -l < contract-ids.txt) + if [ "$contract_count" -ne 4 ]; then + echo "::error::Expected 4 contract IDs, found $contract_count" + cat contract-ids.txt + exit 1 + fi + echo "### Deploy Dry-Run Results" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY cat deploy-dryrun-output.txt >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Contract Manifest" >> $GITHUB_STEP_SUMMARY + echo '```json' >> $GITHUB_STEP_SUMMARY + cat deploy-dryrun-output-manifest.json >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY - name: Upload dry-run output uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 @@ -406,6 +440,7 @@ jobs: name: deploy-dryrun-results path: | stellar/deploy-dryrun-output.txt + stellar/deploy-dryrun-output-manifest.json stellar/contract-ids.txt stellar-chaos-nightly: diff --git a/stellar/scripts/VALIDATION_README.md b/stellar/scripts/VALIDATION_README.md new file mode 100644 index 0000000..7ab8be9 --- /dev/null +++ b/stellar/scripts/VALIDATION_README.md @@ -0,0 +1,144 @@ +# Stellar Deployment Validation + +## Overview + +This directory contains scripts for validating Stellar contract deployments to ensure they complete successfully and produce valid contract IDs. + +## Scripts + +### `deploy-dryrun.sh` +The main deployment script that deploys all Wraith Protocol Stellar contracts to futurenet and runs smoke tests. + +### `validate-deployment.sh` +Validation script that ensures deployment output meets requirements: +- All required contract IDs are present (stealth-announcer, stealth-registry, stealth-sender, wraith-names) +- Contract IDs are valid Stellar addresses (start with 'C', 56 characters, valid base32) +- Contract IDs match the expected network (via stellar.expert URLs) +- **Contract IDs exist on the target network (via RPC query)** +- Deployment completed successfully +- Generates a JSON manifest with validated contract IDs + +**Usage:** +```bash +./validate-deployment.sh +``` + +**Example:** +```bash +./validate-deployment.sh deploy-dryrun-output.txt futurenet +``` + +**Environment Variables:** +- `RPC_URL`: Override the default RPC URL for the network + +**Exit Codes:** +- 0: All validations passed +- 1: Validation failed + +**Outputs:** +- `-manifest.json`: JSON manifest with contract IDs and validation status (always written prior to exit on both success and failure paths) + +**RPC Verification:** +The script queries the Stellar RPC for each contract using `stellar contract info wasm-hash` to verify that the contract actually exists on the specified network. This ensures the deployment was successful and the contracts are accessible. + +### `validate-deployment.test.sh` +Unit tests for the validation script covering: +- Valid deployment output +- Missing contract IDs +- Invalid contract ID formats +- Wrong network detection +- Smoke test failures +- Early failure (missing output file) generating failed manifest and preserving exit code +- Late failure (RPC lookup failure) generating failed manifest and preserving exit code +- Unexpected error/panic line with success marker generating failed manifest and preserving non-zero exit code + +**Usage:** +```bash +./validate-deployment.test.sh +``` + +## CI Integration + +The validation is integrated into the `.github/workflows/ci.yml` workflow in the `stellar-deploy-dryrun` job: + +1. **Removed `continue-on-error: true`** - Job now fails on errors +2. **Removed `|| true` from parsing** - Parsing failures now fail the job +3. **Added exit code checking** - Deploy script failures are caught and reported +4. **Added validation step** - Runs `validate-deployment.sh` to verify output +5. **Added contract count verification** - Ensures all 4 contracts are present +6. **Added manifest artifact** - Uploads JSON manifest with validated IDs + +## Validation Requirements + +### Contract ID Format +- Must start with 'C' +- Must be exactly 56 characters long +- Must contain only valid base32 characters (A-Z, 2-7) + +### Required Contracts +1. stealth-announcer +2. stealth-registry +3. stealth-sender +4. wraith-names + +### Network Validation +Contract IDs are verified against the expected network by: +1. Checking stellar.expert URLs in the deployment output +2. **Querying the RPC endpoint to verify each contract exists on-chain** + +The RPC verification uses `stellar contract info wasm-hash` which: +- Queries the network's RPC endpoint +- Retrieves the WASM hash for the contract +- Fails if the contract doesn't exist on the specified network + +## Artifacts + +The following artifacts are uploaded on every run (success or failure): +- `deploy-dryrun-output.txt` - Full deployment output +- `deploy-dryrun-output-manifest.json` - JSON manifest with validated IDs +- `contract-ids.txt` - Parsed contract IDs (legacy format) + +## Example Manifest + +```json +{ + "network": "futurenet", + "timestamp": "2026-09-27T12:34:56Z", + "contracts": { + "stealth-announcer": "CABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRS", + "stealth-registry": "CBCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRST", + "stealth-sender": "CCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTU", + "wraith-names": "CDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUV" + }, + "validation": { + "status": "passed", + "required_contracts": 4, + "found_contracts": 4 + } +} +``` + +## Troubleshooting + +### Validation Fails with "Missing contract ID" +Check the deployment output to ensure the contract was deployed successfully. Look for error messages in the deploy step. + +### Validation Fails with "Invalid format" +The contract ID doesn't match Stellar's address format. This usually indicates a parsing error or corrupted output. + +### Validation Fails with "Network mismatch" +The deployment was made to a different network than expected. Verify the `RPC_URL` and `NETWORK` environment variables. + +### Validation Fails with "Contract ID does not exist on network" +The contract ID is valid but doesn't exist on the specified network. This indicates: +- The deployment may have failed silently +- The wrong network was specified for validation +- The RPC endpoint is not in sync + +Check the deployment output for errors and verify the RPC_URL matches the deployment network. + +### Smoke tests failed +Check the deployment output for specific test failures. Common causes: +- Network connectivity issues +- Insufficient account balance +- Contract initialization errors diff --git a/stellar/scripts/validate-deployment.sh b/stellar/scripts/validate-deployment.sh new file mode 100644 index 0000000..fa64fd1 --- /dev/null +++ b/stellar/scripts/validate-deployment.sh @@ -0,0 +1,323 @@ +#!/usr/bin/env bash +# stellar/scripts/validate-deployment.sh +# +# Validates the deployment output from deploy-dryrun.sh to ensure: +# 1. All required contract IDs are present +# 2. Contract IDs are valid Stellar contract addresses +# 3. Contract IDs match the expected network (futurenet) +# +# Usage: +# ./validate-deployment.sh +# +# Arguments: +# output-file: Path to the deployment output file +# network: Expected network (e.g., futurenet, testnet, mainnet) +# +# Exit codes: +# 0: All validations passed +# 1: Validation failed + +set -euo pipefail + +OUTPUT_FILE="${1:?Usage: $0 }" +NETWORK="${2:-futurenet}" + +# Color helpers +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color +BOLD='\033[1m' + +ok() { printf " ${GREEN}✓${NC} %s\n" "$*"; } +fail() { printf " ${RED}✗${NC} %s\n" "$*" >&2; } +info() { printf " ${BLUE}→${NC} %s\n" "$*"; } +header() { printf "\n${BOLD}═══ %s ═══${NC}\n" "$*"; } + +# Required contracts +REQUIRED_CONTRACTS=("stealth-announcer" "stealth-registry" "stealth-sender" "wraith-names") +VALIDATION_FAILED=0 +declare -A CONTRACT_IDS + +MANIFEST_FILE="${OUTPUT_FILE%.txt}-manifest.json" + +write_manifest() { + cat > "$MANIFEST_FILE" <> "$MANIFEST_FILE" + fi + echo -n " \"$contract\": \"$cid\"" >> "$MANIFEST_FILE" + first=0 + fi + done + + local found_contracts=0 + for contract in "${REQUIRED_CONTRACTS[@]}"; do + if [ -n "${CONTRACT_IDS[$contract]:-}" ]; then + found_contracts=$((found_contracts + 1)) + fi + done + + cat >> "$MANIFEST_FILE" </dev/null; then + STELLAR_CLI="stellar" + elif command -v soroban &>/dev/null; then + STELLAR_CLI="soroban" + else + fail "Neither 'stellar' nor 'soroban' CLI found in PATH" + info "Install stellar-cli to enable RPC verification" + VALIDATION_FAILED=1 + STELLAR_CLI="" + fi + + if [ -n "$STELLAR_CLI" ]; then + # Configure network for CLI + $STELLAR_CLI network add \ + --global \ + --rpc-url "$RPC_URL" \ + --network-passphrase "$NETWORK_PASSPHRASE" \ + "validation-$NETWORK" 2>/dev/null || true + + # Verify each contract exists on the network + for contract in "${REQUIRED_CONTRACTS[@]}"; do + CONTRACT_ID="${CONTRACT_IDS[$contract]:-}" + + if [ -z "$CONTRACT_ID" ]; then + continue # Already reported as missing + fi + + info "Verifying $contract on network..." + + # Use `stellar contract info` to check if contract exists + # This queries the RPC and fails if contract doesn't exist + CONTRACT_INFO=$($STELLAR_CLI contract info wasm-hash \ + --id "$CONTRACT_ID" \ + --network "validation-$NETWORK" \ + 2>&1) + + EXIT_CODE=$? + + if [ $EXIT_CODE -eq 0 ] && [ -n "$CONTRACT_INFO" ]; then + # Contract exists and returned info + ok "$contract: Verified on $NETWORK (wasm hash: ${CONTRACT_INFO:0:16}...)" + elif echo "$CONTRACT_INFO" | grep -qi "not found\|does not exist\|invalid\|error"; then + # Contract doesn't exist or query failed + fail "$contract: Contract ID does not exist on $NETWORK" + fail " Contract ID: $CONTRACT_ID" + fail " Error: $(echo "$CONTRACT_INFO" | head -1)" + VALIDATION_FAILED=1 + else + # Unexpected response + fail "$contract: Could not verify contract on $NETWORK" + info " Exit code: $EXIT_CODE" + info " Response: ${CONTRACT_INFO:0:100}" + VALIDATION_FAILED=1 + fi + done + + # Clean up temporary network config + $STELLAR_CLI network rm "validation-$NETWORK" 2>/dev/null || true + fi +fi + +header "Contract Manifest" +echo "" +ok "Manifest saved to: $MANIFEST_FILE" + +# Print summary +header "Validation Summary" + +echo "" +printf " ${BOLD}%-22s %-56s %-10s${NC}\n" "Contract" "Contract ID" "Status" +printf " ${BOLD}%-22s %-56s %-10s${NC}\n" "───────" "───────────" "──────" + +for contract in "${REQUIRED_CONTRACTS[@]}"; do + CONTRACT_ID="${CONTRACT_IDS[$contract]:-MISSING}" + if [ "$CONTRACT_ID" = "MISSING" ]; then + STATUS="${RED}✗ MISSING${NC}" + else + STATUS="${GREEN}✓ VALID${NC}" + fi + printf " %-22s %-56s " "$contract" "$CONTRACT_ID" + printf "${STATUS}\n" +done + +echo "" + +if [ $VALIDATION_FAILED -ne 0 ]; then + fail "Validation FAILED - deployment output contains errors or missing contract IDs" + exit 1 +fi + +ok "All validations PASSED" +echo "" +exit 0 diff --git a/stellar/scripts/validate-deployment.test.sh b/stellar/scripts/validate-deployment.test.sh new file mode 100644 index 0000000..9f617b9 --- /dev/null +++ b/stellar/scripts/validate-deployment.test.sh @@ -0,0 +1,424 @@ +#!/usr/bin/env bash +# stellar/scripts/validate-deployment.test.sh +# +# Unit tests for validate-deployment.sh +# +# A mock stellar/soroban CLI binary is injected into PATH so that RPC lookups +# are exercised without a live network. The mock is driven by the environment +# variable MOCK_MISSING_CONTRACT: any contract ID that appears in that +# comma-separated list is treated as non-existent; all others succeed. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +VALIDATE_SCRIPT="$SCRIPT_DIR/validate-deployment.sh" + +# Temporary workspace – cleaned up on exit +TEST_DIR=$(mktemp -d) +trap "rm -rf $TEST_DIR" EXIT + +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' + +TESTS_RUN=0 +TESTS_PASSED=0 + +pass() { printf "${GREEN}✓${NC} %s\n" "$*"; TESTS_PASSED=$((TESTS_PASSED + 1)); } +fail() { printf "${RED}✗${NC} FAIL: %s\n" "$*" >&2; } + +# ────────────────────────────────────────────────────────────────────────────── +# Mock stellar CLI +# +# Installed at $TEST_DIR/bin/stellar. +# Behaviour: +# stellar network add ... → always succeeds silently +# stellar network rm ... → always succeeds silently +# stellar contract info wasm-hash --id ... +# If $ID is listed in $MOCK_MISSING_CONTRACT → exit 1 + "not found" +# Otherwise → exit 0 + fake wasm hash +# ────────────────────────────────────────────────────────────────────────────── + +mkdir -p "$TEST_DIR/bin" + +cat > "$TEST_DIR/bin/stellar" <<'MOCK' +#!/usr/bin/env bash +# Minimal mock for the stellar CLI used by validate-deployment.sh + +# Parse the sub-command path (e.g. "network add", "contract info wasm-hash") +SUBCMD="${1:-} ${2:-} ${3:-}" + +case "$SUBCMD" in + "network add "*|"network rm "*) + exit 0 + ;; + "contract info wasm-hash"*) + # Extract --id value from arguments + CONTRACT_ID="" + while [[ $# -gt 0 ]]; do + if [[ "$1" == "--id" ]]; then + CONTRACT_ID="$2" + shift 2 + else + shift + fi + done + + # Check if this ID is in the MOCK_MISSING_CONTRACT list + MISSING="${MOCK_MISSING_CONTRACT:-}" + if [ -n "$MISSING" ]; then + IFS=',' read -ra MISSING_IDS <<< "$MISSING" + for mid in "${MISSING_IDS[@]}"; do + if [ "$mid" = "$CONTRACT_ID" ]; then + echo "error: contract not found: $CONTRACT_ID" >&2 + exit 1 + fi + done + fi + + # Contract exists – return a fake wasm hash + echo "aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899" + exit 0 + ;; + *) + echo "mock stellar: unhandled command: $*" >&2 + exit 1 + ;; +esac +MOCK + +chmod +x "$TEST_DIR/bin/stellar" + +# Helper: run validate-deployment.sh with the mock CLI injected +run_validate() { + local output_file="$1" + local network="${2:-futurenet}" + PATH="$TEST_DIR/bin:$PATH" \ + MOCK_MISSING_CONTRACT="${MOCK_MISSING_CONTRACT:-}" \ + RPC_URL="http://mock-rpc.invalid" \ + bash "$VALIDATE_SCRIPT" "$output_file" "$network" +} + +# ────────────────────────────────────────────────────────────────────────────── +# Shared valid contract IDs (properly formatted Stellar contract addresses) +# ────────────────────────────────────────────────────────────────────────────── + +ANNOUNCER_ID="CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +REGISTRY_ID="CBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" +SENDER_ID="CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" +NAMES_ID="CDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD" + +# Build a standard four-contract output block +make_output() { + local extra_lines="${1:-}" + cat < /dev/null 2>&1; then + pass "$label" + else + fail "$label (expected pass, got failure)" + fi +} + +assert_fails() { + local label="$1" + local output_file="$2" + local network="${3:-futurenet}" + local expected_code="${4:-1}" + TESTS_RUN=$((TESTS_RUN + 1)) + set +e + MOCK_MISSING_CONTRACT="${MOCK_MISSING_CONTRACT:-}" run_validate "$output_file" "$network" > /dev/null 2>&1 + local actual_code=$? + set -e + if [ "$actual_code" -eq "$expected_code" ]; then + pass "$label (exit code: $actual_code)" + else + fail "$label (expected exit code $expected_code, got $actual_code)" + fi +} + +# ────────────────────────────────────────────────────────────────────────────── +# Test 1: All contracts present and all exist on network → passes +# ────────────────────────────────────────────────────────────────────────────── +test_all_contracts_exist() { + make_output " ✔ All checks passed. Dry-run complete." \ + > "$TEST_DIR/t1.txt" + MOCK_MISSING_CONTRACT="" assert_passes \ + "T1: All contracts present and verified on network" \ + "$TEST_DIR/t1.txt" +} + +# ────────────────────────────────────────────────────────────────────────────── +# Test 2: All IDs present but one contract missing from network → fails +# ────────────────────────────────────────────────────────────────────────────── +test_contract_missing_from_network() { + make_output " ✔ All checks passed. Dry-run complete." \ + > "$TEST_DIR/t2.txt" + # Mock the announcer as non-existent on the network + MOCK_MISSING_CONTRACT="$ANNOUNCER_ID" assert_fails \ + "T2: Contract ID present in output but absent from network RPC → fails" \ + "$TEST_DIR/t2.txt" +} + +# ────────────────────────────────────────────────────────────────────────────── +# Test 3: Multiple contracts missing from network → fails +# ────────────────────────────────────────────────────────────────────────────── +test_multiple_contracts_missing_from_network() { + make_output " ✔ All checks passed. Dry-run complete." \ + > "$TEST_DIR/t3.txt" + MOCK_MISSING_CONTRACT="$REGISTRY_ID,$SENDER_ID" assert_fails \ + "T3: Multiple contract IDs absent from network RPC → fails" \ + "$TEST_DIR/t3.txt" +} + +# ────────────────────────────────────────────────────────────────────────────── +# Test 4: Missing contract in output (not deployed at all) → fails +# ────────────────────────────────────────────────────────────────────────────── +test_missing_contract_in_output() { + cat > "$TEST_DIR/t4.txt" < "$TEST_DIR/t5.txt" < "$TEST_DIR/t6.txt" < "$TEST_DIR/t7.txt" < "$TEST_DIR/t8.txt" + # No success or failure marker — deployment ended without a clean exit + MOCK_MISSING_CONTRACT="" assert_fails \ + "T8: Output lacks success or failure marker → fails" \ + "$TEST_DIR/t8.txt" +} + +assert_manifest_status() { + local label="$1" + local output_file="$2" + local expected_status="$3" + local manifest_file="${output_file%.txt}-manifest.json" + TESTS_RUN=$((TESTS_RUN + 1)) + if [ ! -f "$manifest_file" ]; then + fail "$label (manifest file $manifest_file not found)" + return + fi + if grep -q "\"status\": \"$expected_status\"" "$manifest_file"; then + pass "$label" + else + fail "$label (expected status $expected_status in manifest $manifest_file)" + fi +} + +# ────────────────────────────────────────────────────────────────────────────── +# Test 9: Early failure (missing output file) writes failed manifest and exits non-zero +# ────────────────────────────────────────────────────────────────────────────── +test_early_failure_nonexistent_file() { + local nonexistent="$TEST_DIR/nonexistent.txt" + local manifest="$TEST_DIR/nonexistent-manifest.json" + rm -f "$manifest" + + MOCK_MISSING_CONTRACT="" assert_fails \ + "T9a: Early failure (missing output file) exits with non-zero code" \ + "$nonexistent" + + assert_manifest_status \ + "T9b: Early failure generates manifest with status failed" \ + "$nonexistent" "failed" +} + +# ────────────────────────────────────────────────────────────────────────────── +# Test 10: Late failure (RPC contract lookup failure) writes failed manifest and exits non-zero +# ────────────────────────────────────────────────────────────────────────────── +test_late_failure_rpc_lookup() { + local output="$TEST_DIR/t10.txt" + make_output " ✔ All checks passed. Dry-run complete." > "$output" + local manifest="${output%.txt}-manifest.json" + rm -f "$manifest" + + MOCK_MISSING_CONTRACT="$ANNOUNCER_ID" assert_fails \ + "T10a: Late failure (RPC lookup failed) exits with non-zero code" \ + "$output" + + assert_manifest_status \ + "T10b: Late failure generates manifest with status failed and captured contracts" \ + "$output" "failed" +} + +# ────────────────────────────────────────────────────────────────────────────── +# Test 11: Success marker plus unexpected error/panic line → fails and writes failed manifest +# ────────────────────────────────────────────────────────────────────────────── +test_success_marker_with_unexpected_error() { + local output="$TEST_DIR/t11.txt" + cat > "$output" <&2 + exit 1 +fi + +echo "All tests passed."