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), two runScenario calls return reports whose canonical-bytes (with determinism_check field 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, then crypto.createPrivateKey({key, format: 'raw', type: 'private', oid: 'Ed25519'}) (Node ≥ 20 syntax).
  • The Lamport clock (P3.1.1) is reset at the top of each runScenario via resetLogicalForTesting() — 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:

  1. First submission: applied: trueslashings_applied += 1n.
  2. Second submission (same proof, same alreadyApplied set): applied: false, reason: 'duplicate'slashings_applied unchanged.

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 0xab12 is exactly 3 (= quorum threshold for n=4).
  • The minority count on 0xCAFE is exactly 1.
  • FinalitySM reaches QUORUM after 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:

  1. Unknown scenario_id in input (defensive — TS prevents this).
  2. Mismatched n and arbiters.length.
  3. Empty scenario (zero rounds).
  4. 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 ParityReport rows (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

Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.