Skip to content

V-L2-L2: UNIQUE INDEX(entity_id, previous_hash) makes forks structurally impossible #32

Closed as not planned

Description

@hyperpolymath

Context

V-L2-L1 prevents forks at the application layer. Defence in depth:
make forks structurally impossible by adding a DB-level UNIQUE
constraint on (entity_id, previous_hash). A second writer that
somehow sneaks past the chain_head lock still cannot insert because
the unique constraint rejects the second row with the same predecessor.

What to do

In src/codegen/overlay.rs::generate_provenance_table, add:

CREATE UNIQUE INDEX IF NOT EXISTS ux_provenance_chain
  ON verisimdb_provenance_log(entity_id, previous_hash);

(Genesis records all have previous_hash = ''; one genesis per
entity is exactly what we want, so the unique index correctly
forbids duplicate geneses too.)

In tests, assert that the generated DDL contains the unique
index, and that an attempted duplicate insert fails with a
constraint violation.

Acceptance

  • Generated DDL contains the UNIQUE INDEX
  • DDL test asserts the index is present
  • Integration test: with V-L2-L1 in place, attempt two parallel
    appends; one succeeds, the other gets a constraint-violation
    error and retries against the new head (graceful)
  • Documented in README §"Provenance Tracking" + the threat
    model doc

Activity

  1. hyperpolymath commented on May 16, 2026

    @hyperpolymath
    OwnerAuthor

    Declining the UNIQUE INDEX — it makes legitimate forks impossible (draft PR #104)

    Investigating this alongside #31 surfaced a defect. The proposed
    CREATE UNIQUE INDEX ux_provenance_chain ON verisimdb_provenance_log(entity_id, previous_hash) does not detect
    a fork — it rejects the second row at insert time, discarding
    honest history. Network-partitioned/replicated honest writers and
    simulation branches (ADR-0006) are legitimate divergence; a fork that
    cannot be written cannot be detected or audited. That inverts the
    integrity goal (tamper-evidence + no silent loss, not linearity).

    Decision (full design: docs/decisions/0010-provenance-forks-are-first-class.adoc)

    • Do NOT add UNIQUE INDEX(entity_id, previous_hash). The
      duplicate guard already exists: hash is PRIMARY KEY, and the
      preimage is domain-tagged and covers every field (ADR-0002 / V-L2-C1: hash full audit (actor + before_snapshot + transformation) with domain separation #27),
      so an exact-duplicate row necessarily collides on hash and is
      rejected — while two different entries that share a predecessor
      (a real fork) are correctly allowed.
    • Add a non-unique idx_provenance_predecessor(entity_id, previous_hash) so fork detection (fork_points,
      GROUP BY … HAVING COUNT(*) > 1) is O(log n).
    • "Single writer per entity" becomes an opt-in policy (reject on

      1 head) in application code, not a schema-welded invariant.

    Migration note specific to this issue: because the unique index
    is never created, an existing sidecar that already legitimately
    contains a fork cannot fail to open. Had #32 shipped first, the
    migration would have had to detect and quarantine such rows — a
    hazard this plan avoids by declining the index.

    The retained duplicate-rejection guarantee (the thing #32 actually
    wanted) is covered by exact_duplicate_entry_is_rejected in the test
    plan; the failing-by-design test in the draft PR proves a legitimate
    fork can be written and detected.

    Draft PR (design + failing test, not for merge): #104

  2. added 2 commits that reference this issue on May 16, 2026
  3. hyperpolymath commented on May 17, 2026

    @hyperpolymath
    OwnerAuthor

    Closing as superseded by ADR-0010 (docs/decisions/0010-provenance-forks-are-first-class.adoc), per that ADR's explicit instruction to note this here.

    This issue proposes CREATE UNIQUE INDEX ux_provenance_chain ON verisimdb_provenance_log(entity_id, previous_hash). ADR-0010 rejects that change by design: a unique index does not detect a fork, it rejects the second writer's row at insert time — discarding legitimate divergent history (partitioned/replicated/offline writers, simulation branches). "A fork that cannot be written cannot be detected or audited."

    The correct duplicate guard is the existing hash PRIMARY KEY (domain-tagged preimage over all tamper-relevant fields ⇒ exact duplicates already collide). #32's only valid residue — a non-unique idx_provenance_predecessor for O(log n) fork detection — is folded into the #31 / ADR-0010 implementation. No UNIQUE INDEX(entity_id, previous_hash) should ever be created (also avoids a destructive migration hazard for sidecars that already legitimately contain a fork). Superseded — not implementing as written.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew capability or improvement to existing behaviour

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions