diff --git a/package.json b/package.json index ac21927..f63c2c4 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,13 @@ { "name": "@aere/sdk", - "version": "0.16.1", - "description": "Official AERE Network SDK — typed ethers v6 client for the 16 contracts deployed on AERE L1 (chain ID 2800). WAERE, oracle, identity, faucet, card-escrow, delegated + locked staking, governance, AMM factory, bridge, NFT + marketplace, mining subscriptions.", + "version": "0.16.12", + "description": "Official AERE Network SDK: typed ethers v6 clients for AERE L1 (chain ID 2800). 35+ typed clients over a 130+ contract address book, covering core DeFi, agentic x402 settlement, compliance, ERC-4337 account abstraction, t-of-n post-quantum custody, the 5 live NIST PQC precompiles (Falcon-512/1024, ML-DSA-44, SLH-DSA-128s, SHAKE256), and a seedless hybrid PQC wallet.", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", + "bin": { + "aere": "./dist/cli/aere-cli.js" + }, "exports": { ".": { "types": "./dist/index.d.ts", @@ -15,12 +18,25 @@ "import": "./dist/addresses.js" } }, - "files": ["dist", "README.md"], + "files": [ + "dist", + "README.md" + ], "scripts": { "build": "tsc", - "test": "node --test dist/test/*.js" + "test": "node --test dist/test/*.js", + "test:live": "node dist/test/state-window-live.js" }, - "keywords": ["aere", "blockchain", "ethereum", "evm", "layer-1", "qbft", "defi", "sdk"], + "keywords": [ + "aere", + "blockchain", + "ethereum", + "evm", + "layer-1", + "qbft", + "defi", + "sdk" + ], "homepage": "https://aere.network/docs.html", "repository": "https://git.aere.network/aere-network/sdk-js", "license": "MIT", @@ -31,5 +47,8 @@ "@types/node": "^22.5.0", "ethers": "^6.13.4", "typescript": "^5.6.0" + }, + "dependencies": { + "@noble/post-quantum": "^0.6.1" } } diff --git a/src/test/state-window-live.ts b/src/test/state-window-live.ts new file mode 100644 index 0000000..cea2c7c --- /dev/null +++ b/src/test/state-window-live.ts @@ -0,0 +1,178 @@ +/** + * state-window-live.ts — prove StateWindowReader against the REAL endpoint. + * + * The unit tests in state-window.test.ts run against a fake node. A fake node + * proves the code does what its author expected; it does not prove the endpoint + * still behaves the way the author measured. This file closes that gap by + * pointing the guard at production and asserting on what comes back. + * + * It is deliberately NOT part of `npm test`: it needs the network, and a test + * suite that fails when the internet is down teaches people to ignore red. + * + * node dist/test/state-window-live.js # rpc.aere.network + * node dist/test/state-window-live.js https://rpc2.aere.network + * + * Exit 0 = the guard held and the hazard is still real. + * Exit 1 = something changed. Read the output; do not silence it. + * + * Structure, in this order, because a guard that has never been shown to fail + * cannot be trusted: + * + * STEP 1 PLANTED FAILURE. The raw call really does return a false zero. + * STEP 2 NEGATIVE CONTROL. With the guards disabled, the reader hands that + * false zero straight to the caller. + * STEP 3 THE GUARD. With the guards on, the reader refuses. + * + * If step 1 or step 2 ever stops failing, this file must be revisited: either + * the endpoint was fixed, or the measurement it rests on has gone stale. + * Read-only. No keys, no transactions. + */ +import { StateWindowReader, StateWindowError } from '../state-window.js'; + +const RPC = process.argv[2] ?? 'https://rpc.aere.network'; + +/** Measured: this address signed with nonce 123,063 in block 8,236,382. */ +const SIGNER = '0xbeb33d20dfbbd49ec7ac1f617667f1f02dfd6465'; +const TX_HASH = '0xbfd3620dc705cc1dbf5bf747c02f71f0d4f68227ab8b5ccafe3de15955587ce3'; + +let failures = 0; +const ok = (label: string, detail: string) => console.log(` PASS ${label}\n ${detail}`); +const bad = (label: string, detail: string) => { + failures++; + console.log(` FAIL ${label}\n ${detail}`); +}; + +async function send(method: string, params: unknown[]): Promise { + const res = await fetch(RPC, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }), + }); + const body = (await res.json()) as { result?: unknown; error?: { code: number; message: string } }; + if (body.error) throw new Error(`${body.error.code} ${body.error.message}`); + return body.result; +} + +async function main(): Promise { + console.log(`state-window live proof against ${RPC}`); + console.log(`run at ${new Date().toISOString()}\n`); + + // Anchor the truth in a block BODY, which is retained at any depth and so does + // not itself depend on the world state we are testing. + const tx = (await send('eth_getTransactionByHash', [TX_HASH])) as { + from: string; nonce: string; blockNumber: string; + } | null; + if (!tx) { + bad('anchor', `the endpoint does not know transaction ${TX_HASH}; cannot proceed`); + process.exit(1); + } + const deepBlock = Number.parseInt(tx.blockNumber, 16); + const trueNonce = Number.parseInt(tx.nonce, 16); + const tag = tx.blockNumber; + console.log(`ANCHOR block ${deepBlock} body says ${tx.from} signed with nonce ${trueNonce}`); + if (tx.from.toLowerCase() !== SIGNER) bad('anchor', `expected ${SIGNER}, block body says ${tx.from}`); + console.log(''); + + // ------------------------------------------------------------------------- + console.log('STEP 1 PLANTED FAILURE: the raw call must still lie'); + // ------------------------------------------------------------------------- + const rawNonce = await send('eth_getTransactionCount', [SIGNER, tag]); + if (rawNonce === '0x0') { + ok('eth_getTransactionCount returns a false zero', + `result "0x0" at block ${deepBlock}, where the block body proves ${trueNonce}`); + } else { + bad('eth_getTransactionCount returns a false zero', + `expected "0x0", got ${JSON.stringify(rawNonce)}. If this endpoint now answers honestly, ` + + `the published documentation must be updated, not this assertion.`); + } + + const rawBalance = await send('eth_getBalance', [SIGNER, tag]); + if (rawBalance === null) { + ok('eth_getBalance is honest at the same block', 'result null, which is a refusal and not a value'); + } else { + bad('eth_getBalance is honest at the same block', `expected null, got ${JSON.stringify(rawBalance)}`); + } + + await send('eth_getProof', [SIGNER, [], tag]).then( + () => bad('eth_getProof is honest at the same block', 'it answered; the state window may have changed'), + (e: Error) => ok('eth_getProof is honest at the same block', `error ${e.message}`), + ); + console.log(''); + + // ------------------------------------------------------------------------- + console.log('STEP 2 NEGATIVE CONTROL: with the guards off, the lie gets through'); + // ------------------------------------------------------------------------- + const unguarded = new StateWindowReader(send, { + windowBlocks: 100_000_000, safetyMarginBlocks: 0, corroborateNonce: false, + }); + const leaked = await unguarded.getTransactionCount(SIGNER, deepBlock); + if (leaked === 0) { + ok('the disabled guard returns 0', `getTransactionCount answered ${leaked}, which is wrong by ${trueNonce}`); + } else { + bad('the disabled guard returns 0', `expected 0, got ${leaked}; the negative control no longer controls anything`); + } + console.log(''); + + // ------------------------------------------------------------------------- + console.log('STEP 3 THE GUARD: it refuses instead of guessing'); + // ------------------------------------------------------------------------- + const reader = new StateWindowReader(send); + + await reader.getTransactionCount(SIGNER, deepBlock).then( + (v) => bad('pre-flight refuses the deep nonce', `expected a throw, got ${v}`), + (e: unknown) => + e instanceof StateWindowError && e.detail.reason === 'requested-block-outside-window' + ? ok('pre-flight refuses the deep nonce', `StateWindowError ${e.detail.reason}, depth ${e.detail.depth}`) + : bad('pre-flight refuses the deep nonce', `wrong error: ${e}`), + ); + + // Corroboration is the guard that survives a caller who races past pre-flight. + const racedPast = new StateWindowReader(send, { windowBlocks: 100_000_000, safetyMarginBlocks: 0 }); + await racedPast.getTransactionCount(SIGNER, deepBlock).then( + (v) => bad('corroboration catches what pre-flight missed', `expected a throw, got ${v}`), + (e: unknown) => + e instanceof StateWindowError && e.detail.reason === 'nonce-not-corroborated' + ? ok('corroboration catches what pre-flight missed', `StateWindowError ${e.detail.reason}, refused value ${e.detail.rawResult}`) + : bad('corroboration catches what pre-flight missed', `wrong error: ${e}`), + ); + + for (const [name, call] of [ + ['getBalance', () => reader.getBalance(SIGNER, deepBlock)], + ['getCode', () => reader.getCode(SIGNER, deepBlock)], + ['getStorageAt', () => reader.getStorageAt(SIGNER, '0x0', deepBlock)], + ] as [string, () => Promise][]) { + await call().then( + (v) => bad(`${name} refuses at depth`, `expected a throw, got ${JSON.stringify(v)}`), + (e: unknown) => + e instanceof StateWindowError + ? ok(`${name} refuses at depth`, `StateWindowError ${e.detail.reason}`) + : bad(`${name} refuses at depth`, `wrong error: ${e}`), + ); + } + + // And it still answers where the node can actually answer. + const live = await reader.getTransactionCount(SIGNER, 'latest'); + ok('the reader still answers at latest', `nonce ${live}`); + console.log(''); + + // ------------------------------------------------------------------------- + console.log('STEP 4 MEASURE the window rather than trusting the constant'); + // ------------------------------------------------------------------------- + const m = await reader.measureWindow(SIGNER, 100_000); + console.log(` head ${m.head}, deepest depth answered ${m.deepestOkDepth}, first depth refused ${m.firstFailDepth}`); + if (m.firstFailDepth === 512) { + ok('the window is still 512 blocks', `511 answers, 512 does not, measured just now`); + } else { + bad('the window is still 512 blocks', + `measured ${m.firstFailDepth}. This is not necessarily a bug, but every published ` + + `number that says 512 is now wrong and must be corrected.`); + } + + console.log(`\n${failures === 0 ? 'RESULT: PASS' : `RESULT: FAIL, ${failures} check(s)`}`); + process.exit(failures === 0 ? 0 : 1); +} + +main().catch((e) => { + console.error('live proof aborted:', e); + process.exit(2); +});