Consensus calibration
This commit is contained in:
@@ -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
|
||||
|
||||
Executable
+62
@@ -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.
|
||||
@@ -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 |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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",
|
||||
|
||||
Executable
+48
@@ -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);}
|
||||
Executable
+4
@@ -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));
|
||||
Executable
+66
@@ -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
@@ -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.' },
|
||||
|
||||
Executable
+56
@@ -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/);
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user