239 lines
15 KiB
Markdown
239 lines
15 KiB
Markdown
|
|
# 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)
|