diff --git a/tests/ShouldSettleEurcOnTestnet/main.cpp b/tests/ShouldSettleEurcOnTestnet/main.cpp index 62f80af..a9fccc5 100644 --- a/tests/ShouldSettleEurcOnTestnet/main.cpp +++ b/tests/ShouldSettleEurcOnTestnet/main.cpp @@ -55,12 +55,16 @@ namespace { // https://developers.circle.com/stablecoins/eurc-contract-addresses constexpr std::string_view kContract = "0x08210F9170F89Ab7658F0B5E3fF39b0E03C594D4"; constexpr std::string_view kChainId = "11155111"; -// Two endpoints from different operators (Allnodes, 1RPC) — the same two -// operators the production ethereum entry uses — because the rule under test -// is "no single node's word settles an order": min_confirmations=2 below -// makes both agree, same as the mainnet chains file. +// Three endpoints from different operators (Allnodes, Tenderly, 1RPC) with +// min_confirmations=2, because the rule under test is "no single node's word +// settles an order" — and because public RPCs differ by VANTAGE POINT, not +// just uptime: 1RPC answers residential IPs and 403s the Hetzner runner, +// which stalled the first CI run at 1-of-2 forever. Any two of three settle; +// the corroboration loop stops at quorum, so the third is only ever dialed +// when one of the first two fails. constexpr std::string_view kRpcPrimary = "https://ethereum-sepolia-rpc.publicnode.com"; -constexpr std::string_view kRpcSecondary = "https://1rpc.io/sepolia"; +constexpr std::string_view kRpcSecondary = "https://sepolia.gateway.tenderly.co"; +constexpr std::string_view kRpcTertiary = "https://1rpc.io/sepolia"; // €1 donation = 100 cents = 100 × 10^(6-2) EURC base units. constexpr std::string_view kTransferUnits = "1000000"; @@ -144,11 +148,11 @@ int main(int argc, char** argv) { const fs::path chains = dir / "chains.json"; WriteFile(chains, std::format( R"({{"chains":[{{"name":"ethereum-sepolia",)" - R"("rpcs":["{}","{}"],)" + R"("rpcs":["{}","{}","{}"],)" R"("min_confirmations":2,)" R"("contract":"{}","chain_id":{},)" R"("block_tag":"latest","note":"testnet"}}]}})", - kRpcPrimary, kRpcSecondary, kContract, kChainId)); + kRpcPrimary, kRpcSecondary, kRpcTertiary, kContract, kChainId)); // Fresh random addresses — see the header for why reuse would false-pass. const fs::path pool = dir / "pool.txt"; diff --git a/tools/enable-eurc.sh b/tools/enable-eurc.sh index 6100952..9000283 100755 --- a/tools/enable-eurc.sh +++ b/tools/enable-eurc.sh @@ -90,12 +90,18 @@ if [ -n "$CHAINS_SRC" ]; then else # Several endpoints per chain, from DIFFERENT operators, because a payment # is confirmed only when min_confirmations of them independently agree that - # the money is there — no single node's word settles an order. Base gets - # three (Coinbase's own, Allnodes, Automata) so one being down still leaves - # a quorum; Ethereum gets two. All five were checked to answer - # eth_chainId AND a balanceOf eth_call at block_tag=finalized, which is - # what this rail actually asks of them (Cloudflare's endpoint refuses - # finalized eth_call, which is why it is not here). + # the money is there — no single node's word settles an order. THREE per + # chain, always: public RPCs differ by VANTAGE POINT as well as uptime — + # 1RPC answers residential IPs but 403s requests from Hetzner, which a + # two-endpoint quorum polled from the Hetzner box can never survive (the + # CI testnet gate hit exactly this: 1-of-2 forever, order never settles). + # Any two of three agreeing settle; the corroboration loop stops at + # quorum, so the third is only dialed when one of the first two fails. + # All were checked to answer eth_chainId AND a balanceOf eth_call at + # block_tag=finalized, which is what this rail actually asks of them + # (Cloudflare's endpoint refuses finalized eth_call, which is why it is + # not here) — but check them FROM THE BOX, not from home: that is the + # vantage point that matters. cat > "$WORK/chains.json" <<'JSON' {"chains": [ {"name": "base", @@ -107,6 +113,7 @@ else "chain_id": 8453, "note": "lowest network fees"}, {"name": "ethereum", "rpcs": ["https://ethereum-rpc.publicnode.com", + "https://mainnet.gateway.tenderly.co", "https://1rpc.io/eth"], "min_confirmations": 2, "contract": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c",