Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ai-path/FAILURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
| 40 | **Every in-memory state that affects disconnect MUST be persisted** -- `_feeGranter` was in-memory only; crash recovery restored the tunnel but disconnect failed (0 P2P, no fee grant). Persist to `credentials.enc.json`, restore in `tryFastReconnect()`. | wallet | Agent crash → tunnel restored but cannot end session on-chain |
| 41 | **Reconnect must use same connection mode as original connect** -- `autoReconnect()` was hardcoded to `connectAuto()`, ignoring `opts.subscriptionId`/`opts.planId`. Fee-granted agents reconnected with direct payment and failed with INSUFFICIENT_BALANCE. | wallet | Agent drops → reconnect fails → permanent disconnect |
| 42 | **Fee grant pre-check must validate spend limit, not just existence** -- grant can exist and not be expired but have < 20,000 udvpn remaining (insufficient for one TX). Check `spend_limit` array before connecting. | wallet | Agent passes pre-check but fails at broadcast time with opaque chain error |
| 43 | **`/status` lies — verify ABCI correctness** -- a node can report `catching_up: false` while serving stale ABCI state (rpc.sentinel.co was 22k blocks behind tip on 2026-05-02 with `catching_up=false`). Verify health by querying a known funded address's balance, not just `/status`. Run `node tools/audit-rpc-endpoints.mjs` before each release. | chain | Wallet endpoints return 0 balance for funded addresses; integrators ship broken UX |

---

Expand Down Expand Up @@ -98,6 +99,7 @@
| C14 | queryNode() downloaded ALL nodes to find one | Single node lookup fetches 900+ nodes then `.find()` | No direct endpoint used; full paginated query used for single lookup | Try `/sentinel/node/v3/nodes/{address}` first; fall back to full list | Always use direct endpoints when querying single items |
| C15 | `max_price` Code 106 "invalid price" in MsgStartSession | Session payment fails for nodes with certain price combos (base_value=0.005, quote_value=25M) | Chain v3 price validation rejects combinations that were valid at node registration time | Catch Code 106 → retry WITHOUT `max_price` field; chain uses node's registered price directly | Always implement retry-without-max_price for MsgStartSession; 14/987 nodes affected |
| C16 | Batch payment fails on ONE bad-price node | Entire 5-node batch TX rejected with Code 106 when any node has invalid pricing | Batch contains mix of standard (40M quote) and non-standard (25M quote) prices | Retry entire batch without max_price; if still fails, fall back to individual per-node payments | Batch TX is all-or-nothing; one bad message kills all 5. Always have individual fallback. |
| C17 | RPC reports `catching_up: false` but serves stale ABCI state | Wallet endpoint returns 0 balance for a funded address; `/status` says everything is fine | `rpc.sentinel.co` was ~22k blocks behind tip on 2026-05-02 yet `catching_up=false`; clients picking it first via `RPC_ENDPOINTS[0]` got stale data on every read | Demoted `rpc.sentinel.co` to last-resort fallback; refreshed list to 12 verified endpoints sorted by latency; added `tools/audit-rpc-endpoints.mjs` that checks balance correctness, not just `/status` | Health checks must verify ABCI query correctness against a known funded address, not just `/status`. Run `node tools/audit-rpc-endpoints.mjs` before each release. |

### TUNNEL

Expand Down
47 changes: 32 additions & 15 deletions defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ export const SDK_VERSION = '2.4.0';

// ─── Timestamps ──────────────────────────────────────────────────────────────

/** When these defaults were last verified against the live chain */
export const LAST_VERIFIED = '2026-03-08T00:00:00Z';
/** When these defaults were last verified against the live chain.
* RPC + LCD endpoint lists refreshed 2026-05-02 (audit-rpc-endpoints.mjs).
* Other defaults (gas, transport rates, etc.) still 2026-03-08. */
export const LAST_VERIFIED = '2026-05-02T00:00:00Z';

/** Human-readable note for builders */
export const HARDCODED_NOTE = 'Static defaults — no RPC query server yet. Verify endpoints are live before production use. See README.md "Hardcoded Defaults" section.';
Expand All @@ -44,28 +46,43 @@ export const CHAIN_VERSION = 'v12.0.0'; // sentinelhub version
export const COSMOS_SDK_VERSION = '0.47.17';

// ─── RPC Endpoints (TX broadcast) ────────────────────────────────────────────
// Ordered by reliability. Primary is tried first, fallbacks on failure.
// Verified reachable 2026-03-08.
// Ordered by measured latency from a 22-candidate audit on 2026-04-30 against
// a known funded address (verified each endpoint returned the correct on-chain
// balance, not just /status). Run scripts/audit-rpc-endpoints.mjs to refresh.
//
// rpc.sentinel.co is intentionally LAST: on 2026-04-30 it was ~22k blocks
// behind tip and serving stale ABCI state (returned 0 for funded addresses).
// Kept as last-resort fallback because some integrators hardcode it.

export const RPC_ENDPOINTS = [
{ url: 'https://rpc.sentinel.co:443', name: 'Sentinel Official', verified: '2026-03-08' },
{ url: 'https://sentinel-rpc.polkachu.com', name: 'Polkachu', verified: '2026-03-08' },
{ url: 'https://rpc.mathnodes.com', name: 'MathNodes', verified: '2026-03-08' },
{ url: 'https://sentinel-rpc.publicnode.com', name: 'PublicNode', verified: '2026-03-08' },
{ url: 'https://rpc.sentinel.quokkastake.io', name: 'QuokkaStake', verified: '2026-03-08' },
{ url: 'https://rpc-sentinel.busurnode.com', name: 'Busurnode', verified: '2026-05-02' },
{ url: 'https://sentinel-rpc.publicnode.com', name: 'PublicNode (Allnodes)', verified: '2026-05-02' },
{ url: 'https://rpc.trinitystake.io', name: 'Trinity Stake', verified: '2026-05-02' },
{ url: 'https://rpc.sentinel.validatus.com', name: 'Validatus', verified: '2026-05-02' },
{ url: 'https://sentinel-rpc.polkachu.com', name: 'Polkachu', verified: '2026-05-02' },
{ url: 'https://rpc.dvpn.roomit.xyz', name: 'Roomit', verified: '2026-05-02' },
{ url: 'https://rpc.sentinel.quokkastake.io', name: 'QuokkaStake', verified: '2026-05-02' },
{ url: 'https://rpc.sentinel.suchnode.net', name: 'SuchNode', verified: '2026-05-02' },
{ url: 'https://rpc-sentinel.chainvibes.com', name: 'ChainVibes', verified: '2026-05-02' },
{ url: 'https://rpc.sentineldao.com', name: 'Sentinel Growth DAO', verified: '2026-05-02' },
{ url: 'https://rpc.mathnodes.com', name: 'MathNodes', verified: '2026-05-02' },
{ url: 'https://rpc.sentinel.chaintools.tech', name: 'ChainTools', verified: '2026-05-02' },
{ url: 'https://rpc.sentinel.co:443', name: 'Sentinel Official (stale fallback)', verified: '2026-05-02' },
];

export const DEFAULT_RPC = RPC_ENDPOINTS[0].url;

// ─── LCD Endpoints (REST queries) ────────────────────────────────────────────
// Ordered by reliability. All have same limitations (v3 providers = 501, plan details = 501).
// Verified reachable 2026-03-08.
// Same audit methodology as RPC. lcd.sentinel.co also returned stale data on
// 2026-04-30 (0 balance for a funded address) and sits last for parity.

export const LCD_ENDPOINTS = [
{ url: 'https://lcd.sentinel.co', name: 'Sentinel Official', verified: '2026-03-08' },
{ url: 'https://sentinel-api.polkachu.com', name: 'Polkachu', verified: '2026-03-08' },
{ url: 'https://api.sentinel.quokkastake.io', name: 'QuokkaStake', verified: '2026-03-08' },
{ url: 'https://sentinel-rest.publicnode.com', name: 'PublicNode', verified: '2026-03-08' },
{ url: 'https://api-sentinel.busurnode.com', name: 'Busurnode', verified: '2026-05-02' },
{ url: 'https://api.sentinel.suchnode.net', name: 'SuchNode', verified: '2026-05-02' },
{ url: 'https://api.sentinel.quokkastake.io', name: 'QuokkaStake', verified: '2026-05-02' },
{ url: 'https://sentinel-api.polkachu.com', name: 'Polkachu', verified: '2026-05-02' },
{ url: 'https://sentinel-rest.publicnode.com', name: 'PublicNode (Allnodes)', verified: '2026-05-02' },
{ url: 'https://lcd.sentinel.co', name: 'Sentinel Official (stale fallback)', verified: '2026-05-02' },
];

export const DEFAULT_LCD = LCD_ENDPOINTS[0].url;
Expand Down
123 changes: 123 additions & 0 deletions tools/audit-rpc-endpoints.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#!/usr/bin/env node

/**
* Audit Sentinel RPC + LCD endpoints.
*
* For each candidate, this script verifies:
* 1. Tendermint connect succeeds within 8s
* 2. /status reports `catching_up: false`
* 3. ABCI bank balance query for a known funded address returns the
* expected amount (this is stronger than /status alone -- a node can
* report in-sync while serving stale ABCI state, which is exactly the
* failure mode that bricked rpc.sentinel.co for several weeks)
*
* Output is sorted by latency, ready to paste into `defaults.js`.
*
* Usage:
* node tools/audit-rpc-endpoints.mjs
* node tools/audit-rpc-endpoints.mjs <funded-address> <expected-udvpn>
*
* The default funded address is a public wallet we control; override if it
* gets drained or moved.
*/

import { Tendermint37Client } from '@cosmjs/tendermint-rpc';
import { QueryClient, setupBankExtension } from '@cosmjs/stargate';
import { RPC_ENDPOINTS } from '../defaults.js';

const FUNDED_ADDR = process.argv[2] || 'sent1uav3z70yynp4jnt39c6pg3d6ujw78m52v2h7gs';
const EXPECTED_UDVPN = process.argv[3] || '10000000000';

// Audit candidates = SDK list + every public Sentinel RPC we've ever seen.
// Add new endpoints here when you discover them. The script will tell you
// which ones to keep in defaults.js.
const EXTRA_CANDIDATES = [
['https://rpc-sentinel.busurnode.com', 'Busurnode'],
['https://rpc-sentinel-ia.cosmosia.notional.ventures', 'Notional'],
['https://rpc.sentinel.chaintools.tech', 'ChainTools'],
['https://rpc.dvpn.roomit.xyz', 'Roomit'],
['https://sentinel-rpc.badgerbite.io', 'BadgerBite'],
['https://sentinel-rpc.validatornode.com', 'ValidatorNode'],
['https://rpc.trinitystake.io', 'Trinity Stake'],
['https://rpc.sentineldao.com', 'Sentinel Growth DAO'],
['https://public.stakewolle.com/cosmos/sentinel/rpc', 'Stakewolle'],
['https://sentinel.declab.pro:26628', 'Decloud Nodes Lab'],
['https://rpc.dvpn.me:443', 'MathNodes China'],
['https://rpc.ro.mathnodes.com:443', 'MathNodes Romania'],
['https://rpc.noncompliant.network:443', 'Noncompliant'],
['https://rpc-sentinel.chainvibes.com', 'ChainVibes'],
['https://sentinel.rpc.quasarstaking.ai:443', 'Quasar'],
['https://rpc.sentinel.validatus.com', 'Validatus'],
['https://rpc.sentinel.suchnode.net', 'SuchNode'],
];

// Dedupe by URL: SDK list takes priority for the name field.
const seen = new Set();
const candidates = [];
for (const ep of RPC_ENDPOINTS) {
if (!seen.has(ep.url)) { seen.add(ep.url); candidates.push([ep.url, ep.name]); }
}
for (const [url, name] of EXTRA_CANDIDATES) {
if (!seen.has(url)) { seen.add(url); candidates.push([url, name]); }
}

async function audit(url, name) {
const t0 = Date.now();
let tm = null;
try {
tm = await Promise.race([
Tendermint37Client.connect(url),
new Promise((_, rej) => setTimeout(() => rej(new Error('connect timeout 8s')), 8000)),
]);
const status = await Promise.race([
tm.status(),
new Promise((_, rej) => setTimeout(() => rej(new Error('status timeout 8s')), 8000)),
]);
const height = Number(status.syncInfo.latestBlockHeight);
const catchingUp = !!status.syncInfo.catchingUp;
const q = QueryClient.withExtensions(tm, setupBankExtension);
const bal = await Promise.race([
q.bank.balance(FUNDED_ADDR, 'udvpn'),
new Promise((_, rej) => setTimeout(() => rej(new Error('balance timeout 10s')), 10000)),
]);
const ms = Date.now() - t0;
const balanceOk = bal.amount === EXPECTED_UDVPN;
return { url, name, ok: !catchingUp && balanceOk, height, catchingUp, balance: bal.amount, balanceOk, ms };
} catch (e) {
const ms = Date.now() - t0;
return { url, name, ok: false, error: e.message, ms };
} finally {
try { tm && tm.disconnect(); } catch {}
}
}

console.log(`Auditing ${candidates.length} RPC endpoints against ${FUNDED_ADDR} (expected ${EXPECTED_UDVPN} udvpn)\n`);

const results = [];
for (const [url, name] of candidates) {
process.stdout.write(` ${name.padEnd(28)} ${url.padEnd(58)} `);
const r = await audit(url, name);
results.push(r);
if (r.ok) console.log(`OK h=${r.height} bal=${r.balance} ${r.ms}ms`);
else if (r.error) console.log(`FAIL ${r.error} (${r.ms}ms)`);
else console.log(`STALE catching=${r.catchingUp} balOk=${r.balanceOk} h=${r.height} bal=${r.balance}`);
}

const tier1 = results.filter(r => r.ok).sort((a, b) => a.ms - b.ms);
const tier2 = results.filter(r => !r.ok);

console.log('\n=== TIER 1 — paste this into defaults.js RPC_ENDPOINTS ===');
const today = new Date().toISOString().slice(0, 10);
for (const r of tier1) {
const lat = String(r.ms).padStart(5);
console.log(` { url: '${r.url}', name: '${r.name}', verified: '${today}' }, // ${lat}ms`);
}

console.log('\n=== TIER 2 (failed/stale/wrong-balance) ===');
for (const r of tier2) {
const reason = r.error || (r.catchingUp ? 'catching_up=true' : !r.balanceOk ? `wrong balance ${r.balance}` : 'unknown');
console.log(` ${r.url.padEnd(58)} ${reason}`);
}

console.log(`\n${tier1.length}/${results.length} healthy`);
process.exit(tier1.length === 0 ? 1 : 0);
Loading