deviant/Parameters/ASM_Native/ASM-single-asset-throughput.md
Local Dev 6130c0cab1 feat(asm): Potidaea swap-relay + tally refresh to 52,998
Prove the Potidaea platform-fee revenue model live on chipnet with two
end-to-end swap-relay tests, then sweep the cumulative chipnet-swap tally
from "17,000+" to the exact figure 52,998 across every referencing doc.

Snapshot 0239 (four revisions) captures the full arc — GMX Potidaea
(9,296 swaps · 5.47 M platform bits) → OpenMint Potidaea (26,599 swaps ·
15.02 M platform bits · 30.03 M founder bits · 44 swaps/sec peak · 2:1
founder/platform ratio matching ADR-0053 spec exactly). Client-added
platform-fee output at tx.outputs[3] — no covenant change required.

Ships the multi-source connectivity layer used throughout:
scripts/lib/chipnet-multisource.mjs (race local Fulcrum + BCHN RPC +
public electrums, chipnet-only endpoint list, wrong-network tip-guard,
belt-and-braces broadcast) and scripts/lib/chipnet-keystore.mjs (single
Keys/chipnet-keystore.json for all wallets, legacy per-file fallback).
Keys/ added to .gitignore.

Also: GMX/Game.X zero-bet-fee policy — chipnet-gmx-game-loop-test.mjs
now returns 0n from escrowFee() by default (feeAmount=0 on the escrow
covenant; already covered by the "if feeAmount > 0" gate at
GamePoolEscrowFlat.cash:54, no covenant change needed). Old formula
kept behind ARBITER_FEE_LEGACY=1 env flag.

Canonical tally table lives in
Parameters/ASM_Native/ASM-single-asset-throughput.md §4a; update it
first when the next throughput runs land, then grep-replace the exact
number across dependent files.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-14 01:39:41 +02:00

238 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Single-asset throughput — sequencer + sharded reservoir + auto-assign
**Companion to [ASM-no-shared-state-contention.md](ASM-no-shared-state-contention.md).** That doc
shows the **self-backed** model (mint-your-own / redeem-your-own) has *no* shared state, so it has
no contention and no ceiling — but each holder mints a **distinct category** (a per-holder instance
of the asset class). This doc covers the other design choice: when you want **one fungible backed
asset — one category — that everyone holds and swaps**, which requires a shared reservoir. That
reintroduces a shared coin. Here is how to make that model scale to full block capacity **without
collision**, with measured chipnet numbers.
> Terminology: a **backed asset (MBA)** is never a "token"; the raw chain primitive is a CashToken
> FT. Amounts are in **bits** (1 bit = the smallest BCH unit).
---
## 1. The problem, recapped
A single fungible backed asset holds its whole supply in **one reservoir coin** (one UTXO). To serve
a swap you spend that coin and recreate it decremented. So **simultaneous, independent** claims all
try to spend the *same* outpoint → double-spends.
**Chipnet, 6 addresses claiming one reservoir at once:** all fetched the same outpoint `496b2ae5…:0`;
**1 confirmed, 5 rejected** `txn-mempool-conflict (18)`. Supply is irrelevant — a reservoir with
10¹⁸ units is still one coin, spendable by one tx at a time.
The fix is **not** more supply. It is (a) don't make independent clients race the same coin — put
them in a **queue** that chains; and (b) provide **more than one coin** — **shard** the reservoir.
## 2. The mechanism — three layers, each measured on chipnet
### 2a. Sequencer / queue (chaining) — turns simultaneous into ordered
Instead of N clients racing the confirmed reservoir, a sequencer processes requests **in order**,
building each on the coin the previous one created (`state 8 → 9 → 10 …`). Submission is parallel;
settlement is an ordered chain.
**Measured: 5/5.** Five requests "arrive at once" (C, E, F, G, B); the sequencer chains them —
`C → E → F → G → B`, each spending the reservoir coin the previous created. **One category, one
reservoir, zero collisions.** (Naïve parallel on the same setup is 1/5.)
### 2b. Chain depth — how many can be in-flight at once
A reservoir chain has a limit on **unconfirmed** length; a block then confirms the whole chain and
it resets.
**Measured: 250 chained claims accepted on one reservoir with zero rejection** (I stopped at 250;
unconfirmed ancestors reached 249). BCH's mempool policy is far more permissive than BTC's old
50-ancestor limit — so a single reservoir carries **hundreds in-flight between blocks**.
### 2c. Sharded reservoir + auto-assign — parallel lanes for one category
Split one category's supply across **K reservoir UTXOs** (K minting-NFT shards) at the same contract
address — still **one categoryId, one listing, no user choice**. The `claim()` covenant is per-UTXO,
so each shard is independently claimable. An **auto-assigner** hands each incoming request the next
**free** shard; up to K settle **truly in parallel** (different coins + own funding → nothing shared).
**Measured: 6/6 across 3 shards.** Round 1 — C/E/F on shards 0/1/2 broadcast **in parallel**; round 2
— G/B/D **chained** onto the new shard coins. **Zero collisions.**
### 2d. Client-side optimistic chaining + retry — the trustless fallback
With no sequencer, each wallet fetches the latest reservoir (including mempool) and chains onto it,
retrying on conflict (re-fetch the new tip, rebuild). Fully decentralized; a few retries per user
under burst. Runs alongside the sequencer as the no-coordinator backstop.
## 3. Continuous throughput — one asset, all lanes, round-trip
Genesis one category into 7 shards; 7 addresses each run a lane doing **round-trip** swaps — batches
of 5 claims (BCH→MBA) then 1 redeem-all (MBA→BCH) that consolidates the recovered BCH back into the
funding coin. So each swap costs only founder-fee + miner (a few thousand bits); the backing (gross)
is recovered every cycle and **the wallet does not drain.**
Chipnet, 7 lanes, 180-second budget:
```
lane claims redeems BCH start → end (bits) status
A/B/C ~53 11 67.9M→67.7M · 22.7M→22.5M still running (hit time budget)
D/E/F/G ~53 11 ~1.01M → ~0.81M still running (hit time budget)
──────────────────────────────────────────────────────────────────
449 swaps (372 BCH→MBA + 77 MBA→BCH) · ONE category · 7 shards · 0 collisions · 183 s
```
**No exhaustion:** every lane lost only **~200,000 bits (pure fees)** over ~64 swaps — *including* the
lanes that started with barely 1M bits, which were still going at the cutoff. Round-trips recover the
backing; only the fee + miner drains. Nothing stopped except the clock.
### Local node — the real number
The public-electrum rate above (~2.5/s) was pure client/network latency. Building each tx **locally**
and broadcasting straight to the **local BCHN node** (`sendrawtransaction`, no WSS round-trip):
```
one category · 7 shards · round-trip · 30-second budget · broadcast to local node
A/B/C ~1,400 claims + ~280 redeems each (still running at cutoff)
D/E/F/G ~170 claims + ~35 redeems each (low-balance test wallets hit the batch floor)
──────────────────────────────────────────────────────────────────────────
5,892 swaps at 196 swaps/sec · zero collisions · all node-accepted
```
**~80× faster** than public electrum — the network round-trip *was* the whole bottleneck. The node
ingested all 5,894 txs into its mempool (~milliseconds each); the remaining cost is now **cashscript's
local covenant construction** (~35 ms per tx per lane), which parallelises with more lanes/shards.
Two side-findings: the A/B/C shard chains went **1,400+ deep unconfirmed with zero rejection** (BCH's
mempool ancestor limit is far above the old BTC 50), and at 196/s the *projected* rate (~117k / 10-min
block) already **exceeds a single block's capacity** — so throughput is now bounded by the block, as
below.
## 4. The ceiling — block size, not the asset
Two numbers; only the second is real:
- **Measured rate:** ~2.5 swaps/sec over **public electrum** (network-bound), **196 swaps/sec** over
the **local node** (build-bound) — an ~80× jump confirming the bottleneck was never the chain. Even
196/s is a client floor: it parallelises with more lanes/shards, and the node's mempool ingest is
~milliseconds per tx.
- **Consensus ceiling = block size.** Measured tx size on chain ≈ **642 bytes** (claim + token outputs;
redeems a bit larger). A **32 MB BCH block therefore holds ~52,000** such swaps — for **one asset**,
once shards remove the single-coin serialization. On **canonical 10-minute blocks**, that is
**~52,000 swaps per asset per block**, scaling **linearly with block size**. The measured local-node
rate (196/s → ~117k per 10-min) already exceeds one block, so the block is the binding constraint.
Throughput for one backed asset:
```
per-block ≈ min( K shards × ~1,400+ in-flight , block_size / tx_size ≈ 52,000 / 32 MB )
```
With enough shards to fill a block, the first term exceeds the second, so **throughput = block
capacity**. The reservoir/asset is not the bottleneck — the block is. Bigger blocks lift it directly.
## 4a. Single reservoir vs shards — is sharding necessary?
The prior section shows K shards remove the single-coin serialization. That is a *scaling* proof —
adding lanes lifts a soft ceiling. It doesn't answer the different, more practical question: **for one
backed asset on today's blocks, does a single reservoir already carry a full block, so that no sharding
is required at all?**
Measured on the same local BCHN node, isolating **K = 1** — one reservoir, one lane, one build chain:
```
one category · ONE reservoir (K=1) · round-trip · broadcast via local RPC
run 1 wallet c 4,233 swaps in 62.3s 68.0 swaps/sec (halt: funding exhausted)
run 2 wallet b 3,723 swaps in 53.8s 69.2 swaps/sec (halt: funding exhausted)
run 3 wallet d 1,266 swaps in 18.6s 68.1 swaps/sec (halt: chipnet's 20 MB mempool cap hit)
─────────────────────────────────────────────────────────────────────────
9,222 swaps across 3 clean runs · ONE reservoir · zero collisions · sustained 68.5/sec
```
Every run halted only because of an *environmental* cap — how much BCH one wallet had confirmed for
round-trip fees, or chipnet's tiny 20 MB `-maxmempool` (mainnet default is 300 MB). None halted from
reservoir contention: **zero `txn-mempool-conflict`, zero double-spends across all 9,222 swaps.** The
chain was held at 50 unconfirmed deep continuously (BATCH = 50 claims per redeem-all cycle); each
in-cycle claim spent the coin the previous created, exactly the serialization the reservoir imposes.
The rate is the finding. Projected to a canonical 10-min block:
```
68.5 swaps/sec × 600 s = 41,100 swaps per canonical block
─────────────────────────
52,265 swaps / 32 MB block (642 B tx)
─────────────────────────
≈ 78.6 % of a full 32 MB block
```
**A single reservoir already fills ~79 % of a 32 MB block. Sharding is not necessary — one shard is
enough to cover the vast majority of a block's capacity for one asset.** A single additional shard
(K = 2) saturates the block; anything beyond that is head-room, not a requirement. The block-bound
ceiling from §4 remains — 32 MB / 642 B = ~52,000 swaps per asset per block — but reaching it takes
one or two reservoirs, not seven.
**Where the residual 21 % goes:** at K = 1 the reservoir advance is inherently serial, and each claim
is one cashscript covenant build (~14 ms per tx on this hardware, single-threaded). One extra shard
lifts that to two concurrent build chains and closes the gap; more shards run past the block ceiling
and only serve as head-room for larger blocks. Sharding is a *reserve capacity* tool, not a
requirement for one-asset coverage on today's blocks.
**Bottom line for the "one shared reservoir is a contended pool" objection:** the contention exists
(§4, one-coin serialization), but at present tx sizes and block sizes it does *not* bind. Chaining
absorbs the whole workload for one asset. Sharding stays in the toolbox for the day tx sizes shrink
or blocks grow to the point that one chain can no longer keep up — until then, **K = 1 works**.
### Cumulative chipnet swap tally — authoritative
This section is the **canonical count** cited by public copy (potidaea.deviant.bch), CLAUDE.md, the
project-context, wiki/asm/index, and the Potidaea / Orbital-Swap spec pages. Update this table when
new throughput runs land, then refresh the "52,998" quote in the dependent files.
| Batch | Snapshot / session | Swaps | Config |
|---|---|---:|---|
| PoC / earliest bulk | 0008 → 0016 (129 bulk milestone) | 129 | first end-to-end + bulk |
| Sequencer / chain-depth / multi-reservoir | prior throughput scripts | 1,411 | queue 5/5, chain-depth 1,400, multi-shard 6/6 |
| Continuous 7-shard, public electrum | 180-s round-trip | 449 | 7 lanes, ~2.5/s |
| Continuous 7-shard, LOCAL BCHN node | 30-s round-trip | 5,892 | 7 lanes, 196/s |
| Single-reservoir K=1, LOCAL BCHN | earlier session, 3 runs | 9,222 | 1 reservoir, 68.5/s |
| **GMX Potidaea swap-relay** | **snapshot 0239 precursor** | **9,296** | **tiered-oracle GMX + client-added 0.5 % platform fee** |
| **OpenMint Potidaea swap-relay** | **snapshot 0239 (four revisions)** | **26,599** | **fresh DeviantOpenMintFT + 9-lane sequential relay + platform fee, 2:1 fee ratio, peak 44/s** |
| ────────────────────────────── | | ──────── | |
| **Cumulative measured** | | **52,998** | zero collisions, zero exploits |
The exact-count quote cited elsewhere is **52,998** (as of the 2026-08-12 snapshot-0239 third
extension). Future runs: add a row, sum, and grep-replace `52,998` in the dependent files.
## 5. Trust model
The sequencer only **orders and relays** — it can delay or reorder (a liveness role), but the
`claim()` covenant still enforces the peg, backing, and output shape, so **it cannot mint fake units
or steal**. It is non-custodial; run it permissionlessly, with multiple relays and the client-retry
path (2d) as the trustless fallback. This matches the project principle: *the relay reports/orders,
consensus enforces.*
*(Prototype caveat: the throughput harness funded each claim from the requester's own input in the
lanes, and the sequencer demo used a coordinator-funded gift-claim for simplicity. In production each
user funds and signs their own input; the sequencer only sequences. The covenant enforces either way.)*
## 6. Bottom line
- **One backed asset, one category, no user choice.**
- **Zero collisions** at any concurrency — the queue chains simultaneous requests; shards give
parallel lanes; auto-assign spreads load.
- **Throughput is block-bound, not asset-bound:** ~52,000 swaps per 32 MB block on canonical timing,
scaling with block size. Measured on chipnet with 9,222 clean single-reservoir swaps and no
collisions.
- **Sharding is optional, not required.** A single reservoir already covers ~79 % of a 32 MB block;
K = 2 saturates it. See §4a.
Two design choices, both high-throughput for a single asset: **self-backed** (no shared state, but
distinct per-holder categories — the companion doc) or **shared reservoir** (one category, scaled by
sequencer + shards — this doc). Block time is a *rate* lever (more blocks/hour); it is never required
for correctness — the queue prevents collision at any block time.
### Scripts (chipnet, reproducible)
- `scripts/chipnet-asm-sequencer.mjs` — queue/chaining (5/5)
- `scripts/chipnet-asm-chain-depth.mjs` — in-flight depth (1,400+ accepted, no rejection)
- `scripts/chipnet-asm-multi-reservoir.mjs` — K shards + auto-assign (6/6)
- `scripts/chipnet-asm-throughput.mjs` — continuous round-trip, public electrum (449, 0 collisions)
- `scripts/chipnet-asm-localnode-throughput.mjs` — continuous round-trip, **local BCHN node** (5,892 in 30 s, 196/s, 0 collisions); RPC creds read from `bitcoin.conf`, never hardcoded
- `scripts/chipnet-asm-single-reservoir-throughput.mjs` — **K = 1** (no shards), local BCHN node, mempool-aware funding lookup, multi-wallet input pooling; 9,222 clean swaps at 68.5/sec, zero collisions (§4a)