P3.4.1 — Signed Time Anchors (STA) — Step 1 Audit

1. Scope

Inventory of the surface this slice must intersect with. Per docs/guides/implementation/task-prompts/p3.1-theta-consensus.md §P3.4.1 (lines 1469–1645), the slice ships:

  • src/domains/consensus/time-anchors.ts
  • src/__tests__/domains/consensus/time-anchors.test.ts

with no edits to existing files. The audit therefore inventories the read-side surface this module imports from and the spec surface it must honor.

2. Spec surface inventory

Source Path Authority for
Source prompt docs/guides/implementation/task-prompts/p3.1-theta-consensus.md §P3.4.1 lines 1469–1645 API shape, defaults, semantics
Consensus spec docs/spec/s06-consensus.md §Signed time anchors (line 37–39) “High-reputation nodes periodically broadcast … median … >30s drift … deprioritized”
Gossip spec docs/spec/s08-gossip.md §Signed Time Anchors (STA) (line 94–96) Field shape (timestamp, epoch, signature); peer-time formula peer_time = STA_median + local_offset
Concept docs/3-world/physics/laws/consensus.md §Signed time anchors (intersection) Cross-domain framing — λ feeds θ
Task breakdown docs/guides/implementation/task-breakdown.md §P3.4.1 Effort sizing M (4–8h)

The s06 + s08 spec language is mutually consistent with the prompt and is non-contradicting with the prompt’s defaults (N=7, K=10, drift_threshold 30 000 ms, replay window 10 epochs). No STOP condition.

3. Code surface inventory (read-only intersections)

3.1 P3.1.1 messages.ts (sibling, R89 Wave 1 — shipped at e63a8bcf)

Path: src/domains/consensus/messages.ts

Re-used helpers — pure functions, no I/O:

Helper Purpose for STA
canonicalize (κ P1.5.4, transitively via canonicalSerialize) Body bytes for signing
rewriteBuffersToHex Already encapsulated inside canonicalSerializeUnchecked — Buffer → hex pre-pass before κ canonicalize
Ed25519 PURE-mode (RFC 8032) via node:crypto.sign(null, body, privKey) and verify(null, body, pubKey, sig) Used by P3.4.1’s signAnchor / verifyAnchor
ConsensusSerializationError Re-exported error class — STA reuses it on canonical failures

P3.4.1 does not import any signed/unsigned Vote, Commit, Reveal, ViewChange, or EquivocationProof shape. STA is a NEW leaf message, not an envelope variant of ConsensusMessage. Per the source prompt, STA is a standalone {publisher, timestamp_ms, epoch, signature} 4-tuple.

P3.4.1 needs its own canonical-serialize helper because:

  • κ canonicalize requires plain objects.
  • The signing path strips signature before encoding (same pattern as stripSignatureForSig in messages.ts).
  • A separate path documents that STA does not flow through ConsensusMessage’s msg_type discriminator.

The clean approach: implement an STA-local canonical helper in time-anchors.ts that delegates to the same κ canonicalize after a hand-rolled Buffer → hex pre-pass. This mirrors P3.1.1’s pattern without exposing internal helpers across module boundaries.

3.2 P2.1.1 reputation/schema.ts (λ Phase 2 — shipped at #226)

Path: src/domains/reputation/schema.ts

Re-used reader:

export function selectReputation(
  db: Database.Database,
  node_id: string,
): ReputationRow[];                       // overload — returns all domains for a node
export function selectReputation(
  db: Database.Database,
  node_id: string,
  domain: Domain,
): ReputationRow | null;                  // overload — single (node, domain)

The “eligible publishers” criterion in §P3.4.1 reads:

top N arbiters by λ.reputation.arbitration via selectReputation(domain=”arbitration”) ORDER BY score DESC LIMIT N

This is a per-node read in the public API of schema.ts. To get a top-N leaderboard, callers can either:

  1. Pass an opaque snapshot map arbiter_id → arbitration_score constructed upstream (caller’s responsibility — production wires it to idx_reputations_leaderboard directly, test path passes a fixture Map).
  2. Use a new helper that queries the leaderboard index directly.

The source prompt’s “Ready-to-paste agent prompt” pins option 1 (line 1543-1547):

isEligiblePublisher(publisher_id, reputationSnapshot: ReadonlyMap<string, bigint>, top_n: bigint = 7n)

The “library-level selectReputation” wording in the dispatch packet matches option 1 — selectReputation is the public reader; an STA caller constructs the snapshot Map by walking arbiters they know about (or by a leaderboard query that lives outside this module’s surface). Library-level means “this module reads at the schema-level via selectReputation-derived snapshot, not via an MCP roundtrip” — it does NOT mean “this module embeds a SELECT statement”.

Decision (recorded in contract §3): P3.4.1 takes a snapshot Map as input, keeping time-anchors.ts pure (no DB handle). A separate convenience helper that internally calls selectReputation for every arbiter in a known set is over-engineering for Wave 2; the test corpus exercises it with a fixture Map directly. This keeps the slice fully pure and unit testable without a SQLite handle.

Index reference: idx_reputations_leaderboard ON reputations(domain, score DESC) exists at src/db/migrations/007_reputation.sql:58. Future production callers will use it; this Wave 2 slice does not.

3.3 BPS constants (κ Phase 1)

Path: src/domains/rules/bps-constants.ts

Not directly imported. STA’s reputation scores are read as opaque bigints from the snapshot Map — bps-bound enforcement is λ’s responsibility, already validated at insert time by ReputationRowSchema.

4. Existing tests touching consensus domain

Path: src/__tests__/domains/consensus/

File Purpose Intersect
messages.test.ts P3.1.1 vote-message coverage Adjacent — no symbol overlap

No other consensus tests exist. The P3.4.1 test file is a new file at src/__tests__/domains/consensus/time-anchors.test.ts. No existing suites need updating.

Forbidden-token scanner in messages.test.ts walks only messages.ts — it does NOT need to be extended for time-anchors.ts. P3.4.1 ships its own narrow scanner (per the κ-style discipline P3.1.1 established).

5. Conventions inventory (must conform to)

Convention Source Applied to STA
All quantities as bigint docs/architecture/decisions/ADR-008-bigint-arithmetic.md timestamp_ms, epoch, defaults
No wall-clock reads for signing P3.1.1 module docstring §5 / s06 §Signed time anchors final clause Tests use seeded clock; signing accepts an injected timestamp_ms
No Math.* / Math.random κ determinism § Median uses bigint arithmetic; no Math.floor
No floats κ § All math is bigint
Buffer → hex in canonical body messages.ts §6 Same pattern repeated
Ed25519 PURE-mode (sign(null, body, priv)) messages.ts §8 Same pattern repeated
Named-import crypto (import { sign, verify } from 'node:crypto') messages.ts §2 / messages.test.ts §Group 9 Same; STA scanner enforces
Error class hangs cause chain messages.ts §3 STA reuses ConsensusSerializationError
Defaulted parameters for π-governable constants source prompt §Common gotchas (line 1645) top_n = 7n, k_epochs = 10n, threshold_ms = 30000n, replay_window = 10n

6. Risks identified

Risk Mitigation
Even-count median floor — (a+b)/2 could surprise on bigint division bigint division IS already floor in TS — (1000n + 1010n) / 2n === 1005n. Explicit test case for even count.
Single-arbiter edge — top_N filter must accept the single publisher trivially Test fixture: 1-element Map; isEligible returns true; median = sole anchor’s timestamp
Monotonicity false positives — same epoch, identical timestamp by same publisher Spec wording is epoch_{n+1} > epoch_n AND timestamp_{n+1} >= timestamp_n — strict-> on epoch, non-decreasing on timestamp. Same epoch is rejected as a non-advance, not a violation. The function flags only the violation case (strict-> on epoch but ts decreasing).
Replay protection wording — “epoch < current_epoch - 10” vs. “epoch < current_epoch - 10n” — must be bigint subtraction Use current_epoch - 10n literal in code
Drift “deprioritized” semantics — return must be advisory; no reject Return type is "ok" \| "deprioritized" literal union, NEVER a throw or null
sort stability for top-N — JS Array.sort is stable on Node ≥ 12 Documented in code comment
ConsensusSerializationError import — re-use vs. re-export Re-use via direct import; no re-export at this slice

Source-prompt acceptance criteria (lines 1490–1500) crosswalk to contract invariants (Step 2):

AC Invariant ID Realized by
STA shape {publisher, timestamp_ms, epoch, signature} I1 STA interface in time-anchors.ts
Eligible publishers — top N by reputation.arbitration I2 eligiblePublishers(snapshot, n)
Anchor signature — Ed25519 over canonical (publisher, timestamp_ms, epoch) I3 signAnchor / verifyAnchor
Median computation — last K epochs, equal weight per publisher I4 computeMedian(anchors, k_epochs, current_epoch)
Drift detection — |local − median| > 30s → “deprioritized” (advisory, no reject) I5 detectDrift(local, median, threshold_ms)
Monotonicity per publisher — epoch_{n+1} > epoch_n AND ts_{n+1} >= ts_n I6 validateMonotonicity(anchors_per_publisher)
Replay protection — epoch < current_epoch - 10 rejected I7 rejectReplay(anchor, current_epoch)
Even-count median — floor of (a+b)/2 I4 (sub-clause) bigint (a + b) / 2n
Tests use seeded clock I8 Test posture

8. File-level outcome

Files to be created: 2 (matches source prompt §Files to create)

File Lines (estimate)
src/domains/consensus/time-anchors.ts ~400
src/__tests__/domains/consensus/time-anchors.test.ts ~450

Files to be modified: 0

Existing public surfaces touched: 0 (no exports renamed or removed)

9. Greenlight

Audit signoff: every spec source agrees on shape, defaults, and semantics. λ Phase 2 dependency is shipped at #226. P3.1.1 patterns are clear and re-usable. No STOP condition. Proceed to Step 2 (contract).


Back to top

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

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