diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 727ec22..62bded4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,10 @@ jobs: # test() bodies in this repo, so it cannot be trusted as a gate. - run: pnpm test:types - run: pnpm test + # Run a small fixed-seed sample of Stellar property tests on every PR + - name: Stellar property test sample + if: github.event_name == 'pull_request' + run: pnpm test:fuzz:sample entrypoints: name: Package entry point smoke tests diff --git a/package.json b/package.json index b3b896b..1ee548e 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,8 @@ "format:check": "prettier --check .", "prepare": "husky", "test:fuzz": "FC_RUNS=100000 vitest run test/chains/stellar/properties.test.ts", + "test:fuzz:sample": "FC_RUNS=100 FC_SEED=12345 vitest run test/chains/stellar/properties.test.ts", + "test:fuzz:proof": "FC_PROOF_TEST=1 vitest run test/chains/stellar/properties.test.ts", "test:leak": "node --expose-gc ./node_modules/vitest/vitest.mjs run test/leaks/scan-leak.test.ts --pool=threads", "test:heap-leak": "node --expose-gc ./node_modules/vitest/vitest.mjs run test/leaks/heap-snapshot.test.ts --pool=threads", "size": "size-limit", diff --git a/test/chains/stellar/properties.test.ts b/test/chains/stellar/properties.test.ts index 8299e8d..aff25eb 100644 --- a/test/chains/stellar/properties.test.ts +++ b/test/chains/stellar/properties.test.ts @@ -14,6 +14,38 @@ import { import { computeViewTag } from '../../../src/chains/stellar/stealth'; const FC_RUNS = process.env.FC_RUNS ? parseInt(process.env.FC_RUNS, 10) : 1000; +const FC_SEED = process.env.FC_SEED ? parseInt(process.env.FC_SEED, 10) : undefined; +const FC_PATH = process.env.FC_PATH; + +// Configure fast-check with seed if provided +const configure = () => { + if (FC_SEED !== undefined) { + console.log(`\nšŸ”¬ Using fixed seed: ${FC_SEED}`); + console.log( + ` Replay with: FC_SEED=${FC_SEED} FC_RUNS=${FC_RUNS} pnpm test test/chains/stellar/properties.test.ts\n`, + ); + const config: Record = { seed: FC_SEED }; + if (FC_PATH !== undefined) { + config.path = FC_PATH; + } + return config; + } + return {}; +}; + +// Custom reporter to print seed and path on failure +const reporter = (runDetails: fc.RunDetails) => { + if (runDetails.failed) { + const seed = runDetails.seed; + const path = runDetails.counterexamplePath; + console.error(`\nāŒ Property test failed`); + console.error(` Seed: ${seed}`); + console.error(` Path: ${path}`); + const replayCmd = `FC_SEED=${seed} FC_PATH=${path} FC_RUNS=${FC_RUNS} pnpm test test/chains/stellar/properties.test.ts`; + console.error(` Replay with: ${replayCmd}\n`); + throw new Error(fc.defaultReportMessage(runDetails)); + } +}; // ───────────────────────────────────────────────────────────── // Helpers @@ -48,7 +80,7 @@ describe('scalar algebra sanity', () => { const r2 = (b + a) % L; expect(r1).toBe(r2); }), - { numRuns: FC_RUNS }, + { numRuns: FC_RUNS, ...configure(), reporter }, ); }); @@ -57,7 +89,7 @@ describe('scalar algebra sanity', () => { fc.property(scalarAnyArb, (a) => { expect((a + 0n) % L).toBe(a % L); }), - { numRuns: FC_RUNS }, + { numRuns: FC_RUNS, ...configure(), reporter }, ); }); }); @@ -73,7 +105,7 @@ describe('scalar encoding roundtrip', () => { const normalized = a % L; expect(bytesToScalar(scalarToBytes(normalized)) % L).toBe(normalized); }), - { numRuns: FC_RUNS }, + { numRuns: FC_RUNS, ...configure(), reporter }, ); }); @@ -82,7 +114,7 @@ describe('scalar encoding roundtrip', () => { fc.property(scalarAnyArb, (a) => { expect(scalarToBytes(a)).toHaveLength(32); }), - { numRuns: FC_RUNS }, + { numRuns: FC_RUNS, ...configure(), reporter }, ); }); }); @@ -99,7 +131,7 @@ describe('seedToScalar stability', () => { const b = seedToScalar(seed); expect(a).toBe(b); }), - { numRuns: FC_RUNS }, + { numRuns: FC_RUNS, ...configure(), reporter }, ); }); @@ -116,7 +148,7 @@ describe('seedToScalar stability', () => { // sanity: still a valid scalar space expect(s < 2n ** 256n).toBe(true); }), - { numRuns: FC_RUNS }, + { numRuns: FC_RUNS, ...configure(), reporter }, ); }); }); @@ -140,7 +172,7 @@ describe('stealth pubkey correctness', () => { expect(lhs.equals(rhs)).toBe(true); }), - { numRuns: FC_RUNS }, + { numRuns: FC_RUNS, ...configure(), reporter }, ); }); @@ -159,7 +191,7 @@ describe('stealth pubkey correctness', () => { expect(derived).toEqual(expected); }), - { numRuns: FC_RUNS }, + { numRuns: FC_RUNS, ...configure(), reporter }, ); }); }); @@ -175,7 +207,7 @@ describe('view tag distribution sanity', () => { const tag = computeViewTag(secret); expect(tag >= 0 && tag <= 255).toBe(true); }), - { numRuns: FC_RUNS }, + { numRuns: FC_RUNS, ...configure(), reporter }, ); }); }); @@ -195,7 +227,7 @@ describe('signWithScalar correctness', () => { expect(sig).toHaveLength(64); expect(verifySignature(sig, msg, pub)).toBe(true); }), - { numRuns: FC_RUNS }, + { numRuns: FC_RUNS, ...configure(), reporter }, ); }); @@ -210,7 +242,7 @@ describe('signWithScalar correctness', () => { expect(verifySignature(sig, m2, pub)).toBe(false); }), - { numRuns: FC_RUNS }, + { numRuns: FC_RUNS, ...configure(), reporter }, ); }); @@ -227,7 +259,25 @@ describe('signWithScalar correctness', () => { expect(verifySignature(sig, msg, pub2)).toBe(false); }), - { numRuns: FC_RUNS }, + { numRuns: FC_RUNS, ...configure(), reporter }, ); }); }); + +// ───────────────────────────────────────────────────────────── +// 7. proof test: demonstrates replay output on failure +// ───────────────────────────────────────────────────────────── + +if (process.env.FC_PROOF_TEST === '1') { + describe('replay output proof test', () => { + test('deliberately fails to prove replay output is emitted', () => { + fc.assert( + fc.property(scalarArb, (a: bigint) => { + // This will always fail + expect(a).toBe(0n); + }), + { numRuns: 10, seed: 42, reporter }, + ); + }); + }); +} diff --git a/vitest.config.ts b/vitest.config.ts index 7e7bc32..a374b9c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -31,7 +31,7 @@ export default defineConfig({ 'test/chains/evm/vectors.test.ts', 'test/chains/solana/vectors.test.ts', ], - testTimeout: 1200000, // 20 minutes for high-run nightly fuzz tests + testTimeout: 3600000, // 60 minutes for high-run nightly fuzz tests (100k runs takes ~6.5min + reporting overhead) }, benchmark: { include: ['test/chains/**/bench/**/*.bench.ts', 'test/bench/**/*.bench.ts'],