Praos (Leios not enabled) · 5,000,000 B mempool override · MempoolTimeoutCapacity 20 s
3 workloads, one per region, at 36 tx/s each (13.6 KB/s), 108 tx/s aggregate (40.9 KB/s)
Run 2026-08-26-20-33-6deff--value-leios-nomadperf-coay-nom · time zero 2026-08-26 21:59:03 UTC (the cluster's first
log timestamp) · the generator's first trace at +6:11 · full scheduled duration,
ended by cluster shutdown (no starvation) · 132,048 funds split across 3
workloads, one builder per region (EU node-6, US node-13, AP node-2), 44,016 each ·
2 destination keys per builder · confirmation depth 3
Can a Cardano node run a 5 MB mempool, 28× the default, under a deliberate worst-case load (three disjoint per-region tx streams saturating it), and what does that cost Praos block production? Measured against an identical default-mempool control; the data is below, the conclusions in Section 5.
cardano-node 11.1.0.164, from leios-prototype commit
3e1bec0217b1560827956d5973120bbff983ee96 · running in Praos mode: Leios is not enabled ·
MempoolCapacityBytesOverride 5,000,000 B (Consensus-effective
5,078,016 B) · MempoolTimeoutCapacity 20 s (node default
5.0 s), a second capacity dimension
alongside bytes: the pool is full when the cumulative validation-time of its resident txs
reaches this, or when the byte override is reached, whichever binds first (at 5 MB the
byte cap binds first, so this 20 s is not the binding constraint here) ·
MempoolTimeoutSoft 1 s (default), MempoolTimeoutHard 1.5 s
(default) · maxBlockBodySize 90,112 B (Mainnet's value) · AWS EC2 c5d.2xlarge, x86_64 · memory 16,285,147,136 B (15.2 GiB) · kernel
6.12.30 · fleet CPUs: Intel Xeon Platinum 8124M @ 3.00GHz, 8223CL @ 3.00GHz, 8259CL @
2.50GHz, 8275CL @ 3.00GHz
Building this report fails unless every node namespace it consumes is configured with a
passing severity, at least the needed detail level, and no frequency limit; checked
against each target node's own TracerConfigInfo dump, all three verified
identical. A capped cross-check stream is accepted only after scanning every node log
and finding no limiter notice, so the cap demonstrably never fired.
The load generator: a closed-loop workload driver. A workload is a self-contained circuit: its own disjoint partition of funds (single-use UTxOs), its own builder turning funds into txs, its own token-bucket rate limit, one injection target and one observer following the chain. Workloads share no funds, no txs and no queues, so each region's loop runs, and can fail, independently. Every tx consumes funds and creates outputs; an output becomes a spendable fund again only once the observer has seen its tx reach the configured confirmation depth (on_confirm recycling), so the txs form chains gated by the chain itself, and the funds budget is the buffer that bridges confirmation latency. That buffer is also the alarm: with on_exhaustion = error, a workload that cannot recycle funds as fast as it spends them runs the buffer to zero and fails the run loudly instead of quietly degrading. Between those two borders each workload keeps two buffers, funds waiting to be built (the funds queue) and built txs waiting to be delivered (the payload queue, cap 8,192)
This exact setup: 3 workloads, one per region, each targeting its in-region node: EU · node-6, US · node-13, AP · node-2 · funds 132,048 total, 44,016 per workload · rate limit: token bucket at 36 tx/s per workload, 108 tx/s aggregate (40.9 KB/s at the 379 B announced size) · every tx 2-in 2-out, two witnesses (2 destination signing keys per builder); one tx, three sizes: 371 B raw CBOR, 374 B in the mempool's accounting (CBOR + 3), 379 B announced on the wire (CBOR + 8) · recycling on_confirm at confirmation depth 3 · on_exhaustion = error
Building this report fails unless every tx-centrifuge namespace it consumes is configured with a passing severity, at least the needed detail level, and no frequency limit; checked against the run-script's TraceOptions.
51 pools · 3 regions: 17 × EU eu-central-1 (Frankfurt), 17 × US us-east-1 (N. Virginia), 17 × AP ap-southeast-2 (Sydney) · torus-dense · 306 producer entries = 204 links (102 reciprocated) · 153 intra-region links, 51 per region · 51 inter-region links, 17 per region pair · tx-centrifuge on the explorer machine (eu-central-1): not a pool, its only links are its three injection arrows · p2p graph properties (undirected, over the 51 pools): every pool has exactly 8 distinct link partners (6 outbound producers + 6 inbound, 4 of them mutual) · diameter 4, and every node's eccentricity is 4, so radius 4 too · mean shortest path 2.48 hops · the topology is static over the P2P stack: peer sharing is disabled (ncPeerSharing = PeerSharingDisabled in the node's configuration dump) and each node's producer set is fixed by its topology file, so a node talks only and always to its listed producers
Node positions, the tx-centrifuge diamond and its injection arrows are identical in all three views; hover a node to highlight its links everywhere and see its degrees. Region colors everywhere: EU green, US red, AP blue. Arrows: the links tx-centrifuge injects txs over, one per target, 36 tx/s requested in this run; each target also serves as its workload's chainsync observer (confirmation depth 3 in this run). Links are drawn undirected; direction and per-node producer lists are in the adjacency table.
cardano-cli ping round-trips over the node-to-node handshake, measured at cluster start by each pool's healthcheck against its 6 producers: 306 directed links × 3 samples each = 918 measurements. Milliseconds.
Not an ICMP ping: cardano-cli ping opens a TCP connection and runs the node-to-node handshake protocol, so each sample is an application-level round-trip through both nodes' network stacks and mux, which is what the Cardano protocols actually experience; a raw ping, where ICMP is allowed at all, measures kernel-level echo with no TCP and no node in the path. The TCP connect column is the bare TCP round-trip from the same tool, the closest available stand-in for a raw ping; the gap to the protocol mean is a consistent ~0.3 ms on every link class, the protocol and scheduler overhead. SEM = SD / sqrt(n). The explorer's healthcheck has no producers to ping, so there are no direct tx-centrifuge measurements; the generator sits in eu-central-1, so its target RTTs follow the EU rows: node-6 within EU, node-13 EU to US, node-2 EU to AP.
Everything measured from the run, in three views: Workload (tx-centrifuge), the generator's side of the closed loop, funds, deliveries and demand, from the tool's traces alone · Node mempool, each target pool's size and flows, from the node's Mempool traces alone · Chain, where the two sides meet and the blocks decide: composition, block fill and each workload's share of what was included
All traces in this run are unfiltered (per event): ~3.1 M queue-depth samples, ~2.4 M mempool-size samples, and a per-event stream behind every rate. Depth-style series are drawn as a shaded min-to-max band per 42 s bucket with a 2 px line through each bucket's final value, so extremes survive exactly and oscillation reads as band thickness. Rates use fixed bins drawn as steps, 30 s in the 30-minute left charts and 300 s in the full-run right charts; the two mean-speed rows are running since-first-send averages, exact at every event. Nothing is smoothed. Every chart row is split at 30 minutes: the ramp on the left from zero, the rest of the run on the right, with right axes clipped to the steady range where noted. The crosshair links both halves and all sections; lines interpolate between samples; hover or focus a chart and use arrow keys to read the last observed sample per series. The dropdown under each row names that chart's exact traces and computation.
The generator's side, tx-centrifuge traces only: circulating funds (the closed loop at work), cumulative delivered bytes, the mean delivery speed, and the unserved demand at each target's tx-submission server
What this run demonstrates about tx-centrifuge. A fixed, finite budget of 132,048 UTxOs ran a closed loop for 17 hours 39 minutes with zero fund loss: the funds ledger closes two independent ways, from the border flows (ReplyTxs deliveries minus Recycler returns) and from the queue depths (the Pipe traces), agreeing exactly at the builder border and to within ~200 in-transit funds at the network border. Pull-based delivery never overran a target, and the Unserved demand row is the receipt: because the node pulls, demand is explicit on the wire, and the run shows each workload's rate control independently at work: node-6 re-asks 41,533 ids while its 36 tps bucket caps the fill, node-2 never accumulates more than 19 because its 250 ms RTT already paces it below the bucket, and every curve goes dead flat the moment the pools fill: three independent loops, each visibly finding its own equilibrium. Forks do not shake it either: this run's height-battle rate is ten times its control's (.04968 vs .00477 per block), yet the offered load never wavered, because an orphaned tx's funds are recovered and rebuilt automatically (10,882 orphan recoveries alongside 689,248 clean confirmations) and the pull loop keeps delivering at the chain's pace; the Unserved demand curves stay flat through every battle. It kept all three 5 MB mempools, all three payload queues and the confirmation window saturated for the entire run, was the source of every one of the chain's ~10.8 tx/s for 17 hours, and its fail-fast guard (on_exhaustion = error) stayed armed throughout and never fired. The design choice that makes these numbers benchmark-grade is node-to-node: the load rides the node's own TxSubmission protocol as a genuine peer, so it is pulled at each target's real pace over the real topology, exactly what the network experiences under load. A node-to-client pusher such as tx-firehose fills a single node's mempool directly through the local socket, ideal for localhost and single-node fills, but it bypasses relay topology and contends with block production on the block producer, so it cannot measure diffusion or RTT-paced pull. Closed-loop confirmation-depth recycling gives provable fund conservation and indefinite runs on a finite budget, and rides through forks in-process, as the orphan recoveries above show; the per-target token bucket and fail-fast guard make each region's load independent and its failure loud. The measurement comes off the same wire as the load. The Workload and Chain numbers come from its own traces, the mempool and hardware charts from the nodes' traces, and the composition row joins the two; the sides cross-check: replayed mempool contents match the nodes' counters, and delivered sizes match the mempool's accounting, to the fund, the tx and the byte.
The closed loop measured at the network border: a queueing input-output counter, N(t) =
funds sent minus funds returned, as a share of the workload's 44,016 funds. Sent:
tx-centrifuge TxCentrifuge.TxSubmission.ReplyTxs (DDetailed needed for the
txs list), counted as 2 funds per delivered tx with no builder join, and that is exact,
not an estimate: the event's target identifies the workload by construction
(each workload's tx-submission server feeds from its own payload queue and serves exactly
one target, so a foreign tx cannot appear in the batch), and every tx in this profile
consumes exactly 2 funds (inputs_per_tx = 2, a fixed builder shape; the pools staying
pure 374 B two-witness all run verifies it never drifted). Only the tx count is used,
never the fund lists. Returned: TxCentrifuge.Recycler.AddToPipe (DDetailed suffices,
it carries count; this run logged DMaximum), minus that count,
the number of funds released (confirmed outputs, or original inputs on orphan recoveries; the
recycler handles only its own workload, no join needed; 700,130 releases vs 689,248 pure
confirmations by the Announce cross-check). What circulates is in mempools, the
confirmation window or transit; the rest of the budget sits tool-side in the payload
buffer or the funds queue. Conservation: the counter equals 44,016 minus the funds queue
minus twice the payload queue, to within ~200 in-transit funds at its worst transient,
typically a few dozen (ends
28,508 / 25,412 / 21,596 measured vs 28,520 / 25,434 / 21,780 predicted). Display: 42 s
min-max band with the bucket-final line, no smoothing.
Same TxCentrifuge.TxSubmission.ReplyTxs replies; the per-tx size
fields (raw CBOR bytes, 371 B for a two-witness tx; the same tx announces as 379 B on the
wire, the size the token bucket meters) accumulate into a running total per target. For a
speed, the cumulative curve is the smoothing-free display: the slope is the delivery
rate, ~13.4 KB/s of raw CBOR while the token bucket binds during the fill and a steady
1.59 / 1.32 / 1.24 KB/s (node-6 / node-13 / node-2) after it, a stall would show as a
flat spot, and the end value is
the total delivered: 105.2 MB to node-6, 86.8 to node-13, 81.7 to node-2. A moving
average would lag and hide extremes; a since-start average would stop responding after
the first hours. Display: 42 s band with the bucket-final line, no smoothing.
The running average speed, recomputed at every delivery: total raw CBOR bytes sent to the target so far, divided by the time elapsed since that workload's first send. No window, no weighting, no smoothing; every point is exact. The curve starts at the token bucket's rate while the bucket binds (~13.4 KB/s during the fill), then eases toward the whole-run average as steady state accumulates: it ends at 1.66 KB/s to node-6, 1.37 to node-13, 1.29 to node-2. The dashed gray curve is the same running average over the three targets combined, from a 33.0 KB/s fill peak to 4.31 KB/s at the end. Read it with the Delivered bytes row above: this is that curve's height over elapsed time.
A cumulative counter per target: every tx-centrifuge
TxCentrifuge.TxSubmission.RequestTxIds adds its req, the number
of new tx ids the target asks for (DNormal suffices, it carries ack,
req and target; 310,418 requests), and every
TxCentrifuge.TxSubmission.ReplyTxs batch subtracts the txs it delivered.
The height is every ask the generator could not serve at ask time: during the fill the
36 tps token bucket paces replies below what a fast target asks, the target re-asks, and
the excess accumulates, node-6 to 41,533 ids (asked 325,189, delivered 283,656) and
node-13 to 4,889 in the first minutes, while node-2's 250 ms RTT already paces its asks
and its backlog peaks at 19 and ends at 12. After the fill every curve is flat to the end: in steady state
every ask is served (starvation, the failed run's QueueStarved signature, would climb
here). Display: 42 s min-max band with the bucket-final line, no smoothing.
The nodes' side, Mempool traces only: pool size, and the pool's ingress and egress at each injection target
Node traces Mempool.AddedTx, Mempool.RejectedTx and
Mempool.RemoveTxs, selected by namespace only (~2.4 M events; DNormal
suffices, the node default here). Every one carries mempoolSize.bytes, the
pool's own accounting after the operation (txInBlockSize = sizeTxF + perTxOverhead 4,
about raw CBOR + 3; 374 B per two-witness tx). Display: 42 s min-max band with the
bucket-final line, no smoothing.
Node Mempool traces only. Every Mempool.AddedTx event carries
mempoolSize.bytes after the operation, so the rise over the previous
mempool event's value is exactly the accepted tx's bytes entering the pool; rejected
deliveries change nothing and are excluded by construction. Ingress counts every
accepted arrival, the workload's own deliveries and foreign txs diffusing in from peers
alike, which is why the fill-phase peaks (up to 27 KB/s at node-13) exceed the tool's
13.4 KB/s delivery pace. Running average since the first add (+0:07:13). Ends: node-2
4.14 KB/s (262.7 MB), node-13 3.59 (227.8 MB), node-6 1.94 (122.7 MB); ingress minus
egress equals the pool's resident bytes at its last event, to the byte.
Node Mempool traces only. Every Mempool.RemoveTxs event carries
mempoolSize.bytes after the operation, so the drop from the previous
mempool event's value is exactly the bytes an adopted block claimed from this pool; no
join and no tx-centrifuge data. This is egress from the mempool, not from the chain:
all three nodes adopt the same full blocks at the chain's ~4.06 KB/s (in the mempool's
374 B measure), so a block-level egress would be identical everywhere; what differs per
node, and what this row measures, is how much of each block was actually resident in
its pool. Running average since the first removal (+0:07:48), the same construction as
the Workload section's Mean delivery speed. Ends: node-2 4.06 KB/s (257.7 MB, virtually
the whole chain passed through its pool), node-13 3.51 (222.8 MB), node-6 1.86
(117.8 MB, barely half: its ~99% own-workload pool never held most of what the chain
included; the blocks that remove anything claim ~137 of their ~238 txs from it, ~109
averaged over all 2,896 blocks, since ~600 remove nothing). The dashed line on
both speed rows marks the ~4.06 KB/s whole-chain rate.
Node Mempool traces only: every Mempool.RejectedTx event names exactly one
tx.txid, so the counter adds one per event (verified: events equal txids on
all three nodes). How a rejection happens: a tx offered over tx-submission is validated
before entering the pool, and the offer is refused when the pool already holds that txid
or when its inputs are no longer available because a resident or already-confirmed tx
consumed them; duplicate offers of the same tx arrive over parallel peer paths, and
stragglers keep arriving after confirmation. A full pool by itself does not
reject: an add attempt blocks up to MempoolTimeoutHard (1.5 s) for space, then
disconnects. A rejection is a
refused offer, not an eviction: nothing enters or leaves the pool. How it is accounted
in this report: this counter adds one; the size charts above take the event's unchanged
after-event mempoolSize as one more sample; and the ingress and egress rows
add nothing, because the event's byte delta is zero, which is exactly what keeps
ingress minus egress equal to the resident bytes. Rejections start with the very first
deliveries (+0:07:13) and end at 401,313 for node-2, 325,976 for node-13 and 69,767 for
node-6; the order tracks pool mixing: the most-mixed pool refuses the most re-offers,
node-6's ~99% own-workload pool the fewest.
The arbiter, where the two sides meet: the composition row joins node mempool events to the generator's build map, the block and share rows come from the generator's chain observer, and a node block-fetch trace cross-checks them · 84.1% of blocks are single-workload snapshots; every block is full; the cumulative split is 38.3 / 32.1 / 29.6% (EU / US / AP)
In three steps. One: the generator logs every tx it builds,
TxCentrifuge.Builder.NewTx carries builder and
txId, which gives a dictionary from txId to the workload that built it
(DNormal suffices; this run logged DMaximum). Two: each node logs every pool change, so
replaying Mempool.AddedTx (insert that txid) and
Mempool.RemoveTxs (delete the txids it lists) in log order reconstructs the
exact set of txids resident in the pool at every moment;
Mempool.RejectedTx changes nothing (1.65 M events replayed). Three: at each
moment, look every resident txid up in the dictionary and divide: own-share % =
residents built for this node over all residents, recorded as the last value per 42 s
bucket. Details: node-side txids are truncated to 8 hex digits at DNormal, so the
dictionary matches on the first 8 digits; this is set replay, not sampling, and the
replayed set sizes match the nodes' own mempoolSize counters throughout.
The TxCentrifuge.Observer.Announce stream (the observer following node-6's
chain) clustered into blocks: announcements arrive in per-block bursts spread over
milliseconds while blocks sit ~20 s apart, so a gap above 0.25 s starts a new block.
2,896 blocks over the run, one every 21.9 s on average; the seven bursts of exactly
476 txs are back-to-back full blocks closer than 0.25 s, split back into their two
blocks in announce order. Bytes per block = announced txs × 374 B, exact because every
tx in this run is a verified 374 B two-witness tx; full = 238 txs = 89,012 B, the most
that fits under the 89,088 B effective block body, drawn dashed. Every one of the 2,896
blocks is full, from the very first at +0:07:16 to shutdown: the chain ran at capacity
for the entire run. The left chart keeps the from-zero scale; the right is zoomed to a
500 B window, so the flat line and its 76 B headroom under capacity are visible, and a
not-full block would plunge off the bottom edge; none does, and no burst falls below
238 announced txs, so the gap rule split nothing. 84.1% of blocks carry txs from a
single workload
(the median block: all 238 from one forger's own-workload pool). Node-side cross-check,
from a direct node trace: BlockFetch.Client.CompletedBlockFetch carries
each downloaded block's wire size. Its dominant value is a constant 88,457 B, the full
block on the wire (3,712 of node-6's 4,063 fetches), and its 18 sub-kilobyte entries are
the empty blocks forged before the generator's first delivery, which the announce stream
cannot see; the fetch stream also holds fork and duplicate downloads, which is why its
count exceeds the chain's 2,896. The stream carries a 2 events/s config cap, and the
build verifies no limiter notice exists in any node log, so it is complete.
In three steps. One: the same txId-to-workload dictionary from
Builder.NewTx. Two: the observer following node-6's chain logs one
Observer.Announce per tx entering an adopted block, the complete on-chain
stream in order (689,248 announcements). Three: three running counters, one per
workload; every announcement looks its txId up in the dictionary and adds one to its
builder's counter, and the chart plots each counter over the sum of all three, per bin.
The counters end at 264,018 / 221,370 / 203,860 (EU / US / AP), which is
38.31 / 32.12 / 29.58%, within about five points of equal thirds. Raw shares are
composition, not chain preference: a workload whose funds return
faster builds, sends and therefore lands more txs, and the loop feeds back, since income
is inclusion. The chain-side check is included over delivered per workload:
264,018 / 283,656 = 93.1% EU, 221,370 / 233,971 = 94.6% US, 203,860 / 220,261 = 92.6% AP,
equal within two points; the remainder is txs still in mempools or the confirmation
window at shutdown, plus orphaned txIds rebuilt under new ids. The chain converts what
every workload delivers at the same rate; the EU volume edge is the co-location edge,
its funds return fastest, while the forging lottery is uniform over the 51 pools.
Node traces only: LedgerMetrics carries chainDensity, the
node's own reading of its selected chain's density, emitted about once per slot
(~63,700 samples per node). All three nodes report an identical series, as followers of
the same chain. The value is windowed over the node's recent chain, which is why it
swings between .031 and .073 rather than settling on the whole-run .0457. The dashed
line is the configured activeSlotsCoeff, 0.05; monitoring dashboards often show the
same quantity normalized to it, where 0.05 reads as 100% and this band as 62% to 146%. Display: 42 s min-max band with the
bucket-final line, no smoothing.
Three runs side by side: the vanilla 11.0.1 run, the leios-prototype control without the mempool overrides, and this run with them, +5MB mempool · per metric: each run's average, the control's delta against 11.0.1, and this run's delta against the control · the Summary table is run data, without deltas · both leios runs were driven by tx-centrifuge at 108 tps aggregate: the Summary's TPS 12.0 and Transaction count are the profile's nominal tx-generator values, not the load that ran.
One row per run, each built from that run's own files. The 11.0.1 row is the one
piece here that is not part of the standard flow: a vanilla, non-leios-branch node
build from an earlier campaign, kept for orientation. Run time is locli's
"Run time, s" from summary.org. Total slots and blocks are the raw totals
in the locli filter line of blockprop.control.org. Chain density is blocks
over slots, read against activeSlotsCoeff 0.05. ChainDB's SwitchedToAFork
events (each time a node's chain selection jumps to a competing chain) exist only for
the three target nodes whose logs ship with the run bundle, a sample, not the cluster:
this run 152 / 154 / 157, median 154; the control 68 / 57 / 60, median 60; the vanilla
run ships no node logs at all. locli's Height & slot battles row, computed over all
51 hosts' logs, is in the Control sample table below. The per-node density over time is
the Chain density chart in 2.c. The rows
are, in order: 2026-05-06-08-27-6ca87-1101-value-voltv11-nomadperf-coay-nom,
2026-08-28-14-00-4cd78-defaultmempo-value-leios-nomadperf-coay-nom,
2026-08-26-20-33-6deff--value-leios-nomadperf-coay-nom; both leios runs are
the same cardano-node build, and the short prefixes in the run names tag the run, not
the node. For this run the 2,914 raw blocks exceed the body's announce-based 2,896 by
the 18 empty blocks forged before the generator's first delivery, which the announce
stream cannot see. locli's epoch3+ cut removes blocks 0-804 / 0-730 / 0-722
(11.0.1 / control / 5 MB) and its further in-window exclusions are 156 of 2,400 (6.5%)
/ 79 of 2,383 (3.3%) / 138 of 2,191 (6.3%), for filtered ratios of 0.70 / 0.73 /
0.70.
1. Forge and adoption cost. A header leaves the forge ~20× later (slot start to announced .073 to 1.517 s) and half-cluster adoption takes ~3.3×, +232% (0.50 adoption .626 to 2.077 s). The cost is forge-side, not wire: fetch duration is flat across all three runs (.348 / .354 / .358 s), so the blowups are origin-side, the forge loop working through a full pool, not diffusion.
2. Corroborating costs. Height and slot battles rise ~10× (.0048 to .0497 per block). Chained to forged blocks: .966 / .964 / .900 (11.0.1 / control / 5 MB): the 5 MB run orphaned about one forged block in ten, against about one in thirty for the other two. Chain density falls ~7% (.04904 to .04575) against the 0.05 target. Average CPU is ~3.5× (3.35 to 11.73%), while CPU 85% spans fall to 0.55 slots against ~7 in the other two runs: a full pool is sustained reapply load, not bursts.
3. Caveats. Per-stage forge timings are not comparable across builds: vanilla 11.0.1 books ~48 ms as Mempool snapshotting and ~0 in the forge steps, while the leios-prototype books ~0 snapshotting and folds that cost into the forge steps (Leadership to forged reads .0008 s for 11.0.1 vs .0505 s for the control, the same ~48 ms re-labeled); compare the control against 11.0.1 only on aggregate forge and adoption metrics, never stage by stage. The control was also saturated: its default pools (two blocks, ~180 KB) ran full under the same 108 tps load, and all three runs make full ~88.5 KB blocks, so the comparison isolates mempool size at saturation, not saturation itself. The two leios runs share the node build, generator, load and topology and differ only in the mempool configuration (MempoolCapacityBytesOverride 5,000,000 B and MempoolTimeoutCapacity 20 s against the node defaults), so the Δ vs leios-prototype column isolates the mempool overrides, while Δ vs 11.0.1 crosses node builds and generators (11.0.1 with tx-generator at 12 tps): context, not attribution.
The run completed its full 17 h 39 m schedule and ended by cluster shutdown; no starvation event fired. All three mempools pin at exactly 13,577 txs = 5,077,798 B of pure 374 B two-witness txs. Composition: node-6's pool holds ~99% own-workload txs throughout, and the blocks that remove anything take ~137 of their ~238 txs there (~109 averaged over all blocks; ~600 remove nothing); node-2 and node-13 oscillate between ~28% and ~80% own share; the union across the three pools stays near 27k distinct txs. Every one of the 2,896 blocks is full at 238 txs, one every ~22 s. 84.1% of blocks carry txs from a single workload, and the median block holds all 238 from one workload. The cumulative inclusion split ends at 38.3 / 32.1 / 29.6% (EU / US / AP); included over delivered per workload is 93.1 / 94.6 / 92.6%.
Scope. Praos, Leios not enabled: this run measures the mempool state and the block-production cost of a scaled mempool, not Leios certification. One run per configuration, with an identical default-mempool control on the same hardware and time window. The load is a deliberate worst case for mempool divergence (three disjoint per-region tx streams), not a model of Mainnet's overlapping demand.
1. A 5 MB mempool ran without failing. The node held a 5,078,016 B mempool (28× the default, 13,577 txs) full for the entire 17 h 39 m schedule, ending by cluster shutdown, not starvation: heap flat, funds conserved to within ~200 in-transit at any instant, the fail-fast guard never fired. The budget's margin was thin: node-6 finished at exactly 0 spare funds (~1% headroom overall), so the next run should size the fund budget up. Scaling the mempool this far is mechanically safe under Praos.
2. The cost is the mempool size, and the control attributes it. All three runs produce full ~88.5 KB blocks, so the throughput is the same and the difference is forge cost. Against the identical default-mempool control (same build, generator and load), the 5 MB pool cost roughly 3× block adoption (0.63 to 2.08 s at the median), a header leaving the forge ~1.5 s into the slot (0.07 to 1.52 s), ~10× the height and slot battles (.0048 to .0497), a fall in blocks that reach the chain from .96 to .90 (one forged block in ten orphaned), ~7% lower chain density, and ~3.5× CPU. The cost is forge-side, not diffusion: fetch duration is flat across all three runs (~0.35 s). The 1.52 s forge time exceeds the 1 s slot length: the block's header is announced more than a slot after the slot began. This is the computational backlog at the nodes that saturated all-producer runs exist to expose, now large enough to affect slot timing. Total adoption still sits within the "95% of nodes in 5 s" diffusion target (1.00 adoption 2.48 s), so the security budget is not breached, but the origin-side backlog consumes a large share of it and shows in the degraded chain quality above (the ~10× battles and ~1-in-10 orphaned blocks). It is the pool size, not the prototype build: on the aggregate forge and adoption metrics the control matches vanilla 11.0.1, so the prototype adds no measurable overhead at the default mempool and this cost should generalize to a release node (the per-stage forge breakdown is re-attributed between builds and is not compared stage by stage). The workload is value-only, the cheap case for reapplication; a script-heavy mempool of the same size would cost more. The delta against vanilla 11.0.1 crosses build, generator and load, and is context, not attribution.
3. The imposed fragmentation did not reach the chain. The load holds the mempools maximally divergent by construction: three disjoint per-region tx streams, so the co-located EU node holds ~99% its own region's txs while the two remote nodes oscillate between ~28% and ~80% own share, the union across the three pools is about two disjoint pools' worth, and 84% of blocks carry a single workload. Under Praos this did not affect the chain: every block full, inclusion fair to thirds (38.3 / 32.1 / 29.6%), each region's txs converted at the same rate (93.1 / 94.6 / 92.6% included over delivered). In Praos a block carries its own transactions, so mempool divergence cannot threaten validity or fairness once block production is regionally balanced. This is a Praos property and does not carry to Leios, where an endorser block references transactions its voters must already hold.
4. For Leios, this bounds the input and leaves the consequence open. The measured divergence (union ≈ two mempools) is a data point for TxCache sizing, and the forge-side slowdown under a full pool draws on the same budget an endorser block's validation would compete for. Whether fragmentation of this magnitude prevents endorser blocks from certifying within their vote deadline is not testable in Praos; only a Leios-enabled run measures it. This run shows the fragmented state is real and reachable with three regional load sources; it does not show whether Leios survives it.
Next. Enabling Leios (endorser blocks on) to test finding 4; independently, a TxSubmission V1-versus-V2 comparison (a separate knob that may or may not be combined with the Leios runs); repeats to put error bars on findings 1 and 2; and a node-local, coloring-free divergence metric (block-hit-rate) to carry the finding-3 measurement onto testnets.
Both sides' leftovers: the tool's internal buffers from tx-centrifuge traces, funds waiting to be built (the funds queue) and built txs waiting to be delivered (the payload queue), plus CPU and RTS heap from the node Resources trace
tx-centrifuge traces TxCentrifuge.Pipe.InputsEnqueued and
TxCentrifuge.Pipe.InputsDequeued, unfiltered; DNormal detail suffices
(depth and pipe; this run logged DMaximum) (~1.6 M events). Every event
carries depth, the queue depth after its own operation, so merging both
directions in time order gives the exact depth trajectory; time zero is the earliest
timestamp across the generator and node logs. Display: per 42 s bucket, the band spans min to max and the line passes through the
bucket-final value. No smoothing.
tx-centrifuge traces TxCentrifuge.Pipe.PayloadEnqueued and
TxCentrifuge.Pipe.PayloadDequeued, unfiltered; DNormal suffices (~1.5 M
events); same exact-depth
method and 42 s band display as the funds queue. The 8,192 cap is the TBQueue size
hardcoded in the tx-centrifuge Config.Runtime.
Node trace Resources, field CentiCpu, a cumulative CPU
centisecond counter. Per-sample % is the counter delta over wall time between consecutive
1 s samples; the band is the min to max of those within each 42 s bucket, and the line is
the true bucket mean, the counter difference across the bucket over its duration. Rates
from a counter, no smoothing.
Node trace Resources at 1 Hz; every field ships at any detail level
(~63 k samples per node), field
Heap in bytes. Display: 42 s min-max band with the bucket-final line.
Payload queues reach the 8,192 cap within minutes and stay there for the rest of the run; with the mempools full, targets accept new txs at the chain's replacement rate, ~10.8 tx/s total, one ~238-tx block every ~22 s. The funds queues diverge after the fill: node-6 stays near the floor for the whole run (post-fill maximum 1,570, ending at 0), while node-2 and node-13 hold idle funds in slow waves, up to 23,752 and 17,316 mid-run, ending at 2,198 and 5,852. Circulating funds: node-6 stays inside a 59 to 67% band all run; node-2 and node-13 dip to 9% and 23% mid-run. Recycle income: 269,402 txs returned to node-6's workload (~4.2/s), 223,173 to node-13's, 207,555 to node-2's. 44,016 minus the funds queue minus twice the payload queue matches the circulating counter to within ~200 in-transit funds at its worst transient, typically a few dozen.
Last observed value by the end of each minute since start; · means no sample yet.