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>
77 lines
3 KiB
JavaScript
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,
|
|
};
|
|
}
|
|
}
|