Created
December 30, 2025 07:01
-
-
Save bussyjd/a7885d1e42f33be4957b14c1c2df829a to your computer and use it in GitHub Desktop.
Validates Execution Layer and Consensus Layer endpoints for Aztec validation duties
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # Aztec EL/CL Fitness Validator | |
| # Validates Execution Layer and Consensus Layer endpoints for Aztec validation duties | |
| # | |
| set -uo pipefail | |
| # Note: -e is intentionally omitted to allow graceful error handling | |
| # Default values | |
| CHAIN_ID=1 | |
| GENESIS_TIME=1606824023 # Mainnet beacon chain genesis | |
| SLOT_DURATION=12 | |
| # Counters | |
| EL_PASS=0 | |
| EL_FAIL=0 | |
| EL_WARN=0 | |
| CL_PASS=0 | |
| CL_FAIL=0 | |
| CL_WARN=0 | |
| # Colors | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[0;33m' | |
| NC='\033[0m' # No Color | |
| usage() { | |
| cat <<EOF | |
| Usage: $(basename "$0") --el <EL_URL> --cl <CL_URL> [OPTIONS] | |
| Validates EL/CL endpoints for Aztec validation duties. | |
| Required: | |
| --el <URL> Execution Layer JSON-RPC URL | |
| --cl <URL> Consensus Layer Beacon API URL | |
| Options: | |
| --chain-id <ID> Expected chain ID (default: 1 for mainnet) | |
| --genesis <TIME> Beacon chain genesis timestamp (default: 1606824023 for mainnet) | |
| -h, --help Show this help message | |
| Examples: | |
| $(basename "$0") --el http://geth:8545 --cl http://lighthouse:5052 | |
| $(basename "$0") --el http://geth:8545 --cl http://lighthouse:5052 --chain-id 17000 | |
| EOF | |
| exit 2 | |
| } | |
| # Parse arguments | |
| EL_URL="" | |
| CL_URL="" | |
| while [[ $# -gt 0 ]]; do | |
| case $1 in | |
| --el) | |
| EL_URL="$2" | |
| shift 2 | |
| ;; | |
| --cl) | |
| CL_URL="$2" | |
| shift 2 | |
| ;; | |
| --chain-id) | |
| CHAIN_ID="$2" | |
| shift 2 | |
| ;; | |
| --genesis) | |
| GENESIS_TIME="$2" | |
| shift 2 | |
| ;; | |
| -h|--help) | |
| usage | |
| ;; | |
| *) | |
| echo "Unknown option: $1" | |
| usage | |
| ;; | |
| esac | |
| done | |
| if [[ -z "$EL_URL" || -z "$CL_URL" ]]; then | |
| echo "Error: --el and --cl are required" | |
| usage | |
| fi | |
| # Helper functions | |
| check_pass() { | |
| echo -e "${GREEN}[PASS]${NC} $1" | |
| } | |
| check_fail() { | |
| echo -e "${RED}[FAIL]${NC} $1" | |
| } | |
| check_warn() { | |
| echo -e "${YELLOW}[WARN]${NC} $1" | |
| } | |
| # Make JSON-RPC call to EL | |
| rpc_call() { | |
| local method="$1" | |
| local params="${2:-[]}" | |
| curl -s -X POST "$EL_URL" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{\"jsonrpc\":\"2.0\",\"method\":\"$method\",\"params\":$params,\"id\":1}" \ | |
| --connect-timeout 10 \ | |
| --max-time 30 | |
| } | |
| # Make GET request to CL beacon API | |
| beacon_get() { | |
| local path="$1" | |
| curl -s "${CL_URL}${path}" \ | |
| --connect-timeout 10 \ | |
| --max-time 30 | |
| } | |
| # Print header | |
| echo "" | |
| echo "========================================" | |
| echo " Aztec EL/CL Fitness Validator" | |
| echo "========================================" | |
| echo "" | |
| echo "EL Endpoint: $EL_URL" | |
| echo "CL Endpoint: $CL_URL" | |
| echo "Chain ID: $CHAIN_ID" | |
| echo "" | |
| # ============================================================================= | |
| # EXECUTION LAYER CHECKS | |
| # ============================================================================= | |
| echo "--- Execution Layer Checks ---" | |
| # 1. Chain ID | |
| result=$(rpc_call "eth_chainId" 2>/dev/null || echo '{"error":"connection failed"}') | |
| if echo "$result" | jq -e '.error' > /dev/null 2>&1; then | |
| check_fail "Chain ID - connection failed" | |
| ((EL_FAIL++)) | |
| else | |
| chain_id_hex=$(echo "$result" | jq -r '.result // empty') | |
| if [[ -n "$chain_id_hex" ]]; then | |
| chain_id_dec=$((chain_id_hex)) | |
| if [[ "$chain_id_dec" -eq "$CHAIN_ID" ]]; then | |
| check_pass "Chain ID matches ($chain_id_dec)" | |
| ((EL_PASS++)) | |
| else | |
| check_fail "Chain ID mismatch (got $chain_id_dec, expected $CHAIN_ID)" | |
| ((EL_FAIL++)) | |
| fi | |
| else | |
| check_fail "Chain ID - invalid response" | |
| ((EL_FAIL++)) | |
| fi | |
| fi | |
| # 2. Sync Status | |
| result=$(rpc_call "eth_syncing" 2>/dev/null || echo '{"error":"connection failed"}') | |
| if echo "$result" | jq -e '.error' > /dev/null 2>&1; then | |
| check_fail "Sync status - request failed" | |
| ((EL_FAIL++)) | |
| else | |
| syncing=$(echo "$result" | jq -r '.result') | |
| if [[ "$syncing" == "false" ]]; then | |
| check_pass "Node is fully synced" | |
| ((EL_PASS++)) | |
| else | |
| current=$(echo "$syncing" | jq -r '.currentBlock // "unknown"') | |
| highest=$(echo "$syncing" | jq -r '.highestBlock // "unknown"') | |
| check_fail "Node is syncing (current: $current, highest: $highest)" | |
| ((EL_FAIL++)) | |
| fi | |
| fi | |
| # 3. Block Freshness | |
| result=$(rpc_call "eth_getBlockByNumber" '["latest", false]' 2>/dev/null || echo '{"error":"connection failed"}') | |
| LATEST_BLOCK_HASH="" | |
| if echo "$result" | jq -e '.error' > /dev/null 2>&1; then | |
| check_fail "Block freshness - request failed" | |
| ((EL_FAIL++)) | |
| else | |
| block_timestamp_hex=$(echo "$result" | jq -r '.result.timestamp // empty') | |
| LATEST_BLOCK_HASH=$(echo "$result" | jq -r '.result.hash // empty') | |
| if [[ -n "$block_timestamp_hex" ]]; then | |
| block_timestamp=$((block_timestamp_hex)) | |
| current_time=$(date +%s) | |
| drift=$((current_time - block_timestamp)) | |
| if [[ "$drift" -le 60 ]]; then | |
| check_pass "Block freshness (${drift}s behind)" | |
| ((EL_PASS++)) | |
| else | |
| check_fail "Block freshness - ${drift}s behind (max 60s)" | |
| ((EL_FAIL++)) | |
| fi | |
| else | |
| check_fail "Block freshness - invalid response" | |
| ((EL_FAIL++)) | |
| fi | |
| fi | |
| # 4. eth_getLogs support | |
| block_number_result=$(rpc_call "eth_blockNumber" 2>/dev/null || echo '{"error":"connection failed"}') | |
| if echo "$block_number_result" | jq -e '.error' > /dev/null 2>&1; then | |
| check_fail "eth_getLogs - cannot get block number" | |
| ((EL_FAIL++)) | |
| else | |
| block_number_hex=$(echo "$block_number_result" | jq -r '.result // empty') | |
| if [[ -n "$block_number_hex" ]]; then | |
| block_number=$((block_number_hex)) | |
| from_block=$((block_number - 10)) | |
| from_block_hex=$(printf "0x%x" $from_block) | |
| result=$(rpc_call "eth_getLogs" "[{\"fromBlock\":\"$from_block_hex\",\"toBlock\":\"latest\",\"address\":null}]" 2>/dev/null || echo '{"error":"connection failed"}') | |
| if echo "$result" | jq -e '.error' > /dev/null 2>&1; then | |
| error_msg=$(echo "$result" | jq -r '.error.message // "unknown error"') | |
| check_fail "eth_getLogs - $error_msg" | |
| ((EL_FAIL++)) | |
| else | |
| log_count=$(echo "$result" | jq -r '.result | length') | |
| check_pass "eth_getLogs supported ($log_count logs in last 10 blocks)" | |
| ((EL_PASS++)) | |
| fi | |
| else | |
| check_fail "eth_getLogs - invalid block number" | |
| ((EL_FAIL++)) | |
| fi | |
| fi | |
| # 5. Filter API support (eth_newBlockFilter + eth_getFilterChanges) | |
| # This is CRITICAL for Aztec - tests the full filter lifecycle | |
| result=$(rpc_call "eth_newBlockFilter" 2>/dev/null || echo '{"error":"connection failed"}') | |
| if echo "$result" | jq -e '.error' > /dev/null 2>&1; then | |
| error_msg=$(echo "$result" | jq -r '.error.message // "not supported"') | |
| check_fail "Filter API - eth_newBlockFilter failed: $error_msg" | |
| ((EL_FAIL++)) | |
| else | |
| filter_id=$(echo "$result" | jq -r '.result // empty') | |
| if [[ -n "$filter_id" && "$filter_id" != "null" ]]; then | |
| # Now test eth_getFilterChanges with the filter | |
| sleep 1 # Wait for potential new blocks | |
| changes_result=$(rpc_call "eth_getFilterChanges" "[\"$filter_id\"]" 2>/dev/null || echo '{"error":"connection failed"}') | |
| if echo "$changes_result" | jq -e '.error' > /dev/null 2>&1; then | |
| error_msg=$(echo "$changes_result" | jq -r '.error.message // "failed"') | |
| check_fail "Filter API - eth_getFilterChanges failed: $error_msg" | |
| ((EL_FAIL++)) | |
| else | |
| changes_count=$(echo "$changes_result" | jq -r '.result | length // 0') | |
| check_pass "Filter API supported (filter: $filter_id, changes: $changes_count)" | |
| ((EL_PASS++)) | |
| fi | |
| # Cleanup: uninstall the filter | |
| rpc_call "eth_uninstallFilter" "[\"$filter_id\"]" > /dev/null 2>&1 || true | |
| else | |
| check_fail "Filter API - eth_newBlockFilter returned invalid filter ID" | |
| ((EL_FAIL++)) | |
| fi | |
| fi | |
| # 5b. Filter API - stale/foreign filter handling | |
| # Test that the node gracefully handles unknown filter IDs (critical for multi-node setups) | |
| FAKE_FILTER_ID="0xdeadbeefdeadbeefdeadbeefdeadbeef" | |
| stale_result=$(rpc_call "eth_getFilterChanges" "[\"$FAKE_FILTER_ID\"]" 2>/dev/null || echo '{"error":"connection failed"}') | |
| if echo "$stale_result" | jq -e '.error' > /dev/null 2>&1; then | |
| error_msg=$(echo "$stale_result" | jq -r '.error.message // "unknown"') | |
| # "filter not found" is the expected graceful error | |
| if [[ "$error_msg" == *"filter not found"* || "$error_msg" == *"does not exist"* ]]; then | |
| check_pass "Stale filter handling (graceful error for unknown filter)" | |
| ((EL_PASS++)) | |
| else | |
| # Any other error (like overflow, crash) is a problem | |
| check_fail "Stale filter handling - unexpected error: $error_msg" | |
| ((EL_FAIL++)) | |
| fi | |
| else | |
| # If it returns success for a fake filter, that's also wrong | |
| check_fail "Stale filter handling - accepted invalid filter ID (should error)" | |
| ((EL_FAIL++)) | |
| fi | |
| # 6. eth_call support | |
| result=$(rpc_call "eth_call" '[{"to":"0x0000000000000000000000000000000000000000","data":"0x"},"latest"]' 2>/dev/null || echo '{"error":"connection failed"}') | |
| if echo "$result" | jq -e '.error' > /dev/null 2>&1; then | |
| # Some nodes may return error for empty call, try with a different approach | |
| check_warn "eth_call - empty call returned error (may be normal)" | |
| ((EL_WARN++)) | |
| else | |
| check_pass "eth_call supported" | |
| ((EL_PASS++)) | |
| fi | |
| # 7. eth_estimateGas support | |
| result=$(rpc_call "eth_estimateGas" '[{"from":"0x0000000000000000000000000000000000000001","to":"0x0000000000000000000000000000000000000002","value":"0x0"}]' 2>/dev/null || echo '{"error":"connection failed"}') | |
| if echo "$result" | jq -e '.error' > /dev/null 2>&1; then | |
| check_warn "eth_estimateGas - returned error (may be account-specific)" | |
| ((EL_WARN++)) | |
| else | |
| gas=$(echo "$result" | jq -r '.result // empty') | |
| if [[ -n "$gas" ]]; then | |
| check_pass "eth_estimateGas supported (estimate: $gas)" | |
| ((EL_PASS++)) | |
| else | |
| check_warn "eth_estimateGas - empty response" | |
| ((EL_WARN++)) | |
| fi | |
| fi | |
| # 8. eth_feeHistory support | |
| result=$(rpc_call "eth_feeHistory" '["0x4", "latest", [25, 75]]' 2>/dev/null || echo '{"error":"connection failed"}') | |
| if echo "$result" | jq -e '.error' > /dev/null 2>&1; then | |
| check_fail "eth_feeHistory - not supported" | |
| ((EL_FAIL++)) | |
| else | |
| base_fees=$(echo "$result" | jq -r '.result.baseFeePerGas | length // 0') | |
| if [[ "$base_fees" -gt 0 ]]; then | |
| latest_fee=$(echo "$result" | jq -r '.result.baseFeePerGas[-1]') | |
| check_pass "eth_feeHistory supported (latest baseFee: $latest_fee)" | |
| ((EL_PASS++)) | |
| else | |
| check_fail "eth_feeHistory - invalid response" | |
| ((EL_FAIL++)) | |
| fi | |
| fi | |
| # 9. eth_blobBaseFee support (post-Dencun) | |
| result=$(rpc_call "eth_blobBaseFee" 2>/dev/null || echo '{"error":"connection failed"}') | |
| if echo "$result" | jq -e '.error' > /dev/null 2>&1; then | |
| check_warn "eth_blobBaseFee - not supported (pre-Dencun or unsupported)" | |
| ((EL_WARN++)) | |
| else | |
| blob_fee=$(echo "$result" | jq -r '.result // empty') | |
| if [[ -n "$blob_fee" ]]; then | |
| check_pass "eth_blobBaseFee supported ($blob_fee)" | |
| ((EL_PASS++)) | |
| else | |
| check_warn "eth_blobBaseFee - empty response" | |
| ((EL_WARN++)) | |
| fi | |
| fi | |
| # 10. eth_getBlockByHash with parentBeaconBlockRoot (EIP-4788) | |
| PARENT_BEACON_ROOT="" | |
| if [[ -n "$LATEST_BLOCK_HASH" ]]; then | |
| result=$(rpc_call "eth_getBlockByHash" "[\"$LATEST_BLOCK_HASH\", false]" 2>/dev/null || echo '{"error":"connection failed"}') | |
| if echo "$result" | jq -e '.error' > /dev/null 2>&1; then | |
| check_fail "eth_getBlockByHash - request failed" | |
| ((EL_FAIL++)) | |
| else | |
| PARENT_BEACON_ROOT=$(echo "$result" | jq -r '.result.parentBeaconBlockRoot // empty') | |
| if [[ -n "$PARENT_BEACON_ROOT" && "$PARENT_BEACON_ROOT" != "null" ]]; then | |
| check_pass "eth_getBlockByHash with parentBeaconBlockRoot" | |
| ((EL_PASS++)) | |
| else | |
| check_warn "eth_getBlockByHash - no parentBeaconBlockRoot (pre-Dencun block?)" | |
| ((EL_WARN++)) | |
| fi | |
| fi | |
| else | |
| check_warn "eth_getBlockByHash - skipped (no block hash available)" | |
| ((EL_WARN++)) | |
| fi | |
| # 11. debug_traceTransaction (optional) | |
| # Use a known transaction hash for testing - this is a simple ETH transfer on mainnet | |
| TEST_TX_HASH="0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060" | |
| result=$(rpc_call "debug_traceTransaction" "[\"$TEST_TX_HASH\", {\"tracer\": \"callTracer\"}]" 2>/dev/null || echo '{"error":"connection failed"}') | |
| if echo "$result" | jq -e '.error' > /dev/null 2>&1; then | |
| error_msg=$(echo "$result" | jq -r '.error.message // "not available"') | |
| check_warn "debug_traceTransaction - $error_msg" | |
| ((EL_WARN++)) | |
| else | |
| trace_type=$(echo "$result" | jq -r '.result.type // empty') | |
| if [[ -n "$trace_type" ]]; then | |
| check_pass "debug_traceTransaction supported" | |
| ((EL_PASS++)) | |
| else | |
| check_warn "debug_traceTransaction - unexpected response format" | |
| ((EL_WARN++)) | |
| fi | |
| fi | |
| echo "" | |
| # ============================================================================= | |
| # CONSENSUS LAYER CHECKS | |
| # ============================================================================= | |
| echo "--- Consensus Layer Checks ---" | |
| # 1. Beacon API connectivity | |
| result=$(beacon_get "/eth/v1/beacon/headers" 2>/dev/null || echo '{"error":"connection failed"}') | |
| if echo "$result" | jq -e '.data' > /dev/null 2>&1; then | |
| check_pass "Beacon API reachable" | |
| ((CL_PASS++)) | |
| else | |
| check_fail "Beacon API - connection failed or invalid response" | |
| ((CL_FAIL++)) | |
| fi | |
| # 2. Head slot | |
| result=$(beacon_get "/eth/v1/beacon/headers/head" 2>/dev/null || echo '{"error":"connection failed"}') | |
| HEAD_SLOT=0 | |
| HEAD_ROOT="" | |
| if echo "$result" | jq -e '.data' > /dev/null 2>&1; then | |
| HEAD_SLOT=$(echo "$result" | jq -r '.data.header.message.slot // 0') | |
| HEAD_ROOT=$(echo "$result" | jq -r '.data.root // empty') | |
| check_pass "Head slot: $HEAD_SLOT" | |
| ((CL_PASS++)) | |
| else | |
| check_fail "Head slot - request failed" | |
| ((CL_FAIL++)) | |
| fi | |
| # 3. Slot freshness | |
| if [[ "$HEAD_SLOT" -gt 0 ]]; then | |
| current_time=$(date +%s) | |
| expected_slot=$(( (current_time - GENESIS_TIME) / SLOT_DURATION )) | |
| slot_diff=$((expected_slot - HEAD_SLOT)) | |
| if [[ "$slot_diff" -le 2 ]]; then | |
| check_pass "Slot freshness (${slot_diff} slots behind)" | |
| ((CL_PASS++)) | |
| else | |
| check_fail "Slot freshness - ${slot_diff} slots behind (max 2)" | |
| ((CL_FAIL++)) | |
| fi | |
| else | |
| check_fail "Slot freshness - cannot calculate (no head slot)" | |
| ((CL_FAIL++)) | |
| fi | |
| # 4. Recent blob_sidecars | |
| if [[ "$HEAD_SLOT" -gt 0 ]]; then | |
| # Try a few recent slots (some may be missed blocks) | |
| blob_found=false | |
| for offset in 1 2 3 4 5; do | |
| test_slot=$((HEAD_SLOT - offset)) | |
| result=$(beacon_get "/eth/v1/beacon/blob_sidecars/$test_slot" 2>/dev/null || echo '{"error":"connection failed"}') | |
| if echo "$result" | jq -e '.data' > /dev/null 2>&1; then | |
| blob_count=$(echo "$result" | jq -r '.data | length') | |
| check_pass "Recent blob_sidecars available (slot $test_slot: $blob_count blobs)" | |
| ((CL_PASS++)) | |
| blob_found=true | |
| break | |
| fi | |
| done | |
| if [[ "$blob_found" == "false" ]]; then | |
| check_fail "Recent blob_sidecars - not available in last 5 slots" | |
| ((CL_FAIL++)) | |
| fi | |
| else | |
| check_fail "Recent blob_sidecars - skipped (no head slot)" | |
| ((CL_FAIL++)) | |
| fi | |
| # 5. Historical blob_sidecars (tests blob retention) | |
| if [[ "$HEAD_SLOT" -gt 1000 ]]; then | |
| historical_slot=$((HEAD_SLOT - 1000)) | |
| result=$(beacon_get "/eth/v1/beacon/blob_sidecars/$historical_slot" 2>/dev/null || echo '{"error":"connection failed"}') | |
| http_code=$(curl -s -o /dev/null -w "%{http_code}" "${CL_URL}/eth/v1/beacon/blob_sidecars/$historical_slot" 2>/dev/null || echo "000") | |
| if [[ "$http_code" == "200" ]]; then | |
| blob_count=$(echo "$result" | jq -r '.data | length // 0') | |
| check_pass "Historical blob_sidecars available (slot $historical_slot: $blob_count blobs)" | |
| ((CL_PASS++)) | |
| elif [[ "$http_code" == "404" ]]; then | |
| check_warn "Historical blob_sidecars - slot $historical_slot not found (may be missed block)" | |
| ((CL_WARN++)) | |
| elif [[ "$http_code" == "500" ]]; then | |
| error_msg=$(echo "$result" | jq -r '.message // "server error"' 2>/dev/null) | |
| check_fail "Historical blob_sidecars - server error: $error_msg" | |
| ((CL_FAIL++)) | |
| else | |
| check_fail "Historical blob_sidecars - HTTP $http_code" | |
| ((CL_FAIL++)) | |
| fi | |
| else | |
| check_warn "Historical blob_sidecars - skipped (head slot too low)" | |
| ((CL_WARN++)) | |
| fi | |
| # 6. Headers by block root | |
| if [[ -n "$HEAD_ROOT" ]]; then | |
| result=$(beacon_get "/eth/v1/beacon/headers/$HEAD_ROOT" 2>/dev/null || echo '{"error":"connection failed"}') | |
| if echo "$result" | jq -e '.data' > /dev/null 2>&1; then | |
| check_pass "Headers by block root" | |
| ((CL_PASS++)) | |
| else | |
| check_fail "Headers by block root - request failed" | |
| ((CL_FAIL++)) | |
| fi | |
| else | |
| check_warn "Headers by block root - skipped (no head root)" | |
| ((CL_WARN++)) | |
| fi | |
| echo "" | |
| # ============================================================================= | |
| # EL/CL PAIRING VALIDATION (Critical for Aztec) | |
| # ============================================================================= | |
| echo "--- EL/CL Pairing Checks ---" | |
| PAIR_PASS=0 | |
| PAIR_FAIL=0 | |
| PAIR_WARN=0 | |
| # 1. Validate parentBeaconBlockRoot exists in CL (EIP-4788 correlation) | |
| # This is CRITICAL - Aztec uses parentBeaconBlockRoot to find the CL slot for blob fetching | |
| if [[ -n "$PARENT_BEACON_ROOT" && "$PARENT_BEACON_ROOT" != "null" ]]; then | |
| # Query CL for the beacon block header using parentBeaconBlockRoot | |
| pairing_result=$(beacon_get "/eth/v1/beacon/headers/$PARENT_BEACON_ROOT" 2>/dev/null || echo '{"error":"connection failed"}') | |
| if echo "$pairing_result" | jq -e '.data' > /dev/null 2>&1; then | |
| cl_slot=$(echo "$pairing_result" | jq -r '.data.header.message.slot // empty') | |
| if [[ -n "$cl_slot" ]]; then | |
| # The actual block slot is parentBeaconBlockRoot's slot + 1 | |
| actual_slot=$((cl_slot + 1)) | |
| check_pass "EL/CL pairing: parentBeaconBlockRoot found in CL (slot $actual_slot)" | |
| ((PAIR_PASS++)) | |
| # 2. Verify blob_sidecars are available for that slot | |
| blob_result=$(beacon_get "/eth/v1/beacon/blob_sidecars/$actual_slot" 2>/dev/null || echo '{"error":"connection failed"}') | |
| if echo "$blob_result" | jq -e '.data' > /dev/null 2>&1; then | |
| blob_count=$(echo "$blob_result" | jq -r '.data | length') | |
| check_pass "EL/CL pairing: blob_sidecars available for correlated slot (count: $blob_count)" | |
| ((PAIR_PASS++)) | |
| else | |
| http_code=$(curl -s -o /dev/null -w "%{http_code}" "${CL_URL}/eth/v1/beacon/blob_sidecars/$actual_slot" 2>/dev/null || echo "000") | |
| if [[ "$http_code" == "404" ]]; then | |
| check_warn "EL/CL pairing: No blobs at slot $actual_slot (may be empty block)" | |
| ((PAIR_WARN++)) | |
| else | |
| check_fail "EL/CL pairing: Cannot fetch blob_sidecars for slot $actual_slot (HTTP $http_code)" | |
| ((PAIR_FAIL++)) | |
| fi | |
| fi | |
| else | |
| check_fail "EL/CL pairing: CL returned invalid slot for parentBeaconBlockRoot" | |
| ((PAIR_FAIL++)) | |
| fi | |
| else | |
| # This is the critical failure - CL doesn't have the beacon block that EL references | |
| error_code=$(echo "$pairing_result" | jq -r '.code // empty') | |
| if [[ "$error_code" == "404" ]]; then | |
| check_fail "EL/CL pairing: parentBeaconBlockRoot NOT FOUND in CL - NODES ARE NOT PAIRED!" | |
| ((PAIR_FAIL++)) | |
| else | |
| check_fail "EL/CL pairing: Cannot verify parentBeaconBlockRoot in CL" | |
| ((PAIR_FAIL++)) | |
| fi | |
| fi | |
| else | |
| check_warn "EL/CL pairing: Skipped (no parentBeaconBlockRoot from EL)" | |
| ((PAIR_WARN++)) | |
| fi | |
| # 3. Validate EL timestamp aligns with CL slot timing | |
| if [[ -n "$LATEST_BLOCK_HASH" && "$HEAD_SLOT" -gt 0 ]]; then | |
| # Get the EL block timestamp | |
| el_block=$(rpc_call "eth_getBlockByHash" "[\"$LATEST_BLOCK_HASH\", false]" 2>/dev/null) | |
| el_timestamp_hex=$(echo "$el_block" | jq -r '.result.timestamp // empty') | |
| if [[ -n "$el_timestamp_hex" ]]; then | |
| el_timestamp=$((el_timestamp_hex)) | |
| # Calculate expected slot from EL timestamp | |
| expected_slot=$(( (el_timestamp - GENESIS_TIME) / SLOT_DURATION )) | |
| # Allow 2 slot drift tolerance | |
| slot_drift=$((HEAD_SLOT - expected_slot)) | |
| slot_drift_abs=${slot_drift#-} # Absolute value | |
| if [[ "$slot_drift_abs" -le 2 ]]; then | |
| check_pass "EL/CL timing: EL timestamp aligns with CL slot (drift: $slot_drift slots)" | |
| ((PAIR_PASS++)) | |
| else | |
| check_fail "EL/CL timing: EL timestamp diverges from CL slot (drift: $slot_drift slots)" | |
| ((PAIR_FAIL++)) | |
| fi | |
| else | |
| check_warn "EL/CL timing: Cannot get EL block timestamp" | |
| ((PAIR_WARN++)) | |
| fi | |
| else | |
| check_warn "EL/CL timing: Skipped (missing block hash or head slot)" | |
| ((PAIR_WARN++)) | |
| fi | |
| echo "" | |
| # ============================================================================= | |
| # SUMMARY | |
| # ============================================================================= | |
| EL_TOTAL=$((EL_PASS + EL_FAIL + EL_WARN)) | |
| CL_TOTAL=$((CL_PASS + CL_FAIL + CL_WARN)) | |
| PAIR_TOTAL=$((PAIR_PASS + PAIR_FAIL + PAIR_WARN)) | |
| echo "========================================" | |
| if [[ "$EL_FAIL" -eq 0 && "$CL_FAIL" -eq 0 && "$PAIR_FAIL" -eq 0 ]]; then | |
| echo -e " ${GREEN}RESULT: SUITABLE FOR AZTEC VALIDATION${NC}" | |
| else | |
| echo -e " ${RED}RESULT: NOT SUITABLE FOR AZTEC VALIDATION${NC}" | |
| fi | |
| echo "========================================" | |
| echo "" | |
| echo "EL: $EL_PASS/$EL_TOTAL checks passed ($EL_WARN warnings, $EL_FAIL failures)" | |
| echo "CL: $CL_PASS/$CL_TOTAL checks passed ($CL_WARN warnings, $CL_FAIL failures)" | |
| echo "Pairing: $PAIR_PASS/$PAIR_TOTAL checks passed ($PAIR_WARN warnings, $PAIR_FAIL failures)" | |
| echo "" | |
| # Notes | |
| if [[ "$EL_WARN" -gt 0 || "$CL_WARN" -gt 0 || "$PAIR_WARN" -gt 0 ]]; then | |
| echo "Notes:" | |
| if [[ "$EL_WARN" -gt 0 ]]; then | |
| echo "- Some EL warnings may be acceptable (e.g., debug_traceTransaction is optional)" | |
| fi | |
| if [[ "$CL_WARN" -gt 0 ]]; then | |
| echo "- Some CL warnings may indicate pruned data or missed blocks" | |
| fi | |
| if [[ "$PAIR_WARN" -gt 0 ]]; then | |
| echo "- Pairing warnings may indicate empty blocks or missing blob data" | |
| fi | |
| echo "" | |
| fi | |
| # Exit code | |
| if [[ "$EL_FAIL" -gt 0 || "$CL_FAIL" -gt 0 || "$PAIR_FAIL" -gt 0 ]]; then | |
| exit 1 | |
| else | |
| exit 0 | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment