Fast time: the devnet at 60x for test networks (override-60x.json, --fast-time in both harnesses, proof script)
infra/fast-time/override-60x.json is the devnet with every clock-like consensus parameter divided by 60 and every block count unchanged (finality window, ban and min_daa 120 DAA; merge depth 60; Kaspa finality depth 720; pruning depth at the anticone bound 13,838; coinbase maturity 2; the hourly program epoch 60 blocks with a 10-block lead; the dataset day 24 minutes). The epoch length, lead and day are consensus parameters of the node since devnet-v4 a5ef8b07, carried by the override file. README lists each field, why it scales or not, the flags and the numbers. Measured (simnet.mjs, three devnet-v4 nodes, three vmine voters at 1 block/s, one real-hash CPU miner): next epoch seed in the template at 56.1 s, program swap at 65.1 s wall (DAA 60), first finality lock at 185.5 s wall (checkpoint 5, DAA 149). Both harnesses take --fast-time: finality-attacks s3 PASS in 113 s wall with 16 locks per node (the devnet rule needs 20 min of warm-up at 6 blocks/s before any lock); harness s3 partition and heal 43 s wall for three cuts against 983 s for four on the devnet profile with the same binary. Bench-log entry. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
bd4a64d559
commit
d783b13733
15 changed files with 402 additions and 24 deletions
|
|
@ -690,3 +690,22 @@ CPU proofs (`SP1_PROVER=cpu`), block-56-transfers-3shards:
|
|||
| pending | | | |
|
||||
|
||||
Reading: the shard at `S_p` is 60 M cycles on the prototype table, 9 cycles per pgas against the unit's 1,000 (the modexp entry is about 100x its SP1 cost: R1, one number); a plain transfer shard is 1,400 to 1,600 cycles per pgas because the 200-pgas intrinsic charge carries the fixed cost of the witness check and the two root computations. The aggregator statement is 1.5 to 1.7 M cycles (bincode, an unpatched sha256 of each shard's public values, the keccaks), small next to a shard. The CPU proof times are 3.8x and 4.9x the 3 October v0 numbers on a comparable statement, on a machine three times as loaded; the GPU row stays empty until the PC runs. The devnet was not touched; the simnet ran on ports 29300, 29301 and 29390 and was stopped.
|
||||
|
||||
## 2026-10-04, fast time (60x test profile), Linux cross-compile from the Mac, CI on every push (consensus-engineer)
|
||||
|
||||
Machine: Apple M5 Max, 64 GB, shared with four other agents and the live devnet (load 40 to 76 throughout), every job at nice 19 with at most 4 cargo jobs. The live devnet (26610/26611, 26640/26641, 28640), the Metal worker and the seed's `/opt/igneum/v4/bin` were not touched.
|
||||
|
||||
Fast time. `infra/fast-time/override-60x.json` is the devnet with every clock-like consensus parameter divided by 60 and every block count unchanged (`infra/fast-time/README.md` lists each field and why it scales or not). The hourly program epoch and its lead are now consensus parameters carried by the override file (`pow_epoch_blocks`, `pow_epoch_lead`, plus `pow_day_ms` for the dataset day; devnet-v4 `a5ef8b07`, devnet defaults unchanged: kaspa-consensus-core 79 tests, kaspa-pow 7, igneum-pow 39 pass, and a new test reads the 60x file and checks every rule against `DEVNET_PARAMS`). Proof (`infra/fast-time/simnet.mjs`, three devnet-v4 nodes on ports 29500+, `igneum-devnet-950`, three vmine voters sharing 1 block/s, one real-hash CPU miner thread): next epoch seed in the template at 56.1 s (DAA 53), program swap at 65.1 s wall (DAA 60; the CPU miner's new program and cache ready 397 ms later), first finality lock at 185.5 s wall (checkpoint 5, blue score 150, DAA 149; checkpoint 4 sat one DAA under `min_daa` 120), sinks identical on all three nodes. The devnet reaches the same two events at DAA 3,600 and DAA 7,200 plus a checkpoint.
|
||||
|
||||
| Harness run, same binaries | Devnet profile | 60x profile (`--fast-time`) |
|
||||
|---|---|---|
|
||||
| `tools/finality-attacks` s3, dishonest aggregators | catalogue 32 min at SCALE 0.6 on the 3 Oct node (above); on today's rule the window fills at DAA 7,200 = 20 min at 6 blocks/s before any lock | 113 s wall: 16 locks per node, 0 conflicting certificates, lock hashes agree, median lock latency 1,018 ms, PASS |
|
||||
| `tools/harness` s3, partition and heal (in-process simulator of the devnet-v4 line) | 983 s wall, four cuts of 120 / 600 / 1,800 / 3,700 virtual s (46 / 151 / 400 / 831 s wall), all PASS | 43 s wall, three cuts of 10 / 30 / 62 virtual s (18 / 20 / 26 s wall), all PASS; the 62-s cut beyond the 60-s merge depth converged with a 34-block reorg |
|
||||
|
||||
Both harnesses take `--fast-time` (`tools/harness/lib/net.mjs`, `tools/finality-attacks/lib/net.mjs`): the node and the simulator then come from `vendor/igneum-node/target-integration` (the file carries fields only the devnet-v4 line knows), the merge-depth scenarios scale their cuts with the profile, and the finality runs default to the `--quick` scale.
|
||||
|
||||
Linux cross-compile. `infra/cross/build-linux.sh`: cargo-zigbuild 0.23.4 with zig 0.17.0 (`brew install zig`, `cargo install cargo-zigbuild`, `rustup target add x86_64-unknown-linux-gnu`), target `x86_64-unknown-linux-gnu.2.36` (Debian 12 on the servers), `-p kaspad -p igneum-miner --features kaspad/igneum-pow`, target dir `vendor/igneum-node/target-linux`. Cold build: 1,856 s (30 min 56 s) at 4 jobs, nice 19, on this loaded machine; rocksdb (librocksdb-sys C++), lz4, blst, secp256k1 and the execution layer's crates all linked through zig; no crate failed, so `cross` was not needed (Docker is not installed here anyway). Output: ELF x86-64 PIE, igneumd 46,883,304 B and igneum-miner 8,978,424 B, dynamically linked against libc and libm only. Verified on igneum-seed-1 (Debian 12, glibc 2.36, 2 vCPU) in `/root/xbuild-test`: `igneumd --version` = igneumd 2.1.0, `igneum-miner --help` prints its usage, and a 60-s run of the cross-compiled node on `igneum-devnet-951` (the 60x profile at genesis bits 2^16, real proof of work) with the cross-compiled miner on one CPU thread: the miner adopted the node's 60-block epoch from the template, built its 256 MiB cache in 395 ms, found 7 blocks at 0.011 MH/s, all 7 accepted by the node's own PoW check, 0 rejected (the x86 build of the lottery hash agrees between miner and node; neither binary exposes a standalone vector check). Against the alternatives: the seed's own build took 55 min on its 2 vCPU (4 Oct 2026, above), the builder VM 10 to 25 min on 8 vCPU plus its creation and deletion. `BIN_SOURCE=mac` is wired into `infra/cloud-devnet/provision.sh` (no builder VM, build/bin from the cross-compile) and `infra/seed-nodes/stage-v4.sh` (upload to /root/v4/out, install as before).
|
||||
|
||||
CI. `.github/workflows/ci.yml` runs on every push and pull request of the private repository: igneum-pow `cargo test --release` and the igneum-census build (49 s), the two simulators' `--quick` modes under a 120-s timeout (49 s for the job; `sim/finality_v2.py --quick` is now a true smoke run, 149 s at nice 19 on this loaded Mac and under 40 s on the runner, was 745 s; `sim/difficulty/sim.py --quick` is new, 36 s here), the site build, an internal link check of `site/*.html` (151 links, 0 broken) and a gh-free identity grep of the public export list after the generic scrub (`tools/ci/identity-check.sh`, `tools/ci/forbidden-strings.txt`: 157 files, 0 hits). First run green: https://github.com/igneum-network/igneum/actions/runs/37193811336, 54 s from trigger to completion. The node fork is gitignored and too big for the free runners today; the workflow says so.
|
||||
|
||||
Not done: `igneum-harness-sim`'s per-block cost on the devnet-v4 line (about 70 ms here against 3 ms on the ordering-layer branch, the execution layer's follower) is what still bounds the harness, not the clocks; the fast-time presence window floors at one checkpoint; the GPU workers were not run on fast time (the CPU miner proved the swap).
|
||||
|
|
|
|||
101
infra/fast-time/README.md
Normal file
101
infra/fast-time/README.md
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# Fast time: the devnet at 60x for test networks
|
||||
|
||||
`override-60x.json` is the Igneum devnet with every clock-like consensus parameter divided by 60 and every block
|
||||
count unchanged. A test network on it reaches its first finality lock in about three minutes instead of two hours and
|
||||
crosses an hourly program epoch every minute, so a scenario that took hours of wall time takes minutes. 4 October 2026.
|
||||
|
||||
## Flags
|
||||
|
||||
Node (devnet-v4 line, `vendor/igneum-node-v4`, built in `vendor/igneum-node/target-integration`):
|
||||
|
||||
```
|
||||
igneumd --devnet --devnet-suffix=950 --override-params-file=infra/fast-time/override-60x.json ...
|
||||
```
|
||||
|
||||
The file is complete: every field `OverrideParams` accepts is present (`consensus/core/src/config/params.rs`), so a
|
||||
reader sees the whole profile in one place. Merge it with `skip_proof_of_work: true` for a network without miners
|
||||
(the harnesses do this), or lower `genesis_bits` for CPU miners (the devnet value 0x1d100000 is the GPU difficulty).
|
||||
|
||||
Harnesses: `node tools/finality-attacks/run.mjs s3 --fast-time`, `node tools/harness/run.mjs s3 s4 --fast-time`
|
||||
(or `IGNEUM_FAST_TIME=1`). Both then take the node from `vendor/igneum-node/target-integration/release` because the
|
||||
file carries the PoW schedule fields that only the devnet-v4 line knows; `IGNEUMD=...` overrides. The in-process
|
||||
simulator takes the same file: `igneum-harness-sim --override-params-file infra/fast-time/override-60x.json`.
|
||||
|
||||
Proof script: `node infra/fast-time/simnet.mjs` (3 nodes on ports 29500 and up, `igneum-devnet-950`, data
|
||||
`/tmp/igneum-fast-time`; records the first lock and the first program swap; see "Measured" below).
|
||||
|
||||
Miners: `igneum-miner` follows the epoch length and lead its node reports in every template (`pow_epoch`), no flag.
|
||||
The dataset day is not in the template, so a real-hash miner on a fast-time network takes `IGNEUM_POW_DAY_MS=1440000`
|
||||
in its environment (the node reads it from the file). The environment variables `IGNEUM_POW_EPOCH_BLOCKS`,
|
||||
`IGNEUM_POW_EPOCH_LEAD` and `IGNEUM_POW_DAY_MS` remain the fallback for a node without a params file; the file wins.
|
||||
|
||||
## What scales and what does not
|
||||
|
||||
Time parameters, divided by 60 (devnet value, 60x value):
|
||||
|
||||
| Parameter | Devnet | 60x | Note |
|
||||
|---|---|---|---|
|
||||
| `finality.weight_window` (W2, DAA s) | 7,200 | 120 | the 2-hour weight window becomes 2 minutes |
|
||||
| `finality.min_daa` (C5, F1, DAA s) | 7,200 | 120 | equal to the window on every network, so the first lock waits for a full window |
|
||||
| `finality.equivocation_ban` (3.6, DAA s) | 7,200 | 120 | |
|
||||
| `finality.aggregator_fallback` (DAA s) | 15 | 1 | 15/60 rounds up to the floor of one second |
|
||||
| `finality.presence_window` (Q1, checkpoints) | 20 | 1 | a count of checkpoints, but it stands for 20 x 30 = 600 DAA s of presence; 10 s is under one checkpoint, so the floor of one |
|
||||
| `pow_epoch_blocks` (DAA s) | 3,600 | 60 | the hourly program epoch becomes a minute (new in the node, 4 Oct 2026: a consensus param carried by the override file; before, only an environment variable) |
|
||||
| `pow_epoch_lead` (DAA s) | 600 | 10 | the seed is knowable one lead before the boundary; the miner confirms it a quarter of the lead later (3 DAA s at 60x, 150 on the devnet) |
|
||||
| `pow_day_ms` (ms) | 86,400,000 | 1,440,000 | the dataset day of the 256 MiB cache becomes 24 minutes (new consensus param, same change) |
|
||||
| `blockrate.merge_depth` (DAA s) | 3,600 | 60 | Kaspa's merge-depth bound, one hour of DAA score |
|
||||
| `blockrate.finality_depth` (DAA s) | 43,200 | 720 | Kaspa's pruning-side finality, 12 hours |
|
||||
| `blockrate.pruning_depth` (DAA s) | 108,000 | 13,838 | 30 hours / 60 = 1,800 is under Kaspa's anticone finalisation bound (finality + 2 x merge + 4 x mergeset x k + 2k + 2 = 13,838 at k 18, mergeset 180), and `Bps::pruning_depth` takes the larger, so this does |
|
||||
| `blockrate.coinbase_maturity` (s) | 100 | 2 | 100 s / 60 rounds up |
|
||||
| `deflationary_phase_daa_score` | 0 | 0 | the devnet has no deflationary phase |
|
||||
|
||||
Unchanged, and why:
|
||||
|
||||
| Parameter | Value | Why it stays |
|
||||
|---|---|---|
|
||||
| `blockrate.target_time_per_block` | 1,000 ms | the unit: a 60x network still makes one block per second; 60 blocks per second is a different DAG (k, parents, mergeset) and not what is being tested |
|
||||
| `blockrate.ghostdag_k`, `max_block_parents`, `mergeset_size_limit` | 18, 10, 180 | DAG shape at 1 block/s |
|
||||
| `finality.checkpoint_interval`, `checkpoint_depth` | 30, 20 | blue-score counts: a checkpoint every 30 blocks, determined 20 blocks later |
|
||||
| `finality.dust`, `aggregators` | 5, 8 | a block count and a count of keys |
|
||||
| `difficulty_window_size`, `min_difficulty_window_size`, `difficulty_sample_rate` | 661, 150, 4 | sample counts of Kaspa's sampled DAA (the reference lane of the dual rule); the controller needs its samples to estimate hash rate, and the 120-block LWMA lane, the 8-block warm-up, the 600-block reference start and the per-block clamps (`igneum::difficulty`) are block counts by design |
|
||||
| `past_median_time_window_size`, `past_median_time_sample_rate`, `timestamp_deviation_tolerance` | 27, 10, 132 | the past-median rule's window in samples; Igneum's own 10 s future and back bounds are constants of the rule |
|
||||
| `genesis_bits` | 0x1d100000 | the devnet's GPU difficulty; a CPU test network sets its own (0x1f010000 = 2^16 hashes per block) |
|
||||
| `skip_proof_of_work` | false | the harnesses turn it on themselves |
|
||||
| mass, lane, script and payload limits, `pruning_proof_m`, `max_block_level`, `crescendo_activation`, `storage_mass_parameter`, `pre_deflationary_phase_base_subsidy` | devnet | sizes and rule switches, not clocks |
|
||||
|
||||
Consequences to remember when reading a fast-time result: the merge depth (60 s) is now far under the difficulty
|
||||
window (2,641 s), so a low-difficulty side chain older than a minute cannot merge, which the devnet tolerates for an
|
||||
hour; the presence window of one checkpoint means "finality active" needs a lock within the last checkpoint; the
|
||||
partition bound of spec 3.3.1 is W / (3R) = 13 s at 1 block/s per side (200 s on the devnet), so a split longer than
|
||||
that locks on both sides as the arithmetic says (`docs/bench-log.md`, 4 Oct 2026, 6A long heal).
|
||||
|
||||
The node unit test `fast_time_60x_file_is_the_devnet_at_60x` (`consensus/core/src/config/params.rs`) reads this
|
||||
file and checks every rule above against `DEVNET_PARAMS`; `override_params_carry_the_pow_schedule` and
|
||||
`pow_schedule_defaults_and_clamps` check the new fields and that the devnet defaults are untouched.
|
||||
|
||||
## Measured (4 October 2026, Apple M5 Max shared with four other agents and the live devnet, load 40 to 60)
|
||||
|
||||
`node infra/fast-time/simnet.mjs --secs 240`: three devnet-v4 nodes (`target-integration/release/igneumd`, built
|
||||
from devnet-v4 `a5ef8b07`), three vmine voters sharing 1 block/s, one real-hash CPU miner thread on node 0.
|
||||
|
||||
| Event | Wall time from the first node's start | Chain position |
|
||||
|---|---|---|
|
||||
| next epoch seed reported in the template | 56.1 s | DAA 53 (boundary 60, lead 10, quarter-lead confirm 3) |
|
||||
| first hourly program swap: template epoch index 0 to 1 | 65.1 s | DAA 60 |
|
||||
| CPU miner's new program and 256 MiB cache ready for the new seed | 66.1 s (397 ms compile) | DAA 60 |
|
||||
| first finality lock | 185.5 s | checkpoint 5, blue score 150, DAA 149 (checkpoint 4 sat at DAA 119, one under `min_daa` 120, so it could not certify) |
|
||||
|
||||
Sinks agreed on all three nodes at the end (DAA 171). On the devnet the same two events come at DAA 3,600 (one hour)
|
||||
and DAA 7,200 plus a checkpoint (two hours and change).
|
||||
|
||||
Harness scenarios, same binaries, before and after:
|
||||
|
||||
| Run | Devnet profile | 60x profile |
|
||||
|---|---|---|
|
||||
| `tools/finality-attacks` s3 (dishonest aggregators, six voters at 6 blocks/s) | 32 min for the seven-scenario catalogue at SCALE 0.6 on the 3 Oct node (bench log 4 Oct 2026, s3 alone 180 s plus bring-up); on today's rule (`min_daa` = window) the window fills at DAA 7,200 = 20 min at 6 blocks/s, so s3 at full scale shows no lock inside its 300 s | 113 s wall, 16 locks per node, 0 conflicting certificates, lock hashes agree across nodes, median lock latency 1,018 ms: PASS |
|
||||
| `tools/harness` s3 (partition and heal in the in-process simulator, `igneum-harness-sim` of the devnet-v4 line) | 983 s wall for the four cuts (120, 600, 1,800 and 3,700 s virtual: 46, 151, 400 and 831 s wall, two at a time), all PASS | 43 s wall for the three cuts (10, 30 and 62 s virtual: 18, 20 and 26 s wall), all PASS, the 62-s cut beyond the 60-s merge depth converged with a 34-block reorg |
|
||||
|
||||
The 3 Oct catalogue run reported 3 to 47 s wall per s3 cut on the ordering-layer-only `harness` branch; the
|
||||
devnet-v4 simulator carries the execution layer and costs about 70 ms per block on this loaded machine, hence the
|
||||
983 s. The before and after above are the same binary on the same day.
|
||||
|
||||
53
infra/fast-time/override-60x.json
Normal file
53
infra/fast-time/override-60x.json
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"timestamp_deviation_tolerance": 132,
|
||||
"past_median_time_window_size": 27,
|
||||
"difficulty_window_size": 661,
|
||||
"min_difficulty_window_size": 150,
|
||||
"difficulty_rule": "igneum-dual",
|
||||
"coinbase_payload_script_public_key_max_len": 150,
|
||||
"max_coinbase_payload_len": 16384,
|
||||
"max_tx_inputs": 1000,
|
||||
"max_tx_outputs": 1000,
|
||||
"max_signature_script_len": 250000,
|
||||
"max_script_public_key_len": 10000,
|
||||
"mass_per_tx_byte": 1,
|
||||
"mass_per_script_pub_key_byte": 10,
|
||||
"mass_per_sig_op": 1000,
|
||||
"block_mass_limits": { "compute": 500000, "storage": 500000, "transient": 1000000 },
|
||||
"block_lane_limits": { "lanes_per_block": 50, "gas_per_lane": 1000000000 },
|
||||
"storage_mass_parameter": 1000000000000,
|
||||
"deflationary_phase_daa_score": 0,
|
||||
"pre_deflationary_phase_base_subsidy": 50000000000,
|
||||
"skip_proof_of_work": false,
|
||||
"max_block_level": 250,
|
||||
"pruning_proof_m": 1000,
|
||||
"blockrate": {
|
||||
"target_time_per_block": 1000,
|
||||
"ghostdag_k": 18,
|
||||
"past_median_time_sample_rate": 10,
|
||||
"difficulty_sample_rate": 4,
|
||||
"max_block_parents": 10,
|
||||
"mergeset_size_limit": 180,
|
||||
"merge_depth": 60,
|
||||
"finality_depth": 720,
|
||||
"pruning_depth": 13838,
|
||||
"coinbase_maturity": 2
|
||||
},
|
||||
"pre_crescendo_target_time_per_block": 1000,
|
||||
"crescendo_activation": 0,
|
||||
"genesis_bits": 487587840,
|
||||
"finality": {
|
||||
"checkpoint_interval": 30,
|
||||
"checkpoint_depth": 20,
|
||||
"weight_window": 120,
|
||||
"dust": 5,
|
||||
"presence_window": 1,
|
||||
"aggregators": 8,
|
||||
"equivocation_ban": 120,
|
||||
"min_daa": 120,
|
||||
"aggregator_fallback": 1
|
||||
},
|
||||
"pow_epoch_blocks": 60,
|
||||
"pow_epoch_lead": 10,
|
||||
"pow_day_ms": 1440000
|
||||
}
|
||||
126
infra/fast-time/simnet.mjs
Normal file
126
infra/fast-time/simnet.mjs
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
#!/usr/bin/env node
|
||||
// Proof of the 60x fast-time profile: a 3-node private network on 127.0.0.1 ports 29500 and up, data under
|
||||
// /tmp/igneum-fast-time, network id igneum-devnet-950, every node on infra/fast-time/override-60x.json with
|
||||
// skip_proof_of_work. Three voting vmine miners (the fin-attacks igneum-miner, one per node) share 1 block/s, the
|
||||
// devnet rate, so DAA score advances one per second as on the devnet. One real CPU miner (devnet-v4 igneum-miner,
|
||||
// --engine igneum-pow, 1 thread) follows node 0's templates and prints its program swap at every epoch boundary;
|
||||
// at the devnet genesis difficulty a CPU thread finds no block, so it adds nothing to the chain.
|
||||
//
|
||||
// Records, in wall seconds from the first node's start: the first finality lock (getFinalityCheckpoints) and the
|
||||
// first hourly program swap (template epoch index 0 -> 1 at DAA 60, and the CPU miner's "program and cache ready"
|
||||
// line for the new seed). Expected: lock within about 3 minutes (window 120 DAA + checkpoint depth), swap within
|
||||
// about 2 minutes. Never touches the live devnet (26610/26611, 26640/26641, 28640).
|
||||
//
|
||||
// node infra/fast-time/simnet.mjs [--secs 300] [--no-cpu-miner]
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdirSync, rmSync, writeFileSync, readFileSync, openSync, existsSync } from 'node:fs';
|
||||
import { connectRpc } from '../../tools/finality-attacks/lib/rpc.mjs';
|
||||
import { devAddress } from '../../tools/harness/lib/address.mjs';
|
||||
|
||||
const ROOT = new URL('../../', import.meta.url).pathname;
|
||||
const FILE = `${ROOT}infra/fast-time/override-60x.json`;
|
||||
const IGNEUMD = process.env.IGNEUMD || `${ROOT}vendor/igneum-node/target-integration/release/igneumd`;
|
||||
const VMINE = process.env.IGNEUM_VMINE || `${ROOT}vendor/igneum-node-fin-attacks/target/release/igneum-miner`;
|
||||
const CPU_MINER = process.env.IGNEUM_MINER || `${ROOT}vendor/igneum-node/target-integration/release/igneum-miner`;
|
||||
const TMP = '/tmp/igneum-fast-time';
|
||||
const BASE = 29500, SUFFIX = 950;
|
||||
const args = process.argv.slice(2);
|
||||
const SECS = +(args[args.indexOf('--secs') + 1] || 300);
|
||||
const CPU = !args.includes('--no-cpu-miner');
|
||||
const started = [];
|
||||
const log = (...a) => console.log(new Date().toISOString().slice(11, 23), ...a);
|
||||
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
||||
for (const b of [IGNEUMD, VMINE]) if (!existsSync(b)) { console.error(`missing ${b}`); process.exit(2); }
|
||||
|
||||
rmSync(TMP, { recursive: true, force: true }); mkdirSync(TMP, { recursive: true });
|
||||
const override = `${TMP}/override.json`;
|
||||
writeFileSync(override, JSON.stringify({ ...JSON.parse(readFileSync(FILE, 'utf8')), skip_proof_of_work: true }));
|
||||
|
||||
class Node {
|
||||
constructor(i, connect = []) {
|
||||
this.i = i; this.grpcPort = BASE + i * 10; this.p2pPort = BASE + i * 10 + 1; this.jsonPort = BASE + i * 10 + 2;
|
||||
this.connect = connect; this.dir = `${TMP}/n${i}`; this.logFile = `${this.dir}/node.log`;
|
||||
}
|
||||
get grpc() { return `grpc://127.0.0.1:${this.grpcPort}`; }
|
||||
async start() {
|
||||
mkdirSync(this.dir, { recursive: true });
|
||||
const a = ['--devnet', `--devnet-suffix=${SUFFIX}`, '--nodnsseed', '--disable-upnp', '--nologfiles', '--enable-unsynced-mining', '--utxoindex',
|
||||
`--appdir=${this.dir}`, `--rpclisten=127.0.0.1:${this.grpcPort}`, `--rpclisten-json=127.0.0.1:${this.jsonPort}`,
|
||||
`--listen=127.0.0.1:${this.p2pPort}`, `--override-params-file=${override}`, '--loglevel=info', '--yes'];
|
||||
if (this.connect.length) a.push(`--connect=${this.connect.join(',')}`); else a.push('--outpeers=0');
|
||||
const out = openSync(this.logFile, 'a');
|
||||
this.proc = spawn(IGNEUMD, a, { stdio: ['ignore', out, out] });
|
||||
started.push(this.proc);
|
||||
await sleep(800);
|
||||
this.rpc = await connectRpc(`ws://127.0.0.1:${this.jsonPort}`);
|
||||
log(`n${this.i} up pid ${this.proc.pid} json ${this.jsonPort} p2p ${this.p2pPort}`);
|
||||
return this;
|
||||
}
|
||||
grepLog(re) { try { return readFileSync(this.logFile, 'utf8').split('\n').filter(l => re.test(l)); } catch { return []; } }
|
||||
}
|
||||
function miner(bin, argv, name, env = {}) {
|
||||
const out = openSync(`${TMP}/${name}.log`, 'a');
|
||||
const p = spawn(bin, argv, { stdio: ['ignore', out, out], env: { ...process.env, ...env } });
|
||||
started.push(p);
|
||||
return p;
|
||||
}
|
||||
async function stopAll() {
|
||||
for (const p of started.reverse()) { try { p.kill('SIGINT'); } catch { } }
|
||||
await sleep(1500);
|
||||
for (const p of started) { try { p.kill('SIGKILL'); } catch { } }
|
||||
}
|
||||
process.on('SIGINT', async () => { await stopAll(); process.exit(130); });
|
||||
|
||||
const t0 = Date.now();
|
||||
const since = () => ((Date.now() - t0) / 1000).toFixed(1);
|
||||
const n0 = await new Node(0).start();
|
||||
const n1 = await new Node(1, [`127.0.0.1:${n0.p2pPort}`]).start();
|
||||
const n2 = await new Node(2, [`127.0.0.1:${n0.p2pPort}`]).start();
|
||||
const nodes = [n0, n1, n2];
|
||||
log(`override: ${readFileSync(override, 'utf8').length} bytes; node log says: ${n0.grepLog(/PoW schedule/).join(' | ') || '(no PoW schedule line)'}`);
|
||||
|
||||
// three voters sharing 1 block/s (the devnet rate): DAA advances 1 per second
|
||||
nodes.forEach((n, i) => miner(VMINE, ['vmine', n.grpc, String(SECS), '--share', String(1 / 3), '--bps', '1', '--label', `v${i}`], `vmine-v${i}`));
|
||||
if (CPU) miner(CPU_MINER, ['mine', n0.grpc, '1', String(SECS), 'cpu0', '--engine', 'igneum-pow', '--status-secs', '30'], 'cpu-miner', { IGNEUM_POW_DAY_MS: '1440000' });
|
||||
const pay = devAddress('fast-time');
|
||||
|
||||
let firstLock = null, firstSwap = null, firstPrepare = null, cpuSwap = null, lastDaa = 0, lastEpoch = 0, lastReport = 0;
|
||||
const samples = [];
|
||||
while (Date.now() - t0 < SECS * 1000) {
|
||||
await sleep(1000);
|
||||
let daa = null, epoch = null, nextSeed = null, cps = null;
|
||||
try {
|
||||
const t = await n0.rpc.call('getBlockTemplate', { payAddress: pay, extraData: [] });
|
||||
const pe = t.powEpoch || t.pow_epoch || {};
|
||||
daa = pe.virtualDaaScore ?? t.block?.header?.daaScore; epoch = pe.epochIndex; nextSeed = pe.nextEpochSeed;
|
||||
} catch (e) { log(`template: ${e.message}`); }
|
||||
try { cps = await n0.rpc.call('getFinalityCheckpoints', { last: 20 }); } catch { }
|
||||
const locked = (cps?.checkpoints || []).filter(c => c.state === 'locked');
|
||||
if (firstPrepare == null && nextSeed) { firstPrepare = +since(); log(`next epoch seed known at ${firstPrepare} s (daa ${daa})`); }
|
||||
if (firstSwap == null && epoch >= 1) { firstSwap = +since(); log(`PROGRAM SWAP: template epoch index ${epoch} at ${firstSwap} s wall (daa ${daa})`); }
|
||||
if (firstLock == null && locked.length) {
|
||||
firstLock = +since();
|
||||
const c = locked[0];
|
||||
log(`FIRST LOCK: checkpoint ${c.index} (blue score ${c.blueScore ?? c.blue_score}, daa ${c.daaScore ?? c.daa_score}) locked at ${firstLock} s wall`);
|
||||
}
|
||||
if (cpuSwap == null && CPU) {
|
||||
const lines = (() => { try { return readFileSync(`${TMP}/cpu-miner.log`, 'utf8').split('\n').filter(l => /program and 256 MiB cache ready/.test(l)); } catch { return []; } })();
|
||||
if (lines.length >= 2) { cpuSwap = +since(); log(`CPU MINER SWAP at ${cpuSwap} s wall: ${lines[1].trim()}`); }
|
||||
}
|
||||
if (epoch != null && epoch !== lastEpoch) { log(`epoch ${lastEpoch} -> ${epoch} at daa ${daa}, ${since()} s`); lastEpoch = epoch; }
|
||||
lastDaa = daa ?? lastDaa;
|
||||
if (Date.now() - lastReport > 15000) {
|
||||
lastReport = Date.now();
|
||||
const lockedPer = await Promise.all(nodes.map(async n => { try { const c = await n.rpc.call('getFinalityCheckpoints', { last: 400 }); return (c.checkpoints || []).filter(x => x.state === 'locked').length; } catch { return '?'; } }));
|
||||
log(`t=${since()} s daa ${daa} epoch ${epoch} nextSeed ${nextSeed ? 'known' : 'no'} locked per node ${lockedPer.join('/')}`);
|
||||
samples.push({ t: +since(), daa, epoch, locked: lockedPer });
|
||||
}
|
||||
if (firstLock != null && firstSwap != null && (cpuSwap != null || !CPU) && Date.now() - t0 > 150_000) break;
|
||||
}
|
||||
const sinks = await Promise.all(nodes.map(async n => { try { return (await n.rpc.call('getBlockDagInfo')).sink.slice(0, 12); } catch { return '?'; } }));
|
||||
const summary = { first_lock_s: firstLock, first_swap_s: firstSwap, next_seed_known_s: firstPrepare, cpu_miner_swap_s: cpuSwap, final_daa: lastDaa, sinks_agree: new Set(sinks).size === 1, sinks, samples, secs: SECS, node: IGNEUMD };
|
||||
writeFileSync(`${TMP}/summary.json`, JSON.stringify(summary, null, 2));
|
||||
log(`SUMMARY first lock ${firstLock} s, first program swap ${firstSwap} s (next seed known at ${firstPrepare} s), CPU miner swap ${cpuSwap} s, final daa ${lastDaa}, sinks agree ${summary.sinks_agree}`);
|
||||
await stopAll();
|
||||
process.exit(firstLock != null && firstSwap != null ? 0 : 1);
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -50,6 +50,11 @@
|
|||
}
|
||||
],
|
||||
"log": [
|
||||
{
|
||||
"date": "2026-10-04",
|
||||
"text": "Proving: devnet v4 shards on the Apple M5 Max CPU, loaded machine",
|
||||
"short": "Proving: devnet v4 shards on the Apple M5 Max CPU, loaded machine"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-04",
|
||||
"text": "One-click Windows workers: what the Mac could measure",
|
||||
|
|
|
|||
|
|
@ -47,8 +47,15 @@ node tools/finality-attacks/run.mjs # the achievable catalogue,
|
|||
node tools/finality-attacks/run.mjs s1 s6 # named scenarios
|
||||
node tools/finality-attacks/run.mjs --quick # short durations (smoke)
|
||||
SCALE=0.7 node tools/finality-attacks/run.mjs # scale every duration
|
||||
node tools/finality-attacks/run.mjs s3 --fast-time # the 60x fast-time profile (infra/fast-time/README.md): the weight
|
||||
# window and min_daa are 120 DAA, so the first lock comes at about
|
||||
# DAA 150 instead of 7,200; the node is the devnet-v4 integration build
|
||||
```
|
||||
|
||||
Under the devnet parameters of 4 October 2026 (`min_daa` = window = 7,200 DAA) a scenario cannot see a lock before
|
||||
DAA 7,200, which at the six miners' 6 blocks/s is 20 minutes of warm-up per scenario; `--fast-time` brings that to
|
||||
20 s (s3 measured: 16 locks per node and PASS in 113 s wall, 4 Oct 2026).
|
||||
|
||||
Results print as a table and are written to `/tmp/igneum-fin-attacks/results.{txt,json}`; the S8 transcript is at
|
||||
`/tmp/igneum-fin-attacks/s8-fin-rpc-attack.out`. Exit code is non-zero if any scenario failed.
|
||||
|
||||
|
|
|
|||
|
|
@ -13,9 +13,15 @@ import { mkdirSync, rmSync, writeFileSync, existsSync, readFileSync, openSync }
|
|||
import { createServer, connect as tcpConnect } from 'node:net';
|
||||
import { connectRpc } from './rpc.mjs';
|
||||
|
||||
export const ROOT = new URL('../../../', import.meta.url).pathname; // /Users/joshm/Projects/igneum/
|
||||
export const ROOT = new URL('../../../', import.meta.url).pathname; // the repository root
|
||||
export const TARGET = process.env.IGNEUM_FIN_TARGET || `${ROOT}vendor/igneum-node-fin-attacks/target/release`;
|
||||
export const IGNEUMD = process.env.IGNEUMD || `${TARGET}/igneumd`;
|
||||
// --fast-time (or IGNEUM_FAST_TIME=1): the 60x profile (infra/fast-time/README.md): weight window, ban and min_daa
|
||||
// 120 DAA instead of 7,200, 60-block epochs. The file carries the PoW schedule fields that only the devnet-v4 node
|
||||
// knows, so the node then defaults to the integration build of that line; the hostile vmine miner stays the
|
||||
// fin-attacks one (its RPCs are additive, it drove the v4 node before: docs/bench-log.md, 4 Oct 2026).
|
||||
export const FAST_TIME = process.argv.includes('--fast-time') || process.env.IGNEUM_FAST_TIME === '1';
|
||||
export const FAST_TIME_FILE = `${ROOT}infra/fast-time/override-60x.json`;
|
||||
export const IGNEUMD = process.env.IGNEUMD || (FAST_TIME ? `${ROOT}vendor/igneum-node/target-integration/release/igneumd` : `${TARGET}/igneumd`);
|
||||
export const MINER = process.env.IGNEUM_MINER || `${TARGET}/igneum-miner`;
|
||||
export const TMP = '/tmp/igneum-fin-attacks';
|
||||
export const BASE_PORT = 27800; // gRPC/p2p/json for node i at BASE+i*10 (+0/+1/+2)
|
||||
|
|
@ -29,7 +35,8 @@ export const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|||
export function overrideParams() {
|
||||
mkdirSync(TMP, { recursive: true });
|
||||
const file = `${TMP}/override.json`;
|
||||
writeFileSync(file, JSON.stringify({ skip_proof_of_work: true }));
|
||||
const base = FAST_TIME ? JSON.parse(readFileSync(FAST_TIME_FILE, 'utf8')) : {};
|
||||
writeFileSync(file, JSON.stringify({ ...base, skip_proof_of_work: true }));
|
||||
return file;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,12 +7,15 @@
|
|||
// node tools/finality-attacks/run.mjs s1 s6 # named scenarios
|
||||
// node tools/finality-attacks/run.mjs --quick # short durations (smoke)
|
||||
// SCALE=0.5 node tools/finality-attacks/run.mjs # scale every duration
|
||||
// node tools/finality-attacks/run.mjs s3 --fast-time # the 60x profile (infra/fast-time): the weight window fills
|
||||
// # at DAA 120 instead of 7,200, so locks start within a minute;
|
||||
// # durations default to the --quick scale
|
||||
|
||||
import { Node, Miner, Proxy, stopAll, sleep, log, assertBinaries, TMP } from './lib/net.mjs';
|
||||
import { Node, Miner, Proxy, stopAll, sleep, log, assertBinaries, TMP, FAST_TIME, IGNEUMD } from './lib/net.mjs';
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
|
||||
const QUICK = process.argv.includes('--quick');
|
||||
const SCALE = QUICK ? 0.35 : (parseFloat(process.env.SCALE || '1') || 1);
|
||||
const SCALE = process.env.SCALE ? (parseFloat(process.env.SCALE) || 1) : (QUICK || FAST_TIME ? 0.35 : 1);
|
||||
const dur = (s) => Math.max(20, Math.round(s * SCALE));
|
||||
const results = [];
|
||||
mkdirSync(TMP, { recursive: true });
|
||||
|
|
@ -323,6 +326,7 @@ const ORDER = ['s3', 's2', 's1', 's6', 's4', 's8', 's5'];
|
|||
|
||||
async function main() {
|
||||
assertBinaries();
|
||||
if (FAST_TIME) log(`fast-time 60x profile: node ${IGNEUMD}`);
|
||||
const asked = process.argv.slice(2).filter(a => !a.startsWith('--'));
|
||||
const run = asked.length ? asked : ORDER;
|
||||
for (const key of run) {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,9 @@ node tools/harness/run.mjs # the full catalogue, priority order 5
|
|||
node tools/harness/run.mjs s5 s2 --quick # named scenarios, short durations
|
||||
node tools/harness/run.mjs --no-bench-log # do not append to docs/bench-log.md
|
||||
node tools/harness/scenarios/s1-withhold.mjs --quick # one scenario on its own
|
||||
node tools/harness/run.mjs s3 s4 --fast-time # the 60x fast-time profile (infra/fast-time): merge depth 60 s, so the
|
||||
# partition and eclipse cuts are 10, 30 and 62 s instead of 600, 1,800 and
|
||||
# 3,700; nodes and the simulator come from vendor/igneum-node/target-integration
|
||||
```
|
||||
|
||||
Each run appends one dated entry to `docs/bench-log.md` with a row per scenario (criterion, measured result, pass
|
||||
|
|
|
|||
|
|
@ -14,8 +14,13 @@ import { execSync } from 'node:child_process';
|
|||
import { connectRpc } from './rpc.mjs';
|
||||
|
||||
export const ROOT = new URL('../../../', import.meta.url).pathname;
|
||||
// --fast-time (or IGNEUM_FAST_TIME=1): every clock-like consensus parameter divided by 60 (infra/fast-time/README.md).
|
||||
// The 60x file carries the PoW schedule fields, which only the devnet-v4 node and later know, so the binaries then
|
||||
// default to the integration build of that line (vendor/igneum-node/target-integration) instead of the harness worktree.
|
||||
export const FAST_TIME = process.argv.includes('--fast-time') || process.env.IGNEUM_FAST_TIME === '1';
|
||||
export const FAST_TIME_FILE = `${ROOT}infra/fast-time/override-60x.json`;
|
||||
export const WORKTREE = `${ROOT}vendor/igneum-node-harness`;
|
||||
export const TARGET = process.env.IGNEUM_HARNESS_TARGET || `${WORKTREE}/target/release`;
|
||||
export const TARGET = process.env.IGNEUM_HARNESS_TARGET || (FAST_TIME ? `${ROOT}vendor/igneum-node/target-integration/release` : `${WORKTREE}/target/release`);
|
||||
export const IGNEUMD = process.env.IGNEUMD || `${TARGET}/igneumd`;
|
||||
export const PROBE = process.env.IGNEUM_P2P_PROBE || `${TARGET}/igneum-p2p-probe`;
|
||||
export const SIM = process.env.IGNEUM_HARNESS_SIM || `${TARGET}/igneum-harness-sim`;
|
||||
|
|
@ -31,11 +36,22 @@ export function overrideParams(extra = {}) {
|
|||
mkdirSync(TMP, { recursive: true });
|
||||
const file = `${TMP}/override-params.json`;
|
||||
// skip_proof_of_work: the harness miner never hashes; every other rule (timestamps, DAA, GHOSTDAG, merge
|
||||
// depth, mass, coinbase) runs unchanged. Devnet parameters otherwise (1 BPS, k 18, merge depth 3,600).
|
||||
writeFileSync(file, JSON.stringify({ skip_proof_of_work: true, ...extra }));
|
||||
// depth, mass, coinbase) runs unchanged. Devnet parameters otherwise (1 BPS, k 18, merge depth 3,600), or the
|
||||
// 60x fast-time profile under --fast-time (merge depth 60, finality window 120 DAA, 60-block epochs).
|
||||
const base = FAST_TIME ? JSON.parse(readFileSync(FAST_TIME_FILE, 'utf8')) : {};
|
||||
writeFileSync(file, JSON.stringify({ ...base, skip_proof_of_work: true, ...extra }));
|
||||
return file;
|
||||
}
|
||||
|
||||
// Consensus clock parameters the scenarios scale their durations by: merge depth in DAA seconds (3,600 on the
|
||||
// devnet, 60 under --fast-time). A scenario cut "beyond merge depth" is mergeDepth + 100 s on the devnet and
|
||||
// mergeDepth + 2 s at 60x.
|
||||
export function clockParams() {
|
||||
const o = FAST_TIME ? JSON.parse(readFileSync(FAST_TIME_FILE, 'utf8')) : {};
|
||||
const mergeDepth = o.blockrate?.merge_depth ?? 3600;
|
||||
return { fastTime: FAST_TIME, mergeDepth, scale: 3600 / mergeDepth };
|
||||
}
|
||||
|
||||
export class Node {
|
||||
constructor(index, { connect = [], extraArgs = [], name } = {}) {
|
||||
this.index = index;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
// Runs the in-process simulator (vendor worktree crate igneum/harness-sim) and reads its JSON report.
|
||||
import { spawnSync, spawn } from 'node:child_process';
|
||||
import { readFileSync, mkdirSync, existsSync } from 'node:fs';
|
||||
import { SIM, TMP, log } from './net.mjs';
|
||||
import { SIM, TMP, log, FAST_TIME, FAST_TIME_FILE } from './net.mjs';
|
||||
|
||||
// Under --fast-time the simulator runs the 60x profile too (igneum-harness-sim --override-params-file).
|
||||
const fastArgs = () => (FAST_TIME ? ['--override-params-file', FAST_TIME_FILE] : []);
|
||||
|
||||
export function runSim(name, args, { timeoutMs = 60 * 60_000 } = {}) {
|
||||
if (!existsSync(SIM)) throw new Error(`missing ${SIM}; build igneum-harness-sim in the harness worktree`);
|
||||
mkdirSync(`${TMP}/sim`, { recursive: true });
|
||||
const out = `${TMP}/sim/${name}.json`;
|
||||
const argv = [...args, '--out', out];
|
||||
const argv = [...args, ...fastArgs(), '--out', out];
|
||||
log(`sim ${name}: igneum-harness-sim ${argv.join(' ')}`);
|
||||
const t0 = Date.now();
|
||||
const r = spawnSync('nice', ['-n', '19', SIM, ...argv], { encoding: 'utf8', timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 });
|
||||
|
|
@ -28,7 +31,7 @@ export async function runSims(jobs, { parallel = 3 } = {}) {
|
|||
const { name, args } = jobs[i];
|
||||
mkdirSync(`${TMP}/sim`, { recursive: true });
|
||||
const out = `${TMP}/sim/${name}.json`;
|
||||
const argv = [...args, '--out', out];
|
||||
const argv = [...args, ...fastArgs(), '--out', out];
|
||||
log(`sim ${name}: igneum-harness-sim ${argv.join(' ')}`);
|
||||
const t0 = Date.now();
|
||||
await new Promise((resolve) => {
|
||||
|
|
|
|||
|
|
@ -3,13 +3,16 @@
|
|||
// 127.0.0.1 ports 27200+ and /tmp/igneum-harness, writes a results table per run to docs/bench-log.md, and leaves
|
||||
// the test network stopped. The live devnet (26610/26611, 26640/26641, 28640) and the PC node are never touched.
|
||||
//
|
||||
// node tools/harness/run.mjs [scenario ...] [--quick] [--no-bench-log]
|
||||
// node tools/harness/run.mjs [scenario ...] [--quick] [--no-bench-log] [--fast-time]
|
||||
// scenarios: s5 s2 s1 s3 s6 s4 s7 (default: priority order 5,2,1,3,6,4,7)
|
||||
// --quick runs shorter block counts and durations for a smoke run.
|
||||
// --fast-time runs the network and the simulator on infra/fast-time/override-60x.json (every clock-like consensus
|
||||
// parameter divided by 60: merge depth 60, finality window 120 DAA, 60-block epochs) with the devnet-v4 binaries
|
||||
// from vendor/igneum-node/target-integration; the merge-depth scenarios (3, 4) scale their cuts with it.
|
||||
//
|
||||
// See tools/harness/README.md.
|
||||
|
||||
import { stopAll, assertBinaries } from './lib/net.mjs';
|
||||
import { stopAll, assertBinaries, FAST_TIME, TARGET } from './lib/net.mjs';
|
||||
import { benchLogEntry, appendBenchLog } from './lib/report.mjs';
|
||||
import { stubRows } from './scenarios/stubs.mjs';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
|
@ -30,7 +33,8 @@ function machineLine() {
|
|||
try { cpu = execSync('sysctl -n machdep.cpu.brand_string').toString().trim(); } catch { }
|
||||
try { mem = (parseInt(execSync('sysctl -n hw.memsize').toString().trim(), 10) / 2 ** 30).toFixed(0) + ' GB'; } catch { }
|
||||
let load = ''; try { load = execSync('uptime').toString().match(/load averages?: ([\d. ]+)/)?.[1] || ''; } catch { }
|
||||
return `Machine: ${cpu}, ${mem}, load ${load.trim()}. Private test network of igneumd (release, skip_proof_of_work devnet) on 127.0.0.1 ports 27200+, data /tmp/igneum-harness; the live devnet and the PC node were not touched. Harness: tools/harness/, node fork worktree vendor/igneum-node-harness.`;
|
||||
const profile = FAST_TIME ? 'skip_proof_of_work devnet on the 60x fast-time profile, infra/fast-time/override-60x.json' : 'skip_proof_of_work devnet';
|
||||
return `Machine: ${cpu}, ${mem}, load ${load.trim()}. Private test network of igneumd (release, ${profile}) on 127.0.0.1 ports 27200+, data /tmp/igneum-harness; the live devnet and the PC node were not touched. Harness: tools/harness/, binaries ${TARGET}.`;
|
||||
}
|
||||
|
||||
// Assemble one bench-log entry from result JSONs already written under /tmp/igneum-harness/results, without
|
||||
|
|
@ -82,7 +86,7 @@ async function main() {
|
|||
const allRows = [];
|
||||
for (const key of picks) {
|
||||
if (!SCENARIOS[key]) { console.error(`unknown scenario ${key}`); continue; }
|
||||
console.log(`\n==== scenario ${key}${quick ? ' (quick)' : ''} ====`);
|
||||
console.log(`\n==== scenario ${key}${quick ? ' (quick)' : ''}${FAST_TIME ? ' (fast-time 60x)' : ''} ====`);
|
||||
try {
|
||||
const mod = await SCENARIOS[key]();
|
||||
const { rows } = await mod.run({ quick });
|
||||
|
|
@ -102,7 +106,7 @@ async function main() {
|
|||
const date = new Date().toISOString().slice(0, 10);
|
||||
const entry = benchLogEntry({
|
||||
date: `${date}, consensus attack harness (consensus-engineer)`,
|
||||
title: `catalogue run${quick ? ' (quick)' : ''} on the ordering-layer node`,
|
||||
title: `catalogue run${quick ? ' (quick)' : ''}${FAST_TIME ? ' (fast-time 60x)' : ''} on the ordering-layer node`,
|
||||
machine: machineLine(),
|
||||
rows: allRows,
|
||||
notes: [
|
||||
|
|
|
|||
|
|
@ -8,11 +8,19 @@
|
|||
// Runs in the in-process simulator.
|
||||
|
||||
import { runSims, hist, maxDepth } from '../lib/sim.mjs';
|
||||
import { clockParams } from '../lib/net.mjs';
|
||||
import { saveResult } from '../lib/report.mjs';
|
||||
|
||||
export async function run({ quick = false } = {}) {
|
||||
const cuts = quick ? [120, 600] : [120, 600, 1800, 3700];
|
||||
const jobs = cuts.map(c => ({ name: `s3-cut${c}`, args: ['--scenario', 'partition', '--cut-at', '300', '--cut-secs', String(c), '--run-after', '600', '--sample-secs', '10', '--seed', '3'] }));
|
||||
// Cuts as fractions of the merge depth: 1/30, 1/6, 1/2 and one beyond it (devnet 120, 600, 1,800, 3,700 s). Under
|
||||
// --fast-time (merge depth 60) the same fractions are 2, 10, 30 and 62 s, with the 2-s cut dropped (it equals the
|
||||
// 2-s network delay), and the warm-up and run-after shrink with the clock.
|
||||
const { fastTime, mergeDepth } = clockParams();
|
||||
const fractions = quick ? [1 / 30, 1 / 6] : [1 / 30, 1 / 6, 1 / 2, null];
|
||||
let cuts = fractions.map(f => (f == null ? mergeDepth + (fastTime ? 2 : 100) : Math.round(mergeDepth * f)));
|
||||
if (fastTime) cuts = cuts.filter(c => c > 2);
|
||||
const cutAt = fastTime ? '120' : '300', runAfter = fastTime ? '120' : '600', sample = fastTime ? '2' : '10';
|
||||
const jobs = cuts.map(c => ({ name: `s3-cut${c}`, args: ['--scenario', 'partition', '--cut-at', cutAt, '--cut-secs', String(c), '--run-after', runAfter, '--sample-secs', sample, '--seed', '3'] }));
|
||||
const reports = await runSims(jobs, { parallel: 2 });
|
||||
const rows = []; const data = [];
|
||||
reports.forEach((r, i) => {
|
||||
|
|
@ -26,7 +34,7 @@ export async function run({ quick = false } = {}) {
|
|||
// one-block tip churn at 4 miners and a 2 s delay; the instantaneous sink snapshot (one_chain) is noisier.
|
||||
const pass = r.blue_converged && timeToHeal != null;
|
||||
data.push({ cut_s: c, heal_at_s: healS, converged_blue_at_s: convS, time_to_heal_s: timeToHeal, final_blue_spread: r.final_blue_spread, reorg_depth_at_heal: depths, max_reorg_per_node: r.reorg_hist.map(maxDepth), reorg_hist: r.reorg_hist, rejects: r.rejects, one_chain: r.one_chain, blue_converged: r.blue_converged, final_sinks: r.final_sinks.map(s => s.slice(0, 12)), counts: r.counts, wall_s: r.wall_s });
|
||||
rows.push({ scenario: `3 partition ${c} s${c > 3600 ? ' (beyond merge depth)' : ''}`, criterion: 'one chain after the merge-depth rule; reorg depth and time to heal recorded', result: `one chain: ${r.blue_converged} (blue scores within ${r.final_blue_spread} at the end); healed in ${timeToHeal ?? 'never'} s; losing-side reorg at heal ${Math.max(...depths)} chain blocks (per node ${depths.join('/')}); rejects ${rejects}`, pass });
|
||||
rows.push({ scenario: `3 partition ${c} s${c > mergeDepth ? ' (beyond merge depth)' : ''}${fastTime ? ' [fast-time 60x]' : ''}`, criterion: 'one chain after the merge-depth rule; reorg depth and time to heal recorded', result: `one chain: ${r.blue_converged} (blue scores within ${r.final_blue_spread} at the end); healed in ${timeToHeal ?? 'never'} s; losing-side reorg at heal ${Math.max(...depths)} chain blocks (per node ${depths.join('/')}); rejects ${rejects}`, pass });
|
||||
});
|
||||
saveResult('s3-partition', { rows, data });
|
||||
return { rows, data };
|
||||
|
|
|
|||
|
|
@ -6,11 +6,15 @@
|
|||
// Runs in the in-process simulator.
|
||||
|
||||
import { runSims, maxDepth } from '../lib/sim.mjs';
|
||||
import { clockParams } from '../lib/net.mjs';
|
||||
import { saveResult } from '../lib/report.mjs';
|
||||
|
||||
export async function run({ quick = false } = {}) {
|
||||
const cuts = quick ? [600] : [600, 1800];
|
||||
const jobs = cuts.map(c => ({ name: `s4-eclipse${c}`, args: ['--scenario', 'eclipse', '--cut-at', '60', '--cut-secs', String(c), '--run-after', '600', '--sample-secs', '10', '--seed', '4'] }));
|
||||
// Eclipses of 1/6 and 1/2 of the merge depth (devnet 600 and 1,800 s; 10 and 30 s under --fast-time).
|
||||
const { fastTime, mergeDepth } = clockParams();
|
||||
const cuts = (quick ? [1 / 6] : [1 / 6, 1 / 2]).map(f => Math.round(mergeDepth * f));
|
||||
const runAfter = fastTime ? '120' : '600', sample = fastTime ? '2' : '10';
|
||||
const jobs = cuts.map(c => ({ name: `s4-eclipse${c}`, args: ['--scenario', 'eclipse', '--cut-at', '60', '--cut-secs', String(c), '--run-after', runAfter, '--sample-secs', sample, '--seed', '4'] }));
|
||||
const reports = await runSims(jobs, { parallel: 2 });
|
||||
const rows = []; const data = [];
|
||||
reports.forEach((r, i) => {
|
||||
|
|
@ -26,7 +30,7 @@ export async function run({ quick = false } = {}) {
|
|||
const onHonestSink = lastSample.sinks[3] === lastSample.sinks[0];
|
||||
const pass = victimGap <= 18 && rejoin != null;
|
||||
data.push({ eclipse_s: c, heal_at_s: r.heal_at_s, victim_rejoin_after_s: rejoin, victim_reorg_depth: victimDepth, victim_blue_gap_end: victimGap, victim_on_honest_sink: onHonestSink, reorg_depth_at_heal: r.heal_reorg_depth, victim_max_reorg: maxDepth(r.reorg_hist[3]), counts: r.counts, rejects: r.rejects, final_sinks: r.final_sinks.map(s => s.slice(0, 12)), wall_s: r.wall_s });
|
||||
rows.push({ scenario: `4 eclipse ${c} s`, criterion: 'victim rejoins the honest chain on reconnection within the merge-depth bound; reorg depth recorded', result: `victim rejoined ${rejoin ?? 'never'} s after reconnection (blue-score gap to honest ${victimGap} at the end); victim reorg depth ${victimDepth} chain blocks; adversary built ${r.counts[2].created} blocks that never entered the honest chain`, pass });
|
||||
rows.push({ scenario: `4 eclipse ${c} s${fastTime ? ' [fast-time 60x]' : ''}`, criterion: 'victim rejoins the honest chain on reconnection within the merge-depth bound; reorg depth recorded', result: `victim rejoined ${rejoin ?? 'never'} s after reconnection (blue-score gap to honest ${victimGap} at the end); victim reorg depth ${victimDepth} chain blocks; adversary built ${r.counts[2].created} blocks that never entered the honest chain`, pass });
|
||||
});
|
||||
saveResult('s4-eclipse', { rows, data });
|
||||
return { rows, data };
|
||||
|
|
|
|||
Loading…
Reference in a new issue