Contract — P3.8.1 Parity Harness (R89 θ Wave 5)
§1. Purpose
The multi-arbiter simulation harness. Drives a 4-scenario default
corpus through the shipped θ surface and emits a deterministic
ParityReport. Closes the Phase 3 verification loop.
§2. Public surface
§2.1 Module src/domains/consensus/default-corpus.ts
export type ArbiterId = string;
/** A simulated arbiter's vote contribution in a scenario round. */
export interface HonestVote {
readonly kind: 'honest';
/** 32-byte merkle_root the arbiter signs. */
readonly merkle_root_tag: string;
}
/** A Byzantine arbiter — same role as honest but signs a divergent root. */
export interface ByzantineVote {
readonly kind: 'byzantine';
readonly merkle_root_tag: string;
}
/** An equivocator — signs two distinct merkle_root tags in the same round. */
export interface EquivocatorVote {
readonly kind: 'equivocator';
readonly merkle_root_tag_a: string;
readonly merkle_root_tag_b: string;
}
export type ScenarioVote = HonestVote | ByzantineVote | EquivocatorVote;
export interface ScenarioRound {
/** Spec proposal root for the round (informational; matches majority). */
readonly proposal_root_tag: string;
/** Per-arbiter vote contributions. */
readonly votes: ReadonlyMap<ArbiterId, ScenarioVote>;
}
export interface Scenario {
readonly id: string;
readonly n: bigint;
readonly arbiters: readonly ArbiterId[];
readonly rounds: readonly ScenarioRound[];
}
export const SCENARIO_1: Scenario; // n=1 single-arbiter happy path
export const SCENARIO_2: Scenario; // n=4 all honest
export const SCENARIO_3: Scenario; // n=4 Byzantine (consensus.md §Worked example verbatim)
export const SCENARIO_4: Scenario; // n=4 equivocator
export const DEFAULT_CORPUS: readonly Scenario[];
The 4 scenarios are frozen (Object.freeze applied to each
scenario, each round, each vote map, and the corpus array). Tests
assert this.
§2.2 Module src/domains/consensus/parity-harness.ts
import type { FinalityLevel } from './finality.js';
import type { EquivocationProof } from './messages.js';
import type { Scenario } from './default-corpus.js';
export interface ParityReport {
readonly scenario_id: string;
readonly n: bigint;
readonly rounds_executed: bigint;
readonly finality_reached: FinalityLevel;
readonly equivocation_proofs: readonly EquivocationProof[];
readonly slashings_applied: bigint;
readonly determinism_check: {
readonly seed: bigint;
readonly second_run_identical: boolean;
};
}
/**
* Run one scenario under a deterministic seed. Returns a frozen
* ParityReport. The harness internally runs the scenario twice to
* compute determinism_check.
*
* Pure — no I/O, no clock, no RNG.
*/
export function runScenario(scenario: Scenario, seed: bigint): ParityReport;
/**
* Run every scenario in DEFAULT_CORPUS. Default seed `42n` matches the
* pattern source (κ P1.5.5).
*/
export function runDefaultCorpus(seed?: bigint): readonly ParityReport[];
/**
* Stable canonical bytes of a report for byte-identity comparison.
* Used internally by runScenario for the determinism check, exported
* so test code can compare reports without re-implementing the
* encoder.
*/
export function reportCanonicalBytes(report: ParityReport): string;
export class ParityHarnessError extends Error {}
§3. Behavioral invariants
§3.1 Determinism
- For any
(Scenario, seed), tworunScenariocalls return reports whose canonical-bytes (withdeterminism_checkfield stripped) are byte-identical. - The harness body uses no clock, no RNG, no Math.*, no Date.*. A forbidden-token corpus self-scan in the test file enforces this.
- The Ed25519 keypairs used per arbiter are derived deterministically
from
(seed, scenario_id, arbiter_id)via HMAC-SHA512 to produce a 32-byte raw secret seed, thencrypto.createPrivateKey({key, format: 'raw', type: 'private', oid: 'Ed25519'})(Node ≥ 20 syntax). - The Lamport clock (P3.1.1) is reset at the top of each
runScenarioviaresetLogicalForTesting()— scenarios do not contaminate each other.
§3.2 Per-scenario pass conditions
| Scenario | n | Expected finality | Expected slashings |
|---|---|---|---|
| SCENARIO_1 | 1 | QUORUM | 0 |
| SCENARIO_2 | 4 | QUORUM | 0 |
| SCENARIO_3 | 4 | QUORUM | 0 (D is Byzantine, not equivocating — no proof) |
| SCENARIO_4 | 4 | QUORUM | 1 (D double-signs; slasher fires once) |
§3.3 Idempotent slashing (SC4)
After SC4’s single round produces an EquivocationProof for D, the
harness submits the proof to applyEquivocationSlash:
- First submission:
applied: true→slashings_applied += 1n. - Second submission (same proof, same
alreadyAppliedset):applied: false, reason: 'duplicate'→slashings_appliedunchanged.
The test asserts slashings_applied === 1n after both submissions.
§3.4 Scenario 3 is the worked example verbatim
The harness asserts (in tests):
- SC3’s
proposal_root_tag === '0xab12'. - SC3 votes are
{A: 0xab12, B: 0xab12, C: 0xab12, D: 0xCAFE}. - The majority count on
0xab12is exactly 3 (= quorum threshold for n=4). - The minority count on
0xCAFEis exactly 1. FinalitySMreachesQUORUMafter the third matching vote.
§3.5 Performance
The test driver runs runDefaultCorpus(42n) 2500 times in sequence
(10000 / 4 = 2500 iterations × 4 scenarios = 10k synthetic events) and
asserts the wall-clock elapsed time (measured via Date.now in
the test file only) is < 5000 ms.
§3.6 Frozen output
Returned ParityReport objects are Object.freezed, as are the
equivocation_proofs array and the determinism_check sub-object.
§4. Error semantics
ParityHarnessError is thrown on:
- Unknown scenario_id in input (defensive — TS prevents this).
- Mismatched
nandarbiters.length. - Empty scenario (zero rounds).
- Vote map referencing an arbiter not in
arbiters.
Errors are thrown synchronously. Never via Promise.reject. The
harness is fully synchronous.
§5. The merkle_root_tag encoding
A merkle_root_tag is a short hex prefix string like '0xab12'. The
harness encodes it into a 32-byte Buffer as follows:
buf = Buffer.alloc(32, 0);
hex = tag.startsWith('0x') ? tag.slice(2) : tag;
Buffer.from(hex, 'hex').copy(buf, 0);
So '0xab12' becomes Buffer.from([0xab, 0x12, 0x00, ..., 0x00]).
This matches the spec’s “short-prefix” convention while keeping the
full 32-byte width Buffer required by every signed-vote field.
The rule_version_hash is a constant Buffer.alloc(32, 1) (all-ones
prefix) shared by all scenarios — the parity-harness does not exercise
rule-version drift; that is P1.5.5’s job.
§6. Test surface (locked count)
src/__tests__/domains/consensus/parity-harness.test.ts ships ≥ 30
test cases organized into 8 groups:
| Group | Coverage |
|---|---|
| G1 | Default corpus shape (length, freeze, ids unique) |
| G2 | SCENARIO_1 — n=1 happy path |
| G3 | SCENARIO_2 — n=4 all honest |
| G4 | SCENARIO_3 — n=4 Byzantine (worked-example match) |
| G5 | SCENARIO_4 — n=4 equivocator + idempotent slash |
| G6 | Determinism — same seed ⇒ identical reports |
| G7 | Performance — 10k events × 4 scenarios < 5s |
| G8 | Static scanner — no forbidden tokens in harness body |
§7. Out-of-scope (rejected change requests)
- Cross-scenario aggregate reports (deferred).
- Persisted
ParityReportrows (out of P3.8.1). - Threshold signature aggregation (BLS/MuSig2 — Phase 4).
- HARD or ABSOLUTE level scenarios (Phase 4 epoch sealing).
- Network jitter / packet drop simulation (gossip-layer; out of θ parity).
§8. References
- Step 1 audit:
docs/audits/p3-8-1-parity-harness-audit.md - Spec:
docs/3-world/physics/laws/consensus.md§Worked example - κ pattern:
src/domains/rules/parity-harness.ts,src/__tests__/domains/rules/parity-harness.test.ts - Source prompt:
docs/guides/implementation/task-prompts/p3.1-theta-consensus.md§P3.8.1