diff --git a/.github/hub-failover/README.md b/.github/hub-failover/README.md new file mode 100644 index 00000000..74fa9ec2 --- /dev/null +++ b/.github/hub-failover/README.md @@ -0,0 +1,35 @@ +# Two-hub regtest failover venue + +This Compose venue gives the HF-15b drill two federated hubs with independent databases, three seed-driven indexers, and a BTC control indexer pinned to hub B. The BTC, LTC, and DOGE indexers all use `http://hub-a:10000,http://hub-b:10000` as `HUB_SEED_URLS`. `btc-indexer-control` has no seed list and uses `HUB_API_URL=http://hub-b:10000`. + +Both hubs use the same `HUB_FEED_API_KEY`, `HUB_API_KEY`, and `HUB_REORG_API_KEY`. Their API and P2P endpoints, signing identities, peer seeds, and databases remain distinct. The one-shot `hub-federation-init` service registers both deterministic fixture signing identities in both hubs before any indexer starts. Fixture defaults are supplied for every interpolation so `docker compose config` works without a credential file. A CI caller can override the keys, database password, RPC credentials, seed list, and host ports through its environment. + +Start the venue from the `xchain-node` repository: + +```sh +docker compose -p hub-failover -f .github/hub-failover/compose.yml up -d --wait +``` + +The indexer healthchecks require `/status` to report `bootstrapped: true`, so `--wait` returns only after all four indexers have migrated their databases and bootstrapped their mirrors. + +HF-15b drives these service names: + +| Role | Compose service | Default host port | +| --- | --- | ---: | +| Hub A API | `hub-a` | 64230 | +| Hub A P2P/feed | `hub-a` | 64231 | +| Hub B API | `hub-b` | 64232 | +| Hub B P2P/feed | `hub-b` | 64233 | +| Seed-driven BTC indexer | `btc-indexer` | 64205 | +| Hub-B-pinned BTC control | `btc-indexer-control` | 64207 | +| Seed-driven LTC indexer | `ltc-indexer` | 64214 | +| Seed-driven DOGE indexer | `doge-indexer` | 64224 | +| BTC, LTC, DOGE miners | `btc-miner`, `ltc-miner`, `doge-miner` | 64206, 64215, 64225 | + +The drill records each seed-driven indexer's followed hub, stops that service with `docker compose stop hub-a` or `docker compose stop hub-b`, verifies movement and continued block progress, and restarts it with `docker compose start `. It compares the moved `btc-indexer` with `btc-indexer-control` only while hub B remains available. Hub data is separate in `XChain_Hub_A` and `XChain_Hub_B`, so stopping and restarting a hub exercises peer catch-up rather than a shared database. + +Stop the venue while preserving data with: + +```sh +docker compose -p hub-failover -f .github/hub-failover/compose.yml down +``` diff --git a/.github/hub-failover/compose.yml b/.github/hub-failover/compose.yml new file mode 100644 index 00000000..283192d2 --- /dev/null +++ b/.github/hub-failover/compose.yml @@ -0,0 +1,637 @@ +x-indexer-environment: &indexer-environment + INDEXER_NETWORK: regtest + INDEXER_API_PORT: 3004 + INDEXER_DB_HOST: db + INDEXER_DB_PORT: 3306 + INDEXER_DB_USER: root + INDEXER_DB_PASS: ${DB_PASSWORD:-hub-failover-db} + DECODER_DB_HOST: db + DECODER_DB_PORT: 3306 + DECODER_DB_USER: root + DECODER_DB_PASS: ${DB_PASSWORD:-hub-failover-db} + HUB_API_KEY: ${HUB_API_KEY:-hub-failover-bulk} + HUB_FEED_API_KEY: ${HUB_FEED_API_KEY:-hub-failover-feed} + HUB_REORG_API_KEY: ${HUB_REORG_API_KEY:-hub-failover-reorg} + HUB_DB_HOST: db + HUB_DB_PORT: 3306 + HUB_DB_USER: root + HUB_DB_PASS: ${DB_PASSWORD:-hub-failover-db} + HUB_DB_SYNC_ENABLED: "true" + INDEXER_ALLOW_UNAUTHENTICATED: "true" + HUB_SYNC_PRICE_GRACE_S: 0 + HUB_SYNC_ORACLE_GRACE_S: 0 + HUB_SYNC_ATTEST_RESPONSE_GRACE_S: 0 + HUB_SYNC_MATCH_GRACE_S: 0 + HUB_SYNC_CALL_GRACE_S: 0 + HUB_SYNC_ANCHOR_ATTEST_GRACE_S: 0 + +x-failover-environment: &failover-environment + HUB_SEED_URLS: ${HUB_SEED_URLS:-http://hub-a:10000,http://hub-b:10000} + HUB_CONFIG_URL: http://hub-a:10000 + +x-hub-environment: &hub-environment + HUB_HOST: 0.0.0.0 + HUB_PORT: 10000 + HUB_DB_HOST: db + HUB_DB_PORT: 3306 + HUB_DB_USER: root + HUB_DB_SECRET: ${DB_PASSWORD:-hub-failover-db} + HUB_API_KEY: ${HUB_API_KEY:-hub-failover-bulk} + HUB_FEED_API_KEY: ${HUB_FEED_API_KEY:-hub-failover-feed} + HUB_REORG_API_KEY: ${HUB_REORG_API_KEY:-hub-failover-reorg} + HUB_NETWORK: regtest + HUB_P2P_FEED_ENABLED: "true" + P2P_HOST: 0.0.0.0 + P2P_PORT: 10001 + REQUIRE_SIGNATURES: "true" + ORACLE_EPOCH_START: 1704067200000 + MIN_VALIDATORS: 2 + ORACLE_MIN_SUBMISSIONS: 2 + XCHAIN_HUB_SKIP_MIN_STAKE_ASSERT: 1 + BTC_INDEXER_API_URL: http://btc-indexer:3004 + BTC_INDEXER_URL: http://btc-indexer:3004 + LTC_INDEXER_URL: http://ltc-indexer:3004 + DOGE_INDEXER_API_URL: http://doge-indexer:3004 + DOGE_INDEXER_URL: http://doge-indexer:3004 + +x-service-healthcheck: &service-healthcheck + interval: 10s + timeout: 5s + retries: 90 + start_period: 120s + +x-node-healthcheck: &node-healthcheck + interval: 5s + timeout: 5s + retries: 60 + start_period: 20s + +services: + db: + image: xchain-node-database + command: ["--max-connections=1000", "--innodb-flush-log-at-trx-commit=2", "--innodb-doublewrite=0", "--innodb-buffer-pool-size=1G"] + environment: + MARIADB_ROOT_PASSWORD: ${DB_PASSWORD:-hub-failover-db} + MARIADB_ROOT_HOST: "%" + ports: + - "127.0.0.1:${DB_HOST_PORT:-64200}:3306" + volumes: + - db-data:/var/lib/mysql + healthcheck: + test: ["CMD-SHELL", "healthcheck.sh --connect --innodb_initialized"] + interval: 5s + timeout: 5s + retries: 30 + start_period: 20s + restart: unless-stopped + + db-init: + image: xchain-node-database + depends_on: + db: + condition: service_healthy + environment: + MARIADB_ROOT_PASSWORD: ${DB_PASSWORD:-hub-failover-db} + entrypoint: ["/bin/sh", "-ec"] + command: + - | + mariadb -h db -uroot -p"$${MARIADB_ROOT_PASSWORD}" -e "CREATE DATABASE IF NOT EXISTS XChain_Hub_A; CREATE DATABASE IF NOT EXISTS XChain_Hub_B; CREATE DATABASE IF NOT EXISTS XChain_BTC_Regtest_Decoder; CREATE DATABASE IF NOT EXISTS XChain_BTC_Regtest_Indexer; CREATE DATABASE IF NOT EXISTS XChain_BTC_Regtest_Control_Indexer; CREATE DATABASE IF NOT EXISTS XChain_LTC_Regtest_Decoder; CREATE DATABASE IF NOT EXISTS XChain_LTC_Regtest_Indexer; CREATE DATABASE IF NOT EXISTS XChain_DOGE_Regtest_Decoder; CREATE DATABASE IF NOT EXISTS XChain_DOGE_Regtest_Indexer;" + restart: "no" + + hub-a: + image: xchain-node-xchain-hub + depends_on: + db-init: + condition: service_completed_successfully + environment: + <<: *hub-environment + HUB_DB_NAME: XChain_Hub_A + HUB_PUBLIC_API_URL: http://hub-a:10000 + P2P_VALIDATOR_ADDR: ws://hub-a:10001 + SEED_NODES: ws://hub-b:10001 + SIGNING_PRIVKEY_HEX: 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f + ports: + - "127.0.0.1:${HUB_A_HOST_PORT:-64230}:10000" + - "127.0.0.1:${HUB_A_P2P_HOST_PORT:-64231}:10001" + healthcheck: + <<: *service-healthcheck + test: ["CMD-SHELL", "wget -qO- --post-data='{\"jsonrpc\":\"2.0\",\"method\":\"health\",\"id\":1}' --header='Content-Type: application/json' http://127.0.0.1:10000/ >/dev/null 2>&1"] + restart: unless-stopped + + hub-b: + image: xchain-node-xchain-hub + depends_on: + db-init: + condition: service_completed_successfully + environment: + <<: *hub-environment + HUB_DB_NAME: XChain_Hub_B + HUB_PUBLIC_API_URL: http://hub-b:10000 + P2P_VALIDATOR_ADDR: ws://hub-b:10001 + SEED_NODES: ws://hub-a:10001 + SIGNING_PRIVKEY_HEX: 202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f + ports: + - "127.0.0.1:${HUB_B_HOST_PORT:-64232}:10000" + - "127.0.0.1:${HUB_B_P2P_HOST_PORT:-64233}:10001" + healthcheck: + <<: *service-healthcheck + test: ["CMD-SHELL", "wget -qO- --post-data='{\"jsonrpc\":\"2.0\",\"method\":\"health\",\"id\":1}' --header='Content-Type: application/json' http://127.0.0.1:10000/ >/dev/null 2>&1"] + restart: unless-stopped + + hub-federation-init: + image: xchain-node-xchain-hub + depends_on: + hub-a: + condition: service_healthy + hub-b: + condition: service_healthy + environment: + HUB_API_KEY: ${HUB_API_KEY:-hub-failover-bulk} + entrypoint: ["/bin/sh", "-ec"] + command: + - | + for hub in hub-a hub-b; do + for registration in \ + '{"jsonrpc":"2.0","id":1,"method":"registervalidator","params":{"signing_pubkey":"03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8","addr":"ws://hub-a:10001"}}' \ + '{"jsonrpc":"2.0","id":2,"method":"registervalidator","params":{"signing_pubkey":"29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7","addr":"ws://hub-b:10001"}}'; do + wget -qO- --header="x-api-key: $${HUB_API_KEY}" --header="Content-Type: application/json" --post-data="$${registration}" "http://$${hub}:10000/" | grep -q '"status":"success"' + done + done + restart: "no" + + btc-node: + image: xchain-node-bitcoin-regtest-node + environment: + NODE_USER: ${RPC_USER:-hub-failover} + NODE_PASSWORD: ${RPC_PASSWORD:-hub-failover-rpc} + entrypoint: ["/bin/sh", "-ec"] + command: + - | + umask 077 + sed -e "s|^rpcuser=.*|rpcuser=$${NODE_USER}|" -e "s|^rpcpassword=.*|rpcpassword=$${NODE_PASSWORD}|" /etc/bitcoin/bitcoin.conf > /run/bitcoin.conf + exec bitcoind -conf=/run/bitcoin.conf -datadir=/root/.bitcoin/ + tmpfs: + - /run + ports: + - "127.0.0.1:${BTC_NODE_HOST_PORT:-64201}:18444" + volumes: + - btc-node-data:/root/.bitcoin + healthcheck: + <<: *node-healthcheck + test: ["CMD-SHELL", "bitcoin-cli -conf=/run/bitcoin.conf -datadir=/root/.bitcoin/ getblockchaininfo >/dev/null 2>&1"] + restart: unless-stopped + + btc-miner: + image: xchain-node-bitcoin-regtest-xchain-regtest-miner + depends_on: + btc-node: + condition: service_healthy + environment: + NETWORK: bitcoin-regtest + NODE_URL: btc-node + NODE_PORT: 18444 + NODE_USER: ${RPC_USER:-hub-failover} + NODE_PASSWORD: ${RPC_PASSWORD:-hub-failover-rpc} + REGTEST_MINER_API_PORT: 3005 + ports: + - "127.0.0.1:${BTC_MINER_HOST_PORT:-64206}:3005" + healthcheck: + <<: *service-healthcheck + test: ["CMD-SHELL", "node -e \"const h=require('http');const q=h.request({host:'127.0.0.1',port:3005,method:'POST',headers:{'content-type':'application/json'}},r=>process.exit(r.statusCode===200?0:1));q.on('error',()=>process.exit(1));q.end(JSON.stringify({jsonrpc:'2.0',method:'health',id:1}))\""] + restart: unless-stopped + + btc-tracker: + image: xchain-node-bitcoin-regtest-xchain-utxo-tracker + depends_on: + btc-node: + condition: service_healthy + environment: + COIN: bitcoin + NETWORK: bitcoin-regtest + NODE_URL: btc-node + NODE_PORT: 18444 + NODE_USER: ${RPC_USER:-hub-failover} + NODE_PASSWORD: ${RPC_PASSWORD:-hub-failover-rpc} + UTXO_TRACKER_API_PORT: 3001 + CORS_ORIGIN: "*" + ports: + - "127.0.0.1:${BTC_TRACKER_HOST_PORT:-64202}:3001" + volumes: + - btc-tracker-data:/data/xchain-utxo-tracker + healthcheck: + <<: *service-healthcheck + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3001/status >/dev/null 2>&1"] + restart: unless-stopped + + btc-decoder: + image: xchain-node-bitcoin-regtest-xchain-decoder + depends_on: + db-init: + condition: service_completed_successfully + btc-node: + condition: service_healthy + environment: + COIN: bitcoin + NETWORK: bitcoin-regtest + NODE_URL: btc-node + NODE_PORT: 18444 + NODE_USER: ${RPC_USER:-hub-failover} + NODE_PASSWORD: ${RPC_PASSWORD:-hub-failover-rpc} + DECODER_DB_HOST: db + DECODER_DB_PORT: 3306 + DECODER_DB_NAME: XChain_BTC_Regtest_Decoder + DECODER_DB_USER: root + DECODER_DB_PASS: ${DB_PASSWORD:-hub-failover-db} + DECODER_API_PORT: 3002 + ports: + - "127.0.0.1:${BTC_DECODER_HOST_PORT:-64203}:3002" + healthcheck: + <<: *service-healthcheck + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3002/live >/dev/null 2>&1"] + restart: unless-stopped + + btc-encoder: + image: xchain-node-bitcoin-regtest-xchain-encoder + depends_on: + btc-node: + condition: service_healthy + btc-tracker: + condition: service_healthy + environment: + NETWORK: bitcoin-regtest + NODE_URL: btc-node + NODE_PORT: 18444 + NODE_USER: ${RPC_USER:-hub-failover} + NODE_PASSWORD: ${RPC_PASSWORD:-hub-failover-rpc} + UTXO_TRACKER_URL: btc-tracker + UTXO_TRACKER_API_PORT: 3001 + ENCODER_API_PORT: 3003 + ENCODER_RATE_LIMIT_RPM: 99999 + CORS_ORIGIN: "*" + ports: + - "127.0.0.1:${BTC_ENCODER_HOST_PORT:-64204}:3003" + healthcheck: + <<: *service-healthcheck + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3003/status >/dev/null 2>&1"] + restart: unless-stopped + + btc-indexer: + image: xchain-node-bitcoin-regtest-xchain-indexer + depends_on: + db-init: + condition: service_completed_successfully + btc-decoder: + condition: service_healthy + btc-tracker: + condition: service_healthy + hub-a: + condition: service_healthy + hub-b: + condition: service_healthy + hub-federation-init: + condition: service_completed_successfully + environment: + <<: [*indexer-environment, *failover-environment] + INDEXER_COIN: BTC + INDEXER_DB_NAME: XChain_BTC_Regtest_Indexer + DECODER_DB_NAME: XChain_BTC_Regtest_Decoder + UTXO_TRACKER_URL: btc-tracker + UTXO_TRACKER_API_PORT: 3001 + HUB_DB_NAME: XChain_BTC_Regtest_Indexer + ports: + - "127.0.0.1:${BTC_INDEXER_HOST_PORT:-64205}:3004" + healthcheck: + <<: *service-healthcheck + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3004/status 2>/dev/null | grep -Eq '\"bootstrapped\"[[:space:]]*:[[:space:]]*true'"] + restart: unless-stopped + + btc-indexer-control: + image: xchain-node-bitcoin-regtest-xchain-indexer + depends_on: + db-init: + condition: service_completed_successfully + btc-decoder: + condition: service_healthy + btc-tracker: + condition: service_healthy + hub-b: + condition: service_healthy + hub-federation-init: + condition: service_completed_successfully + environment: + <<: *indexer-environment + INDEXER_COIN: BTC + INDEXER_DB_NAME: XChain_BTC_Regtest_Control_Indexer + DECODER_DB_NAME: XChain_BTC_Regtest_Decoder + UTXO_TRACKER_URL: btc-tracker + UTXO_TRACKER_API_PORT: 3001 + HUB_API_URL: http://hub-b:10000 + HUB_CONFIG_URL: http://hub-b:10000 + HUB_DB_NAME: XChain_BTC_Regtest_Control_Indexer + ports: + - "127.0.0.1:${BTC_CONTROL_INDEXER_HOST_PORT:-64207}:3004" + healthcheck: + <<: *service-healthcheck + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3004/status 2>/dev/null | grep -Eq '\"bootstrapped\"[[:space:]]*:[[:space:]]*true'"] + restart: unless-stopped + + ltc-node: + image: xchain-node-litecoin-regtest-node + environment: + NODE_USER: ${RPC_USER:-hub-failover} + NODE_PASSWORD: ${RPC_PASSWORD:-hub-failover-rpc} + entrypoint: ["/bin/sh", "-ec"] + command: + - | + umask 077 + sed -e "s|^rpcuser=.*|rpcuser=$${NODE_USER}|" -e "s|^rpcpassword=.*|rpcpassword=$${NODE_PASSWORD}|" /etc/litecoin/litecoin.conf > /run/litecoin.conf + exec litecoind -conf=/run/litecoin.conf -datadir=/root/.litecoin/ + tmpfs: + - /run + ports: + - "127.0.0.1:${LTC_NODE_HOST_PORT:-64210}:18444" + volumes: + - ltc-node-data:/root/.litecoin + healthcheck: + <<: *node-healthcheck + test: ["CMD-SHELL", "litecoin-cli -conf=/run/litecoin.conf -datadir=/root/.litecoin/ getblockchaininfo >/dev/null 2>&1"] + restart: unless-stopped + + ltc-miner: + image: xchain-node-litecoin-regtest-xchain-regtest-miner + depends_on: + ltc-node: + condition: service_healthy + environment: + NETWORK: litecoin-regtest + NODE_URL: ltc-node + NODE_PORT: 18444 + NODE_USER: ${RPC_USER:-hub-failover} + NODE_PASSWORD: ${RPC_PASSWORD:-hub-failover-rpc} + REGTEST_MINER_API_PORT: 3005 + ports: + - "127.0.0.1:${LTC_MINER_HOST_PORT:-64215}:3005" + healthcheck: + <<: *service-healthcheck + test: ["CMD-SHELL", "node -e \"const h=require('http');const q=h.request({host:'127.0.0.1',port:3005,method:'POST',headers:{'content-type':'application/json'}},r=>process.exit(r.statusCode===200?0:1));q.on('error',()=>process.exit(1));q.end(JSON.stringify({jsonrpc:'2.0',method:'health',id:1}))\""] + restart: unless-stopped + + ltc-tracker: + image: xchain-node-litecoin-regtest-xchain-utxo-tracker + depends_on: + ltc-node: + condition: service_healthy + environment: + COIN: litecoin + NETWORK: litecoin-regtest + NODE_URL: ltc-node + NODE_PORT: 18444 + NODE_USER: ${RPC_USER:-hub-failover} + NODE_PASSWORD: ${RPC_PASSWORD:-hub-failover-rpc} + UTXO_TRACKER_API_PORT: 3001 + CORS_ORIGIN: "*" + ports: + - "127.0.0.1:${LTC_TRACKER_HOST_PORT:-64211}:3001" + volumes: + - ltc-tracker-data:/data/xchain-utxo-tracker + healthcheck: + <<: *service-healthcheck + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3001/status >/dev/null 2>&1"] + restart: unless-stopped + + ltc-decoder: + image: xchain-node-litecoin-regtest-xchain-decoder + depends_on: + db-init: + condition: service_completed_successfully + ltc-node: + condition: service_healthy + environment: + COIN: litecoin + NETWORK: litecoin-regtest + NODE_URL: ltc-node + NODE_PORT: 18444 + NODE_USER: ${RPC_USER:-hub-failover} + NODE_PASSWORD: ${RPC_PASSWORD:-hub-failover-rpc} + DECODER_DB_HOST: db + DECODER_DB_PORT: 3306 + DECODER_DB_NAME: XChain_LTC_Regtest_Decoder + DECODER_DB_USER: root + DECODER_DB_PASS: ${DB_PASSWORD:-hub-failover-db} + DECODER_API_PORT: 3002 + ports: + - "127.0.0.1:${LTC_DECODER_HOST_PORT:-64212}:3002" + healthcheck: + <<: *service-healthcheck + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3002/live >/dev/null 2>&1"] + restart: unless-stopped + + ltc-encoder: + image: xchain-node-litecoin-regtest-xchain-encoder + depends_on: + ltc-node: + condition: service_healthy + ltc-tracker: + condition: service_healthy + environment: + NETWORK: litecoin-regtest + NODE_URL: ltc-node + NODE_PORT: 18444 + NODE_USER: ${RPC_USER:-hub-failover} + NODE_PASSWORD: ${RPC_PASSWORD:-hub-failover-rpc} + UTXO_TRACKER_URL: ltc-tracker + UTXO_TRACKER_API_PORT: 3001 + ENCODER_API_PORT: 3003 + ENCODER_RATE_LIMIT_RPM: 99999 + CORS_ORIGIN: "*" + ports: + - "127.0.0.1:${LTC_ENCODER_HOST_PORT:-64213}:3003" + healthcheck: + <<: *service-healthcheck + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3003/status >/dev/null 2>&1"] + restart: unless-stopped + + ltc-indexer: + image: xchain-node-litecoin-regtest-xchain-indexer + depends_on: + db-init: + condition: service_completed_successfully + ltc-decoder: + condition: service_healthy + ltc-tracker: + condition: service_healthy + hub-a: + condition: service_healthy + hub-b: + condition: service_healthy + hub-federation-init: + condition: service_completed_successfully + environment: + <<: [*indexer-environment, *failover-environment] + INDEXER_COIN: LTC + INDEXER_DB_NAME: XChain_LTC_Regtest_Indexer + DECODER_DB_NAME: XChain_LTC_Regtest_Decoder + UTXO_TRACKER_URL: ltc-tracker + UTXO_TRACKER_API_PORT: 3001 + HUB_DB_NAME: XChain_LTC_Regtest_Indexer + ports: + - "127.0.0.1:${LTC_INDEXER_HOST_PORT:-64214}:3004" + healthcheck: + <<: *service-healthcheck + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3004/status 2>/dev/null | grep -Eq '\"bootstrapped\"[[:space:]]*:[[:space:]]*true'"] + restart: unless-stopped + + doge-node: + image: xchain-node-dogecoin-regtest-node + environment: + NODE_USER: ${RPC_USER:-hub-failover} + NODE_PASSWORD: ${RPC_PASSWORD:-hub-failover-rpc} + entrypoint: ["/bin/sh", "-ec"] + command: + - | + umask 077 + sed -e "s|^rpcuser=.*|rpcuser=$${NODE_USER}|" -e "s|^rpcpassword=.*|rpcpassword=$${NODE_PASSWORD}|" /etc/dogecoin/dogecoin.conf > /run/dogecoin.conf + exec dogecoind -conf=/run/dogecoin.conf -datadir=/root/.dogecoin/ + tmpfs: + - /run + ports: + - "127.0.0.1:${DOGE_NODE_HOST_PORT:-64220}:18444" + volumes: + - doge-node-data:/root/.dogecoin + healthcheck: + <<: *node-healthcheck + test: ["CMD-SHELL", "dogecoin-cli -conf=/run/dogecoin.conf -datadir=/root/.dogecoin/ getblockchaininfo >/dev/null 2>&1"] + restart: unless-stopped + + doge-miner: + image: xchain-node-dogecoin-regtest-xchain-regtest-miner + depends_on: + doge-node: + condition: service_healthy + environment: + NETWORK: dogecoin-regtest + NODE_URL: doge-node + NODE_PORT: 18444 + NODE_USER: ${RPC_USER:-hub-failover} + NODE_PASSWORD: ${RPC_PASSWORD:-hub-failover-rpc} + REGTEST_MINER_API_PORT: 3005 + ports: + - "127.0.0.1:${DOGE_MINER_HOST_PORT:-64225}:3005" + healthcheck: + <<: *service-healthcheck + test: ["CMD-SHELL", "node -e \"const h=require('http');const q=h.request({host:'127.0.0.1',port:3005,method:'POST',headers:{'content-type':'application/json'}},r=>process.exit(r.statusCode===200?0:1));q.on('error',()=>process.exit(1));q.end(JSON.stringify({jsonrpc:'2.0',method:'health',id:1}))\""] + restart: unless-stopped + + doge-tracker: + image: xchain-node-dogecoin-regtest-xchain-utxo-tracker + depends_on: + doge-node: + condition: service_healthy + environment: + COIN: dogecoin + NETWORK: dogecoin-regtest + NODE_URL: doge-node + NODE_PORT: 18444 + NODE_USER: ${RPC_USER:-hub-failover} + NODE_PASSWORD: ${RPC_PASSWORD:-hub-failover-rpc} + UTXO_TRACKER_API_PORT: 3001 + CORS_ORIGIN: "*" + ports: + - "127.0.0.1:${DOGE_TRACKER_HOST_PORT:-64221}:3001" + volumes: + - doge-tracker-data:/data/xchain-utxo-tracker + healthcheck: + <<: *service-healthcheck + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3001/status >/dev/null 2>&1"] + restart: unless-stopped + + doge-decoder: + image: xchain-node-dogecoin-regtest-xchain-decoder + depends_on: + db-init: + condition: service_completed_successfully + doge-node: + condition: service_healthy + environment: + COIN: dogecoin + NETWORK: dogecoin-regtest + NODE_URL: doge-node + NODE_PORT: 18444 + NODE_USER: ${RPC_USER:-hub-failover} + NODE_PASSWORD: ${RPC_PASSWORD:-hub-failover-rpc} + DECODER_DB_HOST: db + DECODER_DB_PORT: 3306 + DECODER_DB_NAME: XChain_DOGE_Regtest_Decoder + DECODER_DB_USER: root + DECODER_DB_PASS: ${DB_PASSWORD:-hub-failover-db} + DECODER_API_PORT: 3002 + ports: + - "127.0.0.1:${DOGE_DECODER_HOST_PORT:-64222}:3002" + healthcheck: + <<: *service-healthcheck + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3002/live >/dev/null 2>&1"] + restart: unless-stopped + + doge-encoder: + image: xchain-node-dogecoin-regtest-xchain-encoder + depends_on: + doge-node: + condition: service_healthy + doge-tracker: + condition: service_healthy + environment: + NETWORK: dogecoin-regtest + NODE_URL: doge-node + NODE_PORT: 18444 + NODE_USER: ${RPC_USER:-hub-failover} + NODE_PASSWORD: ${RPC_PASSWORD:-hub-failover-rpc} + UTXO_TRACKER_URL: doge-tracker + UTXO_TRACKER_API_PORT: 3001 + ENCODER_API_PORT: 3003 + ENCODER_RATE_LIMIT_RPM: 99999 + CORS_ORIGIN: "*" + ports: + - "127.0.0.1:${DOGE_ENCODER_HOST_PORT:-64223}:3003" + healthcheck: + <<: *service-healthcheck + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3003/status >/dev/null 2>&1"] + restart: unless-stopped + + doge-indexer: + image: xchain-node-dogecoin-regtest-xchain-indexer + depends_on: + db-init: + condition: service_completed_successfully + doge-decoder: + condition: service_healthy + doge-tracker: + condition: service_healthy + hub-a: + condition: service_healthy + hub-b: + condition: service_healthy + hub-federation-init: + condition: service_completed_successfully + environment: + <<: [*indexer-environment, *failover-environment] + INDEXER_COIN: DOGE + INDEXER_DB_NAME: XChain_DOGE_Regtest_Indexer + DECODER_DB_NAME: XChain_DOGE_Regtest_Decoder + UTXO_TRACKER_URL: doge-tracker + UTXO_TRACKER_API_PORT: 3001 + HUB_DB_NAME: XChain_DOGE_Regtest_Indexer + ports: + - "127.0.0.1:${DOGE_INDEXER_HOST_PORT:-64224}:3004" + healthcheck: + <<: *service-healthcheck + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3004/status 2>/dev/null | grep -Eq '\"bootstrapped\"[[:space:]]*:[[:space:]]*true'"] + restart: unless-stopped + +volumes: + db-data: + btc-node-data: + btc-tracker-data: + ltc-node-data: + ltc-tracker-data: + doge-node-data: + doge-tracker-data: diff --git a/CHANGELOG.md b/CHANGELOG.md index 706e49b2..f62ed82d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.22.0] - 2026-10-04 + +### Added +- Added a two-hub regtest venue for indexer failover drills. + +### Changed +- Configured HUB_SEED_URLS and HUB_FEED_API_KEY for indexers, explorers, and managed hubs. +- Limited feed credentials to failover clients and gated the failover venue on indexer readiness. + ## [0.21.3] - 2026-10-03 ### Added diff --git a/package-lock.json b/package-lock.json index 2b2c396b..b01c466f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "xchain-node", - "version": "0.21.3", + "version": "0.22.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "xchain-node", - "version": "0.21.3", + "version": "0.22.0", "license": "AGPL-3.0-or-later", "dependencies": { "@dankest-llc/xchain-sdk": "^0.18.0", diff --git a/package.json b/package.json index 5819aa10..cc0fe95f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xchain-node", - "version": "0.21.3", + "version": "0.22.0", "description": "xchain-node allows users to install, configure and run XChain platform nodes.", "license": "AGPL-3.0-or-later", "repository": { diff --git a/src/config/index.js b/src/config/index.js index 7028545c..372a1248 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -205,8 +205,11 @@ module.exports = { get GITHUB_TOKEN() { return process.env.GITHUB_TOKEN }, get HUB_API_KEY() { return process.env.HUB_API_KEY }, get HUB_API_URL() { return process.env.HUB_API_URL }, + get HUB_FEED_API_KEY() { return process.env.HUB_FEED_API_KEY }, get HUB_NETWORK() { return process.env.HUB_NETWORK }, get HUB_PORT() { return process.env.HUB_PORT }, + get HUB_PUBLIC_API_URL() { return process.env.HUB_PUBLIC_API_URL }, + get HUB_SEED_URLS() { return process.env.HUB_SEED_URLS }, get INDEXER_API_KEY() { return process.env.INDEXER_API_KEY }, get LEVELDB_CACHE_BYTES() { return process.env.LEVELDB_CACHE_BYTES }, get LEVELDB_WRITE_BUFFER_BYTES() { return process.env.LEVELDB_WRITE_BUFFER_BYTES }, diff --git a/src/release-manifest.json b/src/release-manifest.json index 8ae75a6a..e221e3c4 100644 --- a/src/release-manifest.json +++ b/src/release-manifest.json @@ -1,9 +1,9 @@ { "_comment": [ - "Pinned component set for XChain Platform v0.21.3.", + "Pinned component set for XChain Platform v0.22.0.", "Generated by bin/write-release-manifest.js from the ACTUAL tagged master merge commits.", "xchain-node is the carrier and is not listed: checking out its tag IS this manifest.", - "A train tags only the repos it touches. This one moves xchain-vm, xchain-indexer, xchain-hub, xchain-sync, xchain-explorer and xchain-sdk.", + "A train tags only the repos it touches. This one moves xchain-indexer, xchain-hub, xchain-explorer and xchain-sdk.", "Every other component is unchanged and keeps the tag it already carries, which is", "what section 4 means by a version being the platform version at which a component", "last changed. A gap is unchanged, not skipped.", @@ -12,8 +12,8 @@ "repo's origin/master, and is a GPG-signed annotated tag verifying against the", "platform release key (fingerprint 1DA7C4896F56EA22CF491EDF4361611A82F90B70)." ], - "platform_version": "0.21.3", - "released": "2026-10-03", + "platform_version": "0.22.0", + "released": "2026-10-04", "components": { "xchain-vm": { "tag": "v0.21.3", @@ -24,12 +24,12 @@ "commit": "cc2c0802f1674e5aee397b0b9c4c4fce5dc2e551" }, "xchain-indexer": { - "tag": "v0.21.3", - "commit": "6e28b030ffa09601c8851d2b5dc0938230077083" + "tag": "v0.22.0", + "commit": "d670628fee21a6135b143f7a9dc3d2d27170311f" }, "xchain-hub": { - "tag": "v0.21.3", - "commit": "06212b5923af4a18c793d5be217f489c4e739c9f" + "tag": "v0.22.0", + "commit": "fdd04a502157787c90f318cd49c7aa7fea3754ff" }, "xchain-sync": { "tag": "v0.21.3", @@ -44,12 +44,12 @@ "commit": "8b2092686b6d5eb26117df5625a6899839d3caf4" }, "xchain-explorer": { - "tag": "v0.21.3", - "commit": "db698605ed8420f8e61239abc422af825001a500" + "tag": "v0.22.0", + "commit": "b019947e4d2544d44f628829d196b7b4848c0851" }, "xchain-sdk": { - "tag": "v0.21.3", - "commit": "da0e03ddf0546f029809657ca412c953878a14b3" + "tag": "v0.22.0", + "commit": "406db31e45d82337a551ec479e106e8f4bc13a98" }, "xchain-e2e-test": { "tag": "v0.21.0", diff --git a/src/services/config_service/database.js b/src/services/config_service/database.js index b1fb6ba5..398c02cd 100644 --- a/src/services/config_service/database.js +++ b/src/services/config_service/database.js @@ -120,6 +120,7 @@ function setIndexerHubPassword(defaultConfig, defaultValues, module) { } function mergeDefaults(defaultConfig, defaultValues) { + if (defaultValues.HUB_SEED_URLS) delete defaultConfig.HUB_API_URL for (const key in defaultValues) { if (!(key in defaultConfig)) defaultConfig[key] = defaultValues[key] } diff --git a/src/services/config_service/defaults.js b/src/services/config_service/defaults.js index d8de5b37..8a5e8b5d 100644 --- a/src/services/config_service/defaults.js +++ b/src/services/config_service/defaults.js @@ -276,15 +276,33 @@ function configureSharedBeforeHubKey(defaultValues) { // hand to a remote address; a container-local // default that overrode it would point a working production // explorer at a hostname that does not resolve. +function configureExplorerHubFeed(defaultValues, module) { + if (module !== EXPLORER_MODULE_NAME || config.EXPLORER_CHECKPOINT_SELF_SYNC === undefined || + config.EXPLORER_CHECKPOINT_SELF_SYNC === "") return + if (config.HUB_SEED_URLS !== undefined && config.HUB_SEED_URLS !== "") { + defaultValues.HUB_SEED_URLS = config.HUB_SEED_URLS + } else { + defaultValues.HUB_API_URL = config.HUB_API_URL || + ("http://" + getDockerContainerImageName(HUB_MODULE_NAME, "", "") + ":" + defaultValues.HUB_PORT) + } + if (config.HUB_FEED_API_KEY !== undefined && config.HUB_FEED_API_KEY !== "") { + defaultValues.HUB_FEED_API_KEY = config.HUB_FEED_API_KEY + } +} + +function configureManagedHubDiscovery(defaultValues, module) { + if (module !== HUB_MODULE_NAME) return + if (config.HUB_PUBLIC_API_URL !== undefined && config.HUB_PUBLIC_API_URL !== "") { + defaultValues.HUB_PUBLIC_API_URL = config.HUB_PUBLIC_API_URL + } +} + function configureSharedAfterHubKey(defaultValues, module) { if (config.CORS_ORIGIN !== undefined && config.CORS_ORIGIN !== "") { defaultValues.CORS_ORIGIN = config.CORS_ORIGIN } + configureExplorerHubFeed(defaultValues, module) if (module === EXPLORER_MODULE_NAME) { - if (config.EXPLORER_CHECKPOINT_SELF_SYNC !== undefined && config.EXPLORER_CHECKPOINT_SELF_SYNC !== "") { - defaultValues.HUB_API_URL = config.HUB_API_URL || - ("http://" + getDockerContainerImageName(HUB_MODULE_NAME, "", "") + ":" + defaultValues.HUB_PORT) - } if (config.EXPLORER_VM_QUERY_ENABLED !== undefined && config.EXPLORER_VM_QUERY_ENABLED !== "") { defaultValues.EXPLORER_VM_QUERY_ENABLED = config.EXPLORER_VM_QUERY_ENABLED } @@ -331,6 +349,7 @@ function configureSharedAfterHubKey(defaultValues, module) { } } } + configureManagedHubDiscovery(defaultValues, module) } module.exports = { diff --git a/src/services/config_service/services.js b/src/services/config_service/services.js index 13331821..932037c8 100644 --- a/src/services/config_service/services.js +++ b/src/services/config_service/services.js @@ -229,6 +229,13 @@ function configureIndexerBeforeHubKey(defaultValues, module, network) { if (config.HUB_API_KEY !== undefined && config.HUB_API_KEY !== "") { defaultValues.HUB_API_KEY = config.HUB_API_KEY } + if (config.HUB_FEED_API_KEY !== undefined && config.HUB_FEED_API_KEY !== "") { + defaultValues.HUB_FEED_API_KEY = config.HUB_FEED_API_KEY + } + if (config.HUB_SEED_URLS !== undefined && config.HUB_SEED_URLS !== "") { + defaultValues.HUB_SEED_URLS = config.HUB_SEED_URLS + delete defaultValues.HUB_API_URL + } } } diff --git a/src/services/hub_service/hub_module_config.js b/src/services/hub_service/hub_module_config.js index c509bbdd..da23d968 100644 --- a/src/services/hub_service/hub_module_config.js +++ b/src/services/hub_service/hub_module_config.js @@ -75,10 +75,7 @@ function buildHubModuleConfig(nextModule, defaultConfigCoinNetwork, ctx) { // install/recreate time from the same host env var, and the explorer falls back // to it for hand-written config.json deployments. function buildCheckpointConfig(defaultConfigCoinNetwork) { - return { - hub_url: config.HUB_API_URL || - ("http://" + getDockerContainerImageName(HUB_MODULE_NAME, "", "") + ":" + - defaultConfigCoinNetwork.HUB_PORT), + const checkpointConfig = { db_host: defaultConfigCoinNetwork.INDEXER_DB_HOST, db_port: defaultConfigCoinNetwork.INDEXER_DB_PORT, user: defaultConfigCoinNetwork.INDEXER_DB_USER, @@ -90,6 +87,17 @@ function buildCheckpointConfig(defaultConfigCoinNetwork) { name: defaultConfigCoinNetwork.INDEXER_DB_NAME + '_HubMirror', self_sync: true } + if (config.HUB_SEED_URLS !== undefined && config.HUB_SEED_URLS !== '') { + checkpointConfig.hub_seed_urls = config.HUB_SEED_URLS + } else { + checkpointConfig.hub_url = config.HUB_API_URL || + ("http://" + getDockerContainerImageName(HUB_MODULE_NAME, "", "") + ":" + + defaultConfigCoinNetwork.HUB_PORT) + } + if (config.HUB_FEED_API_KEY !== undefined && config.HUB_FEED_API_KEY !== '') { + checkpointConfig.hub_feed_api_key = config.HUB_FEED_API_KEY + } + return checkpointConfig } // Is the self-synced checkpoint mirror opted in for THIS deployment? @@ -126,7 +134,8 @@ async function isCheckpointSelfSyncEnabled(deps = {}) { if (!containerEnv) return false return (containerEnv.EXPLORER_CHECKPOINT_SELF_SYNC !== undefined && containerEnv.EXPLORER_CHECKPOINT_SELF_SYNC !== "") || - (containerEnv.HUB_API_URL !== undefined && containerEnv.HUB_API_URL !== "") + (containerEnv.HUB_API_URL !== undefined && containerEnv.HUB_API_URL !== "") || + (containerEnv.HUB_SEED_URLS !== undefined && containerEnv.HUB_SEED_URLS !== "") } module.exports = { buildHubModuleConfig, buildCheckpointConfig, isCheckpointSelfSyncEnabled } diff --git a/test/unit/config_service.test.js b/test/unit/config_service.test.js index ec13eeeb..062ea63e 100644 --- a/test/unit/config_service.test.js +++ b/test/unit/config_service.test.js @@ -333,3 +333,4 @@ require('./config_service.test/shared_service_guards.test') require('./config_service.test/ensure_hub_api_key.test') require('./config_service.test/filter_command_parameters.test') require('./config_service.test/resolve_args.test') +require('./config_service.test/hub_failover.test') diff --git a/test/unit/config_service.test/hub_failover.test.js b/test/unit/config_service.test/hub_failover.test.js new file mode 100644 index 00000000..2ee15423 --- /dev/null +++ b/test/unit/config_service.test/hub_failover.test.js @@ -0,0 +1,122 @@ +'use strict' + +const { expect } = require('chai') +const { + HUB_MODULE_NAME, + EXPLORER_MODULE_NAME, + makeServiceWithConfig +} = require('./helpers.test') + +function registerEnvCleanup() { + const envNames = [ + 'EXPLORER_CHECKPOINT_SELF_SYNC', + 'HUB_API_URL', + 'HUB_FEED_API_KEY', + 'HUB_PUBLIC_API_URL', + 'HUB_SEED_URLS' + ] + let savedEnv + + beforeEach(function () { + savedEnv = Object.fromEntries(envNames.map(name => [name, process.env[name]])) + for (const name of envNames) delete process.env[name] + }) + + afterEach(function () { + for (const [name, value] of Object.entries(savedEnv)) { + if (value === undefined) delete process.env[name] + else process.env[name] = value + } + }) +} + +describe('ConfigService indexer hub failover environment', function () { + registerEnvCleanup() + + it('writes seed and feed credentials to every indexer without a pinned feed URL', async function () { + process.env.HUB_SEED_URLS = 'default,http://seed.example:10002' + process.env.HUB_FEED_API_KEY = 'feed-key-fixture' + const config = await makeServiceWithConfig('').getDefaultConfig( + 'xchain-indexer', 'bitcoin', 'testnet') + + expect(config.HUB_SEED_URLS).to.equal('default,http://seed.example:10002') + expect(config.HUB_FEED_API_KEY).to.equal('feed-key-fixture') + expect(config).not.to.have.property('HUB_API_URL') + expect(config.HUB_CONFIG_URL).to.equal( + 'http://' + config.HUB_API_HOST + ':' + config.HUB_PORT) + }) + + it('removes an old per-coin pinned feed URL when host seed mode is set', async function () { + process.env.HUB_SEED_URLS = 'default' + const config = await makeServiceWithConfig('HUB_API_URL=http://old-feed.example:10002\n') + .getDefaultConfig('xchain-indexer', 'litecoin', 'testnet') + + expect(config.HUB_SEED_URLS).to.equal('default') + expect(config).not.to.have.property('HUB_API_URL') + expect(config.HUB_CONFIG_URL).to.equal('http://xchain-node-xchain-hub:10000') + }) + + it('keeps the pinned default when seed mode is unset', async function () { + const config = await makeServiceWithConfig('').getDefaultConfig( + 'xchain-indexer', 'dogecoin', 'mainnet') + + expect(config.HUB_API_URL).to.equal('http://xchain-node-xchain-hub:10000') + expect(config).not.to.have.property('HUB_SEED_URLS') + expect(config).not.to.have.property('HUB_FEED_API_KEY') + }) + + it('does not write indexer-only discovery settings to sibling coin services', async function () { + process.env.HUB_SEED_URLS = 'default' + process.env.HUB_FEED_API_KEY = 'feed-key-fixture' + const config = await makeServiceWithConfig('').getDefaultConfig( + 'xchain-decoder', 'bitcoin', 'testnet') + + expect(config.HUB_API_URL).to.equal('http://xchain-node-xchain-hub:10000') + expect(config).not.to.have.property('HUB_SEED_URLS') + expect(config).not.to.have.property('HUB_FEED_API_KEY') + }) +}) + +describe('ConfigService managed hub failover environment', function () { + registerEnvCleanup() + + it('writes seed and feed credentials to a self-syncing explorer without a pinned feed URL', async function () { + process.env.EXPLORER_CHECKPOINT_SELF_SYNC = '1' + process.env.HUB_SEED_URLS = 'default' + process.env.HUB_FEED_API_KEY = 'explorer-feed-key-fixture' + const config = await makeServiceWithConfig('').getDefaultConfig( + EXPLORER_MODULE_NAME, null, null) + + expect(config.HUB_SEED_URLS).to.equal('default') + expect(config.HUB_FEED_API_KEY).to.equal('explorer-feed-key-fixture') + expect(config).not.to.have.property('HUB_API_URL') + }) + + it('writes only the public API URL to a managed hub', async function () { + process.env.HUB_FEED_API_KEY = 'hub-feed-key-fixture' + process.env.HUB_PUBLIC_API_URL = 'https://public-hub.example' + const service = makeServiceWithConfig('') + const hubConfig = await service.getDefaultConfig(HUB_MODULE_NAME, null, null) + const explorerConfig = await service.getDefaultConfig(EXPLORER_MODULE_NAME, null, null) + + expect(hubConfig.HUB_PUBLIC_API_URL).to.equal('https://public-hub.example') + expect(hubConfig).not.to.have.property('HUB_FEED_API_KEY') + expect(explorerConfig).not.to.have.property('HUB_FEED_API_KEY') + expect(explorerConfig).not.to.have.property('HUB_PUBLIC_API_URL') + }) + + it('treats empty failover environment values as unset', async function () { + process.env.HUB_SEED_URLS = '' + process.env.HUB_FEED_API_KEY = '' + process.env.HUB_PUBLIC_API_URL = '' + const service = makeServiceWithConfig('') + const indexerConfig = await service.getDefaultConfig( + 'xchain-indexer', 'bitcoin', 'testnet') + const hubConfig = await service.getDefaultConfig(HUB_MODULE_NAME, null, null) + + expect(indexerConfig.HUB_API_URL).to.equal('http://xchain-node-xchain-hub:10000') + expect(indexerConfig).not.to.have.property('HUB_SEED_URLS') + expect(indexerConfig).not.to.have.property('HUB_FEED_API_KEY') + expect(hubConfig).not.to.have.property('HUB_PUBLIC_API_URL') + }) +}) diff --git a/test/unit/hub_service.test.js b/test/unit/hub_service.test.js index e788431c..727c05e2 100644 --- a/test/unit/hub_service.test.js +++ b/test/unit/hub_service.test.js @@ -375,3 +375,5 @@ describe('HubService.isCheckpointSelfSyncEnabled', function () { expect(enabled).to.be.false }) }) + +require('./hub_service_failover.test') diff --git a/test/unit/hub_service_failover.test.js b/test/unit/hub_service_failover.test.js new file mode 100644 index 00000000..35f5910b --- /dev/null +++ b/test/unit/hub_service_failover.test.js @@ -0,0 +1,61 @@ +'use strict' + +const { expect } = require('chai') +const { + buildCheckpointConfig, + isCheckpointSelfSyncEnabled +} = require('../../src/services/hub_service/hub_module_config') + +const COIN_CONFIG = { + INDEXER_DB_HOST: 'mariadb', + INDEXER_DB_PORT: 3306, + INDEXER_DB_USER: 'xchain_indexer_bitcoin_regtest', + INDEXER_DB_PASS: 'secret', + INDEXER_DB_NAME: 'XChain_BTC_regtest', + HUB_PORT: 10000 +} + +describe('HubService failover checkpoint config', function () { + const envNames = ['HUB_API_URL', 'HUB_FEED_API_KEY', 'HUB_SEED_URLS'] + let savedEnv + + beforeEach(function () { + savedEnv = Object.fromEntries(envNames.map(name => [name, process.env[name]])) + for (const name of envNames) delete process.env[name] + }) + + afterEach(function () { + for (const [name, value] of Object.entries(savedEnv)) { + if (value === undefined) delete process.env[name] + else process.env[name] = value + } + }) + + it('writes seeds and the feed key without a pinned hub URL', function () { + process.env.HUB_API_URL = 'http://old-hub.internal:10002' + process.env.HUB_SEED_URLS = 'default,http://seed.internal:10002' + process.env.HUB_FEED_API_KEY = 'feed-key-fixture' + const config = buildCheckpointConfig(COIN_CONFIG) + + expect(config.hub_seed_urls).to.equal('default,http://seed.internal:10002') + expect(config.hub_feed_api_key).to.equal('feed-key-fixture') + expect(config).not.to.have.property('hub_url') + }) + + it('writes the feed key alongside a pinned endpoint when seeds are unset', function () { + process.env.HUB_FEED_API_KEY = 'feed-key-fixture' + const config = buildCheckpointConfig(COIN_CONFIG) + + expect(config.hub_feed_api_key).to.equal('feed-key-fixture') + expect(config.hub_url).to.equal('http://xchain-node-xchain-hub:10000') + expect(config).not.to.have.property('hub_seed_urls') + }) + + it('stays opted in when the explorer container remembers seed mode', async function () { + const enabled = await isCheckpointSelfSyncEnabled({ + env: {}, + readContainerEnv: async () => ({ HUB_SEED_URLS: 'default' }) + }) + expect(enabled).to.be.true + }) +})