Consensus calibration

This commit is contained in:
2026-10-20 09:26:12 -04:00
parent 4080114085
commit 9d61c177b7
11 changed files with 259 additions and 1 deletions
+2
View File
@@ -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
+62
View File
@@ -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.
+2
View File
@@ -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 |
+2
View File
@@ -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. |
+2
View File
@@ -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",
+48
View File
@@ -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;i<args.length;i++) {
if(!known.has(args[i]))throw new Error('Unknown option');
if(args[i]==='--hashes'||args[i]==='--network-hps')i++;
}
if(!args.includes('--consent'))throw new Error('Explicit --consent required: native RandomX consumes CPU, energy and ~256 MiB light / >2.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)||value<min||value>max)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;i<count;i++) {
blob.writeBigUInt64LE(BigInt(i+1),32);const t=performance.now();await engine.hash(key,blob);samples.push(performance.now()-t);
}
const warmMs=performance.now()-start,warmHps=count/(warmMs/1000);
start=performance.now();await engine.hash(otherKey,blob);const epochSwitchMs=performance.now()-start;
samples.sort((a,b)=>a-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);}
+4
View File
@@ -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));
+66
View File
@@ -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<p. NOT probability of double-spend after z confirmations. */
export function idealCatchup(attackerShare: number, deficit: number): number {
if (!Number.isFinite(attackerShare) || attackerShare < 0 || attackerShare > 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.' };
}
+1 -1
View File
@@ -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.' },
+56
View File
@@ -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)!.timestamp<steady.at(-1)!.timestamp);
assert.ok(BigInt(`0x${minimum.at(-1)!.target}`)<BigInt(`0x${MAX_TARGET}`));
assert.ok(future.every(p=>p.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/);
});
+14
View File
@@ -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);