Aller au contenu principal

R1 — Atomic Chain Writer

R1 is the substrate that holds the rest of the framework up. Every audit row written to a state machine's entangled state goes through this writer. Its job is to guarantee that no two writers can ever produce an entangled state break under concurrency, even when many processes are racing to append the next row.

Why it exists

The entangled state is a singly-linked hash list:

rowHash(N) = sha256(`${zeqondNumber}|${stateHash}|${prevHash}`)
prev_hash[N+1] = rowHash(N)

Without serialisation, two writers can both read the same head, both compute prev_hash against it, and both insert a row claiming the same logical timestamp. The entangled state branches. Every downstream verifier disagrees.

The previous implementation cached the entangled state head in a JavaScript Map<originId, string> per process. Two writers reading the cache concurrently both saw the same head, both wrote with the same prev_hash, and the entangled state silently diverged. The legacy break at row 15053 on zeq07792026349 was the visible symptom.

What R1 ships

Postgres advisory lock per origin. Every append acquires pg_advisory_xact_lock(hashtext(origin_id)) at the start of the transaction. Concurrent writers on the same entangled state block at this line until the prior transaction commits.

await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext(${args.originId}))`);

SELECT-latest inside the same transaction. Once the lock is held, the writer reads the current head row from the database — never a JS cache.

const [last] = await tx
.select({ zeqondNumber, stateHash, prevHash })
.from(auditLogTable)
.where(eq(auditLogTable.originId, args.originId))
.orderBy(desc(auditLogTable.zeqondNumber))
.limit(1);

Bigint conversion. node-postgres returns bigint columns as JS strings by default. The writer explicitly converts before any arithmetic:

const lastZ: bigint | null = last ? BigInt(String(last.zeqondNumber)) : null;

Without this, lastZ + 1n against a string throws Cannot mix BigInt and other types silently inside the catch — the retry hits the same error, and the per-machine ticker swallows the rejection.

UNIQUE INDEX defence in depth. Even if every other guard fails, the database refuses to accept a duplicate logical timestamp on the same entangled state:

CREATE UNIQUE INDEX audit_log_origin_zeqond_uq
ON audit_log (origin_id, zeqond_number);

The writer also includes an optimistic-zeqond-bump retry: if the proposed zeqond is already taken, bump to last.zeqond + 1 and retry up to five times. With the advisory lock in place this branch is cold but kept as defence-in-depth.

The JS heads cache is gone. Every write reads the canonical head from the database inside the transaction. Postgres is the source of truth.

Stress verification

Two stress rounds against a live chain (zeq07792026349) post-deploy:

RoundConcurrent writesOrganic ticksChain state
1815HEALTHY (0 broken links)
22015HEALTHY (0 broken links)

79 rows added across both rounds, zero cannot mix BigInt errors in the writer log, chain-repair.mjs reports chain is healthy — no rows updated after each round.

File map

  • shared/api-core/src/lib/auditLog.ts — atomic writer
  • shared/api-core/migrations/20260428_chain_invariant.sqlUNIQUE INDEX
  • apps/zeq-dev/scripts/chain-repair.mjs — forward-relink repair tool

What this enables

R1 is the load-bearing piece for everything above it:

  • R2 awareness — validates the trailing 1024-row window after each tick; only meaningful if the writer guarantees no concurrent corruption.
  • R3 phase-locked cipher — devices encrypt payloads keyed on (zeqond, originId); nonce reuse is impossible because the UNIQUE INDEX prevents a second row from sharing the same zeqond on the same origin.
  • HZC archiver — continuity proofs assume the entangled state has no internal branches.
  • ZeqCompliance v1 — every envelope's zeqond ties to exactly one row.

Try it

Spin up an entangled state, post events concurrently, and inspect:

# 8 concurrent prove writes
for i in $(seq 1 8); do
curl -s -X POST https://zeqapi.com/api/chain/<your-slug>/event \
-H "Authorization: Bearer zsm_..." \
-H "Content-Type: application/json" \
-d "{\"transitionId\":\"stress-$i\",\"transitionType\":\"compute\",\"stateHash\":\"0x$(openssl rand -hex 32)\"}" &
done
wait

# Validate the chain
node --env-file=.env scripts/chain-repair.mjs <your-slug>
# → "chain is healthy — no rows updated"