deviant/scripts/asm-sequencer/lib/state.mjs
Local Dev b8b110dc97 feat(asm): tier-2 sequencer service drafted + chipnet-proved 5/5
Adds the coordinator-funded HTTP relay from
Parameters/ASM_Native/ASM-single-asset-throughput.md §2a — the
low-latency path that complements the tier-4 wallet retry (0272/0275/
0277) by turning N simultaneous claim requests into an ordered chain
from ONE reservoir. Trust surface: the covenant enforces the swap
math either way; the relay can only order and rebroadcast.

scripts/asm-sequencer/
  server.mjs   — Node HTTP daemon: GET /health, POST /claim, POST /admin/reload-tip
  client.mjs   — reference client (portable to wallet + dApp)
  lib/state.mjs — SequencerState + async mutex over the chain

scripts/chipnet-asm-sequencer-daemon-test.mjs
  spawns daemon, fires 5 parallel /claim requests, verifies txid chain

Chipnet result: 5 wire arrivals within 3 ms produce 5/5 accepted in
daemon-assigned order; each tx's `before.reservoir` matches the
previous tx's txid. Reservoir advanced 5 times from one starting
outpoint, zero collisions. Baseline naïve parallel is 1/5.

Snapshot 0278 documents the trust model, gotchas
(chipnet wallet BCH depletion, cashaddr → tokenaddr conversion), and
the roadmap: wallet-side client wire, then SIGHASH_ANYONECANPAY
user-funded variant, then multi-category.

Wiki handoff (wiki/asm/zero-contention-protocol.md) updated with the
tier-2 status.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-28 01:47:40 +02:00

77 lines
3 KiB
JavaScript

// In-memory sequencer state — tracks the local reservoir tip and the
// coordinator's BCH tip across a chain of in-flight claims.
//
// Each successful claim advances BOTH tips locally, so the next tx can be
// built and broadcast without waiting for confirmation. This is what turns
// simultaneous arrivals into an ordered chain (5/5 on chipnet, doc §2a).
//
// One `SequencerState` instance per category. The daemon holds a Map of
// them, keyed by category id. A single top-level mutex serialises the
// build+broadcast so two concurrent /claim calls don't fork the chain.
export class SequencerState {
/**
* @param {object} tok chipnet-asm token JSON — category, rate, feeBps, contract addr, founderLb…
* @param {object} coordUtxo starting coordinator BCH UTXO {txid, vout, satoshis}
* @param {object} reservoir starting reservoir UTXO {txid, vout, satoshis, token: {amount, category, nft: {commitment}}}
*/
constructor(tok, coordUtxo, reservoir) {
this.tok = tok;
this.coordUtxo = coordUtxo;
this.reservoir = reservoir;
this.chainDepth = 0; // how many in-flight (unconfirmed) txs deep we are
this.chainStartedAt = Date.now(); // reset when a block confirms
this.totalServed = 0; // lifetime accepted claims
this.lastServedAt = null;
this.lastError = null;
this._mutex = Promise.resolve(); // serialises .withLock()
}
/** Run `fn` under the state mutex — the ONLY correct way to advance the
* chain. Two concurrent /claim requests will queue behind each other. */
withLock(fn) {
const next = this._mutex.then(() => fn());
this._mutex = next.catch(() => {}); // don't poison the chain on error
return next;
}
/** Called AFTER a successful broadcast. Advances the in-memory tips so
* the next tx spends this one's outputs — the sequencer chain. */
advance({ newReservoir, newCoordUtxo }) {
this.reservoir = newReservoir;
this.coordUtxo = newCoordUtxo;
this.chainDepth += 1;
this.totalServed += 1;
this.lastServedAt = Date.now();
this.lastError = null;
}
/** Called AFTER a broadcast fails. Chain state is intact; caller should
* either re-fetch from the node (if the whole tip is stale) or return the
* error to the client without advancing. */
markError(err) {
this.lastError = err instanceof Error ? err.message : String(err);
}
/** Snapshot for /health responses — no mutex needed for reads. */
snapshot() {
return {
category: this.tok.categoryId,
ticker: this.tok.name,
reservoir: {
txid: this.reservoir.txid,
vout: this.reservoir.vout,
ftAmount: String(this.reservoir.token.amount),
},
coordinator: {
txid: this.coordUtxo.txid,
vout: this.coordUtxo.vout,
satoshis: String(this.coordUtxo.satoshis),
},
chainDepth: this.chainDepth,
totalServed: this.totalServed,
lastServedAt: this.lastServedAt,
lastError: this.lastError,
};
}
}