diff --git a/README.md b/README.md index f20d4d9..a6ac961 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,8 @@ A separate mainnet profile has its own network/genesis identity, ledger and mark ## Tests and development checks +Offline preparation tools: `analyze:consensus` reports deterministic retarget/timestamp scenarios; `benchmark:randomx` runs a bounded native benchmark only with explicit `--consent`. Neither launches a node nor changes consensus or approval settings. See [calibration evidence and limitations](docs/consensus-calibration.md) before interpreting rates or illustrative targets. + ```sh npm test npm run build diff --git a/docs/consensus-calibration.md b/docs/consensus-calibration.md new file mode 100755 index 0000000..3d1d447 --- /dev/null +++ b/docs/consensus-calibration.md @@ -0,0 +1,62 @@ +# Consensus calibration and adversarial analysis — stage 3 + +**Decision remains NO-GO.** This stage supplies reproducible offline tools and bounded measurements, not new launch parameters, an independent audit or an operational network. Consensus, initial target, genesis, chain identity and approval/payment gates are unchanged. + +## Tools and safety boundaries + +- `analyze:consensus`: deterministic JSON report calling the **actual** `expectedTarget`, `medianTime` and `workFor` helpers. Six 60-record traces: steady 900-second spacing, rapid one-second spacing, slow 7,200-second spacing, a seven-day pause at a window boundary, minimum-median timestamps with normally advancing wall time, and timestamps at the allowed +120-second future edge. +- Records contain only fields read by those mathematical helpers. They are **not valid blocks**, have no PoW/signatures/state commitments, are never submitted to a ledger and do not add a fake-PoW production option. Spacing is externally imposed rather than sampled from constant-hashrate mining. The comparison of cumulative work is not proof that a miner can actually produce those traces. +- `benchmark:randomx`: requires `--consent`, uses the trusted local reference binary and accepts `--hashes` (4–256, default 32), optional `--fast` (explicit >2.3 GiB memory choice), and `--network-hps` (integer 1–10^12, default 1). Unknown/invalid arguments fail before starting native work. A 60-second wall-time deadline closes the engine; native requests also retain their own bounds. No endpoint, wallet, ledger, payout or launch option exists. CPU/energy consumption is explicit. +- Benchmark output includes executable SHA-256, platform/Node version, mode, cold hash time, repeat hash, warm sequential IPC-inclusive rate, hash latency samples and one epoch/cache switch. The declared network hashrate is a **hypothesis**, never inferred by multiplying a local worker rate by an assumed number of machines. Output targets are illustrative strings only and are never written into consensus/configuration. + +These tools can be invoked via the package scripts on Node 24. The Docker test target includes the trusted Linux-native engine; offline benchmark execution can use a network-disabled disposable container. Do not benchmark a production node or enable fast mode without an explicit memory/energy budget. Keep reports with the exact image/native digest and host/container constraints. + +## Verified arithmetic and interpretation + +Wikideck interprets the 256-bit RandomX output as a big-endian integer and accepts hash <= target. Assuming uniform hash output, success probability is `(target + 1) / 2^256`, expected trials are `2^256 / (target + 1)`, and expected seconds at H hashes/s are expected trials divided by H. These are expectations, not schedules or upper time bounds. + +The current `0fff…ffff` maximum/initial target requires **16 expected hashes**. The 15-minute setting controls the retarget goal; it does not force miners to wait 15 minutes before submitting an initially easy block. For a specified integer network rate H and goal S seconds, the analysis computes `floor(2^256 / (H*S)) - 1`, bounded by the existing allowed target range. This formula assumes stable aggregate honest work and does not choose a safe launch rate, security budget, or rounding-independent exact interval. + +The report also includes `(q/(1-q))^z` for q < 0.5 (1 for q >= 0.5), under an idealized fixed-difficulty Poisson race with attacker share q and a **known current deficit** of z blocks. This is the probability of ever catching up under those assumptions, **not** double-spend probability after z observed confirmations: attacker progress during confirmation waiting is not modeled. There is no network latency, eclipse, selfish mining, timestamp manipulation, adaptive difficulty, economic cost or attack detection model. Do not derive a payment finality policy from this table. + +## Local native measurement (2026-10-20) + +One sample, offline Linux x64 Docker, **1 CPU quota / 1 GiB memory limit**, Node v24.22.0, light mode, 32 sequential warm hashes, existing pinned RandomX v1.2.1 implementation: + +| Measurement | Result | +| --- | --- | +| Executable SHA-256 | 5e1b99ea9df075d75a676a4ea1090e62d7738d03ac6316c41f59dedfffac4318 | +| Cold hash (including initialization/startup) | 7,379 ms | +| Immediate repeated same-key hash | 1,520 ms | +| Warm batch elapsed | 16,645 ms | +| Warm sequential IPC-inclusive rate | **1.9225 H/s** | +| Warm p50 / p95 individual request | 552 / 755 ms | +| One epoch/cache switch hash | 4,226 ms | +| Initial expected interval at this measured single-worker rate | **8.323 seconds** | + +The host was **not a dedicated controlled benchmark machine**; co-tenancy, scheduling, container CPU throttling, virtualization and thermal conditions were not instrumented. The surprising repeat-vs-warm timings reinforce that one short sample cannot certify capacity. It is a `hash`/verification-path measurement with per-request IPC, not native miner batch throughput; it includes neither full block validation nor state-root/SQLite/network overhead. Cache initialization/switch measurements are single samples, not statistically characterized distributions. Fast mode was not measured. Node `process.memoryUsage` would omit the native subprocess, so no misleading whole-engine RSS claim is made. + +For an **explicitly hypothetical** aggregate 100 H/s network and 900-second target, the tool produced illustrative target `0000ba69dbdd3ac13d7c4a74c8111cb7aecee4bd24e8d5b85d9222dc8bfdff5b` (90,000 expected hashes). This was **not applied** and is not a recommendation. Expected rewrite cost, miner diversity, rate variance, launch surge/decline and adversarial hashrate remain unmeasured. + +## Deterministic scenario findings + +1. **Steady/slow at maximum target:** stable 900-second spacing retains the initial target; 8× slower spacing cannot ease above maximum. There is no wall-clock emergency easing during a stall. +2. **Rapid trace:** difficulty does not change before the ten-block boundary. After ten one-second-spaced records the target is quartered by the elapsed-time floor (`floor(8100/4) = 2025 seconds`). Further imposed rapid windows keep increasing required work; this does not mean the same hashrate could sustain those arrival times. +3. **Seven-day boundary pause:** the pause is placed before the first block of the third ten-block window. The endpoint difference within that window excludes the inter-window gap. This illustrates a boundary blind spot, not a fully executed exploit: the choice of where a pause occurs matters and requires review of alternative retarget designs. The current maximum target also limits what this particular trace can reveal about recovery from already-high difficulty. +4. **Median-minimum trace:** timestamps just above the actual prior median can satisfy timestamp bounds while lagging wall time substantially. Retarget uses timestamp differences, not independently verified elapsed mining time. Analyze time-warp/oscillation attacks with quantified hashpower and viable block production before selecting rules. +5. **Constant future-edge offset:** +120 seconds is allowed by the current timestamp rule. A constant offset cancels out of interval differences; it is not by itself evidence of a difficulty exploit. Clock skew and shifting offsets still need adversarial analysis. Validator tests reject timestamps at/below median or beyond future allowance **before proof work** and exercise the exact allowed edge reaching subsequent commitment checks. +6. **Partition work comparison:** synthetic steady and rapid branches have different accumulated work, and the existing strict-work rule favors the greater-work branch. Existing ledger/native integration tests cover real validated branch/reorg handling separately. The synthetic report is not a multi-host partition, adversarial mining or economic-security rehearsal. + +## Validation and remaining launch evidence + +Build and full regression suite passed **92 tests, 0 failed, 0 skipped**, including real RandomX HTTP integration and six new calibration/timestamp tests. Tests assert inclusive-target arithmetic, bounded inputs, exact window/clamp behavior, reproducibility, scenario timestamp bounds, probability assumptions, consent refusal before engine access and validator timestamp rejection. There is no claim of formal consensus correctness. + +Before production calibration can be approved: + +- Benchmark the exact final native/image build on multiple intended independent miner/validator hosts; record CPU model, operating mode, effective quotas, competing load, cold/warm/cache-switch distributions and batch-miner throughput. Use repeated runs and documented confidence/variance, not this single sample. +- Establish credible aggregate honest hashrate, launch ramp/surge/drop ranges and attack/rewrite cost assumptions; independently review target choice and the inherited ten-block/clamped endpoint retarget, including gaps across windows and timestamp attacks. +- Add stochastic mining/partition/clock/adaptive-hashpower models and sustained multi-host rehearsals with valid reference proofs, full state commitments, mempool/reorg persistence, networking and realistic resource pressure. +- Freeze reviewed rules and actual genesis date with a new identity where required. Publish the manifest/artifact hashes and obtain independent review and explicit operator sign-off. Do not patch an active chain's rules in place. +- Decide payment holds/reserves/disputes independently; ten confirmations cannot be sold as absolute finality on an unbootstrapped PoW chain. Real payments remain blocked. + +See [security review](security-review.md), [launch evidence register](operator-launch.md) and the read-only preflight. The calibration blocker remains open. \ No newline at end of file diff --git a/docs/operator-launch.md b/docs/operator-launch.md index c42776c..1d6f37a 100755 --- a/docs/operator-launch.md +++ b/docs/operator-launch.md @@ -23,6 +23,8 @@ The runtime image can execute the entry point with `--launch-preflight` as well; ## 2. Evidence register — all required before production sign-off +[Stage-3 consensus calibration evidence](consensus-calibration.md) contains a bounded local native measurement and deterministic timestamp/retarget scenarios. It is not the missing representative network-hashpower or independent consensus review evidence. Do not populate launch approvals from its illustrative targets. + For each item record **owner, artifact/release ID, date, evidence location, reviewer and disposition**. Empty means unverified, not passed. | Hold point | Required evidence | Current status | diff --git a/docs/security-review.md b/docs/security-review.md index 1fa5846..114a147 100755 --- a/docs/security-review.md +++ b/docs/security-review.md @@ -12,6 +12,8 @@ Scope: current consensus/ledger/P2P/RandomX integration, HTTP entry point, brows ## Findings that block production +**Stage-3 follow-up:** [Consensus calibration tools and evidence](consensus-calibration.md) add an offline native benchmark and deterministic actual-helper scenarios plus validator timestamp regressions. Initial target is 16 expected hashes; a short quota-limited sample is not a network calibration. SEC-02 remains open pending representative hashpower, adversarial/multi-host evidence and independent review; no consensus/genesis change was made. + | ID | Priority / scope | Evidence and impact | Required disposition | | --- | --- | --- | --- | | SEC-01 | High · public node availability | [HTTP full-state routes](../server/testnet/http.ts#L63) return [full cloned canonical snapshots](../server/testnet/ledger.ts#L223). The ledger retains [all DAG nodes in memory](../server/testnet/ledger.ts#L29); state-root calculation rebuilds the NFT set. Read cost and startup replay grow with chain/fork history. Per-IP request limits do not impose a global CPU, response-byte, memory or disk budget. | Bounded/paged public RPC and a compatible paged client; tested caching/indexing; validation admission budgets; disk/fork retention or a reviewed archival strategy. Demonstrate the intended workload and malicious traffic do not starve consensus or trigger OOM/disk exhaustion. Do not add an arbitrary consensus height cap. | diff --git a/package.json b/package.json index e929a55..f0fcd8c 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "start": "tsx server/index.ts", "launch:preflight": "node --experimental-strip-types server/index.ts --launch-preflight", "test:rpc-load": "node --experimental-strip-types scripts/rpc-load.ts", + "analyze:consensus": "node --experimental-strip-types scripts/consensus-analysis.ts", + "benchmark:randomx": "node --experimental-strip-types scripts/benchmark-randomx.ts", "miner": "tsx server/testnet/miner.ts", "native:configure": "cmake -S native -B native/build -DCMAKE_BUILD_TYPE=Release -DWIKIDECK_RANDOMX_TESTS=ON", "native:build": "cmake --build native/build --config Release --parallel 2", diff --git a/scripts/benchmark-randomx.ts b/scripts/benchmark-randomx.ts new file mode 100755 index 0000000..75a2d0a --- /dev/null +++ b/scripts/benchmark-randomx.ts @@ -0,0 +1,48 @@ +import { createHash } from 'node:crypto'; +import { readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { arch, platform } from 'node:os'; +import { RandomX } from '../server/testnet/randomx.ts'; +import { MAX_TARGET, sha256 } from '../server/testnet/consensus.ts'; +import { expectedHashes, targetForHashrate } from '../server/analysis/calibration.ts'; + +const args=process.argv.slice(2),known=new Set(['--consent','--fast','--hashes','--network-hps']); +for(let i=0;i2.3 GiB fast. No chain/network access.'); +const number=(name:string,fallback:number,min:number,max:number)=>{ + const i=args.indexOf(name),text=i<0?String(fallback):args[i+1]; + const value=Number(text); + if(!text||!/^\d+$/.test(text)||!Number.isSafeInteger(value)||valuemax)throw new Error(`Invalid ${name}`); + return value; +}; +const count=number('--hashes',32,4,256),networkHps=number('--network-hps',1,1,1_000_000_000_000); +const binary=resolve(process.env.RANDOMX_BINARY??`native/build/wikideck-randomx${process.platform==='win32'?'.exe':''}`); +// Fingerprint the exact executable; it must come from the trusted pinned build, never a download URL. +const binarySha256=createHash('sha256').update(await readFile(binary)).digest('hex'); +const engine=new RandomX({binary,fast:args.includes('--fast')}); +const stop=()=>engine.close();process.once('SIGINT',stop);process.once('SIGTERM',stop); +const deadline=setTimeout(stop,60000);deadline.unref(); +try { + const key=sha256('wikideck-offline-benchmark-epoch-a'),otherKey=sha256('wikideck-offline-benchmark-epoch-b'); + const blob=Buffer.alloc(40);Buffer.from(sha256('wikideck-offline-benchmark-blob'),'hex').copy(blob); + let start=performance.now();const first=await engine.hash(key,blob);const coldMs=performance.now()-start; + start=performance.now();if(await engine.hash(key,blob)!==first)throw new Error('Repeated reference hash differs'); + const repeatMs=performance.now()-start; + const samples:number[]=[];start=performance.now(); + for(let i=0;ia-b); + console.log(JSON.stringify({schema:'wikideck-randomx-benchmark-v1',mode:args.includes('--fast')?'fast':'light', + platform:platform(),arch:arch(),node:process.version,binarySha256,hashes:count,coldMs,repeatMs,warmMs,warmHashesPerSecond:warmHps, + p50HashMs:samples[Math.floor(samples.length*.5)],p95HashMs:samples[Math.min(samples.length-1,Math.floor(samples.length*.95))],epochSwitchMs, + currentInitialExpectedHashes:expectedHashes(MAX_TARGET),currentInitialExpectedSecondsAtMeasuredSingleWorker:expectedHashes(MAX_TARGET)/warmHps, + assumedNetworkHashesPerSecond:networkHps,illustrativeTargetForAssumedNetwork:targetForHashrate(networkHps), + note:'Local single-worker IPC-inclusive warm hash benchmark, not a network/miner-throughput estimate. Cold includes initialization; epoch switch is one cache change. Network H/s is an explicit assumption, not extrapolated from this machine.', + consensusChanged:false,productionReady:false},null,2)); +} finally {clearTimeout(deadline);engine.close();process.off('SIGINT',stop);process.off('SIGTERM',stop);} \ No newline at end of file diff --git a/scripts/consensus-analysis.ts b/scripts/consensus-analysis.ts new file mode 100755 index 0000000..f7d90b0 --- /dev/null +++ b/scripts/consensus-analysis.ts @@ -0,0 +1,4 @@ +import { calibrationReport } from '../server/analysis/calibration.ts'; + +if (process.argv.slice(2).length) throw new Error('No options accepted. This deterministic analysis never selects launch parameters.'); +console.log(JSON.stringify(calibrationReport(),null,2)); \ No newline at end of file diff --git a/server/analysis/calibration.ts b/server/analysis/calibration.ts new file mode 100755 index 0000000..2229e53 --- /dev/null +++ b/server/analysis/calibration.ts @@ -0,0 +1,66 @@ +import { CONSENSUS, type TestnetBlock } from '../../shared/testnet.ts'; +import { CHAIN_ID, GENESIS_TIME, MAX_TARGET, expectedTarget, medianTime, workFor } from '../testnet/consensus.ts'; + +const SPACE = 1n << 256n; +/** Analysis only. Calls the actual retarget/median helpers but DOES NOT construct valid blocks, + * perform PoW, invoke Ledger, or provide a production fake-PoW selector. */ +export type Scenario = 'steady' | 'rapid' | 'slow' | 'stall' | 'median-minimum' | 'future-edge'; +export type TracePoint = { height: number; wallTime: number; timestamp: number; target: string; + cumulativeWork: string; expectedHashes: number; nextTarget: string }; +export function expectedHashes(target: string): number { + if (!/^[a-f0-9]{64}$/.test(target)) throw new Error('Invalid target'); + return Number(SPACE) / Number(BigInt(`0x${target}`) + 1n); +} +export function targetForHashrate(hashesPerSecond: number, seconds = CONSENSUS.targetSeconds): string { + if (!Number.isSafeInteger(hashesPerSecond) || hashesPerSecond < 1 || hashesPerSecond > 1_000_000_000_000 || + !Number.isSafeInteger(seconds) || seconds < 1 || seconds > 86400) throw new Error('Hashrate/interval outside analysis bounds'); + const denominator = BigInt(hashesPerSecond) * BigInt(seconds); + const target = SPACE / denominator - 1n; + const maximum = BigInt(`0x${MAX_TARGET}`); + return (target < 1n ? 1n : target > maximum ? maximum : target).toString(16).padStart(64,'0'); +} +export function simulate(scenario: Scenario, count = 60): TracePoint[] { + if (!['steady','rapid','slow','stall','median-minimum','future-edge'].includes(scenario) || + !Number.isSafeInteger(count) || count < 1 || count > 1000) throw new Error('Invalid bounded simulation'); + const history: TestnetBlock[] = [], result: TracePoint[] = []; + let wallTime = GENESIS_TIME, cumulativeWork = 0n; + for (let i = 0; i < count; i++) { + const target = expectedTarget(history); + const spacing = scenario === 'rapid' ? 1 : scenario === 'slow' ? CONSENSUS.targetSeconds * 8 + : scenario === 'stall' && i === 20 ? 7 * 86400 : CONSENSUS.targetSeconds; + wallTime += spacing; + const timestamp = scenario === 'median-minimum' ? medianTime(history) + 1 + : scenario === 'future-edge' ? wallTime + CONSENSUS.maxFutureSeconds : wallTime; + if (timestamp <= medianTime(history) || timestamp > wallTime + CONSENSUS.maxFutureSeconds) throw new Error('Analysis generated an invalid timestamp'); + // Only these fields are read by expectedTarget/medianTime. Never submit these records to a node. + history.push({ createdAt: timestamp, target } as TestnetBlock); + cumulativeWork += workFor(target); + result.push({ height:i+1,wallTime,timestamp,target,cumulativeWork:cumulativeWork.toString(), + expectedHashes:expectedHashes(target),nextTarget:expectedTarget(history) }); + } + return result; +} +/** Idealized fixed-difficulty Poisson race: probability of ever catching up from a CURRENT + * known z-block deficit, q 1 || + !Number.isSafeInteger(deficit) || deficit < 0 || deficit > 1000) throw new Error('Invalid race assumptions'); + if (deficit === 0 || attackerShare >= .5) return 1; + return (attackerShare / (1-attackerShare)) ** deficit; +} +export function calibrationReport() { + const scenarios = Object.fromEntries((['steady','rapid','slow','stall','median-minimum','future-edge'] as const) + .map(name=>[name,simulate(name)])); + const a=scenarios.steady.at(-1)!,b=scenarios.rapid.at(-1)!; + return { schema:'wikideck-consensus-analysis-v1',chainId:CHAIN_ID,consensusChanged:false, + scope:'Deterministic header-math scenarios only; no valid blocks, mining, payments or launch.', + initialExpectedHashes:expectedHashes(MAX_TARGET),targetSeconds:CONSENSUS.targetSeconds, + assumptions:'Spacing is externally imposed, not stochastic mining at constant hashrate. Work comparisons do not imply an attacker can produce that work.', + scenarios, + partitionExample:{steadyHeight:a.height,rapidHeight:b.height,steadyWork:a.cumulativeWork,rapidWork:b.cumulativeWork, + winnerByStrictWork:BigInt(b.cumulativeWork)>BigInt(a.cumulativeWork)?'rapid':'steady', + warning:'Models fork-choice inputs, not peer synchronization, block validity or achievable hashpower.'}, + knownDeficitCatchup:[.1,.25,.4,.49].map(share=>({attackerShare:share,probability:idealCatchup(share,10),currentDeficit:10})), + candidateTargets:[1,10,100,1000,10000].map(hps=>({assumedNetworkHashesPerSecond:hps,target:targetForHashrate(hps)})), + decision:'NO-GO: measurement, independent analysis, multi-host rehearsal and explicit operator approval still required.' }; +} \ No newline at end of file diff --git a/server/preflight.ts b/server/preflight.ts index 18a9618..b44b7f5 100755 --- a/server/preflight.ts +++ b/server/preflight.ts @@ -25,7 +25,7 @@ export function launchPreflight(env: NodeJS.ProcessEnv = process.env, now = Date } checks.push( { id: 'public-node-resource-controls', status: 'blocked', detail: 'Stage-1 global RPC budgets, bounded pages/client history and read caching exist. In-memory DAG growth, full-history UI/reconciliation and production-scale load/capacity evidence remain unresolved.' }, - { id: 'consensus-launch-calibration', status: 'blocked', detail: 'Initial RandomX difficulty, retarget behavior, final genesis and economic security have not been calibrated and independently reviewed for production.' }, + { id: 'consensus-launch-calibration', status: 'blocked', detail: 'Stage-3 bounded offline native benchmarks and deterministic retarget/timestamp analysis exist. Representative network hashrate, stochastic/multi-host adversarial evidence, reviewed launch rules/genesis and independent economic-security review remain missing.' }, { id: 'wallet-key-protection', status: 'blocked', detail: 'Stage-2 encrypted vault storage, explicit migration/unlock and lock-aware signing exist. Legacy plaintext copies, unlocked-page compromise, migration/lifecycle coverage and independent production wallet/recovery review remain unresolved.' }, { id: 'live-market-settlement', status: 'blocked', detail: 'No live SHKeeper adapter or reviewed payout/refund/reorg system exists. Mock payment cannot constitute real payment for valuable cards.' }, { id: 'release-security-review', status: 'unverified', detail: 'Independent consensus/application review and release-specific dependency/native/container scans require external evidence; an operator environment flag is not evidence.' }, diff --git a/tests/calibration.test.ts b/tests/calibration.test.ts new file mode 100755 index 0000000..31e81c5 --- /dev/null +++ b/tests/calibration.test.ts @@ -0,0 +1,56 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { execFileSync, spawnSync } from 'node:child_process'; +import { CONSENSUS } from '../shared/testnet.ts'; +import { MAX_TARGET, workFor } from '../server/testnet/consensus.ts'; +import { calibrationReport, expectedHashes, idealCatchup, simulate, targetForHashrate } from '../server/analysis/calibration.ts'; + +test('calibration math uses inclusive target probability and bounded explicit hashrate assumptions',()=>{ + assert.equal(expectedHashes(MAX_TARGET),16); + assert.equal(workFor(MAX_TARGET),16n); + assert.equal(targetForHashrate(1),((1n<<256n)/900n-1n).toString(16).padStart(64,'0')); + assert.ok(Math.abs(expectedHashes(targetForHashrate(100))-90000)<1e-8); + assert.equal(targetForHashrate(1,1),MAX_TARGET); + for(const value of [0,-1,NaN,Infinity,1.5,1e15])assert.throws(()=>targetForHashrate(value)); + assert.throws(()=>expectedHashes('bad')); +}); +test('deterministic traces preserve exact retarget boundaries and show asymmetric stall recovery',()=>{ + const steady=simulate('steady'),rapid=simulate('rapid'),slow=simulate('slow'),stall=simulate('stall'); + assert.deepEqual(simulate('rapid'),rapid); + assert.ok(steady.every(p=>p.target===MAX_TARGET)); + assert.equal(rapid[8].nextTarget,MAX_TARGET); + assert.equal(BigInt(`0x${rapid[9].nextTarget}`),BigInt(`0x${MAX_TARGET}`)/4n); + assert.equal(rapid[10].target,rapid[9].nextTarget); + assert.ok(slow.every(p=>p.target===MAX_TARGET)); // Cannot ease above MAX_TARGET. + assert.equal(stall[20].wallTime-stall[19].wallTime,7*86400); + assert.equal(stall[20].target,MAX_TARGET); // No emergency wall-clock difficulty decay. + assert.equal(stall[20].nextTarget,MAX_TARGET); + assert.ok(BigInt(rapid.at(-1)!.cumulativeWork)>BigInt(steady.at(-1)!.cumulativeWork)); + assert.throws(()=>simulate('rapid',1001)); +}); +test('median-minimum and future-edge traces are timestamp-policy inputs, not valid mined chains',()=>{ + const minimum=simulate('median-minimum'),future=simulate('future-edge'),steady=simulate('steady'); + assert.ok(minimum.at(-1)!.timestampp.timestamp-p.wallTime===CONSENSUS.maxFutureSeconds)); + assert.equal(future[9].nextTarget,MAX_TARGET); // Constant timestamp offset cancels in retarget differences. + const report=calibrationReport();assert.equal(report.consensusChanged,false); + assert.match(report.scope,/no valid blocks/i);assert.match(report.decision,/NO-GO/); +}); +test('ideal catchup probabilities are explicitly conditioned on known current deficit',()=>{ + assert.equal(idealCatchup(.5,10),1);assert.equal(idealCatchup(.9,10),1); + assert.equal(idealCatchup(.25,0),1);assert.equal(idealCatchup(0,10),0); + assert.ok(Math.abs(idealCatchup(.25,10)-(1/3)**10)<1e-15); + assert.ok(idealCatchup(.4,10)>idealCatchup(.25,10)); + for(const q of [-1,NaN,Infinity,1.1])assert.throws(()=>idealCatchup(q,10)); +}); +test('analysis CLI is offline and benchmark refuses CPU work without consent',()=>{ + const output=execFileSync(process.execPath,['--experimental-strip-types','scripts/consensus-analysis.ts'],{encoding:'utf8',timeout:30000}); + assert.equal(JSON.parse(output).initialExpectedHashes,16); + const denied=spawnSync(process.execPath,['--experimental-strip-types','scripts/benchmark-randomx.ts'],{ + encoding:'utf8',timeout:30000,env:{...process.env,RANDOMX_BINARY:'/nonexistent-no-engine-must-start'}}); + assert.notEqual(denied.status,0);assert.match(denied.stderr,/consent required/); + const bad=spawnSync(process.execPath,['--experimental-strip-types','scripts/benchmark-randomx.ts','--consent','--hashes','1000000'],{ + encoding:'utf8',timeout:30000,env:{...process.env,RANDOMX_BINARY:'/nonexistent'}}); + assert.notEqual(bad.status,0);assert.match(bad.stderr,/Invalid --hashes/); +}); \ No newline at end of file diff --git a/tests/testnet-ledger.test.ts b/tests/testnet-ledger.test.ts index 3def78e..3d8990f 100644 --- a/tests/testnet-ledger.test.ts +++ b/tests/testnet-ledger.test.ts @@ -138,6 +138,20 @@ test('testnet genesis is empty, manifest-bound, isolated and defensively copied' assert.throws(() => ledger.inventory(0, 0)); }); +test('timestamp attack boundaries reject at median and beyond future limit before proof work',async t=>{ + const {ledger,engine}=await fixture(t); + const now=GENESIS_TIME+CONSENSUS.targetSeconds*20; + t.mock.method(Date,'now',()=>now*1000); + const candidate=await mine(ledger.template(alice.owner,alice.publicKey,articleFor([]),statsSnapshot())); + const calls=engine.calls; + await assert.rejects(ledger.submit({...candidate,createdAt:GENESIS_TIME}),/timestamp/); + await assert.rejects(ledger.submit({...candidate,createdAt:now+CONSENSUS.maxFutureSeconds+1}),/timestamp/); + assert.equal(engine.calls,calls);assert.equal(ledger.size,0); + // Full validation reaches commitment checks for the exact allowed future edge, not timestamp rejection. + await assert.rejects(ledger.submit({...candidate,createdAt:now+CONSENSUS.maxFutureSeconds}),/block ID/); + assert.equal(engine.calls,calls); +}); + test('DAG retains equal-work branches, serializes duplicates and selects strictly greater cumulative work', async t => { const { ledger, dir } = await fixture(t); t.mock.method(Date, 'now', () => (GENESIS_TIME + CONSENSUS.targetSeconds * 20) * 1000);