Skip to content

Prototyping #55

Description

@willjgould

Codecardano-ledger: 12 commits on nicolas/dynamic-pricing
Codeouroboros-consensus: 9 commits on nicolas/dynamic-pricing
Codecardano-node: 5 commits on nicolas/dynamic-pricing
Codeouroboros-leios: 13 commits on nicolas/dynamic-pricing

Prototype the tiered-pricing mechanism end-to-end on the Cardano Dijkstra era (the linear-Leios prototype), and run it under real load on a devnet so we can see the mechanism behave rather than argue about it on paper.

The mechanism: two inclusion strategies — urgent and optimistic — priced independently. Each publishes its own per-byte price. A transaction declares the lane it wants and carries a bid; the ledger quotes a price from that lane's published rate and the transaction's size, and rejects the transaction if the bid does not cover the quote. At the end of every block, each lane's price is stepped by an EIP-1559 controller against that lane's own utilisation. The two lanes are separate markets, end to end: separate prices, separate mempool queues, separate block budgets, separate transports.


What has been built

The ledger — #66

All five dynamic-pricing rules are implemented on the Dijkstra era and build -Werror: the pricing state carried on UTxOState, the new transaction-body fields (dtbInclusion, dtbBidFee, dtbFeeRefundAccount), the UTXO rule that quotes a price and enforces bid ≥ quote (BidBelowQuote) while accounting per-lane usage, the LEDGER rule that flushes pending refunds, and the BBODY rule that checks the block and reprices at its end.

The controller is Will's per-lane EIP-1559 step: target utilisation 1/2, D = 8, so at most ±12.5% per block — recalibrated 2026-07-13 to the value Will's simulator actually runs (764f16a10; we shipped D = 4 first), floored by the price-discrimination floor that keeps urgent above optimistic. Each lane steers against its own budget — urgent against the regular block, optimistic against the endorser block's real mainnet budget of 12 MB (the CIP-164 closure limit).

Two refinements came out of running it. The U1 predicate is exposed as an O(1) re-check, so a new tip no longer forces full re-validation of the mempool. And repricing now happens on application: a lane's controller steps only on a block that carries its own transport's bytes, and holds otherwise. A ranking block and a certified endorser block arrive in separate reprices, so without this each lane read the other's block as "my lane ran empty" and dropped 25% between full blocks.

Consensus — #67

There were consensus changes. The forge maps each lane onto its own transport: the ranking block takes the urgent lane, the endorser block takes the optimistic lane, and the two are disjoint by construction — no filler, and an optimistic transaction is always applied after every urgent one.

On top of that sits Giorgos's EB min-fill rule: an endorser block is only issued when it is worth certifying, at least half a regular block (45,056 bytes). Below that it is withheld and the transactions pool for the next round.

Each lane also gets its own diffusion-time budget, deliberately asymmetric — urgent keeps a short window (~1.7 s) because time-sensitive traffic must not queue for hours, while the patient lane buffers several endorser blocks' worth (~13.3 s). Before that, a deep patient backlog would saturate the shared budget and starve urgent admission.

The mempool — #68

The two lanes are genuinely separate structures, not a filtered view: admission routes by the declared lane, each lane holds its own tickets and its own byte and time budgets.

Cross-lane conflicts resolve first-come: an incoming transaction never displaces one already admitted, whichever lane it sits in. That choice pays for itself — under first-come, an incoming transaction provably commutes with every admitted one (input conflicts are rejected at the door, which is exactly Polina's reordering lemma), so urgent admission became O(1) where it used to re-apply the whole optimistic lane. Measured: 446 tx/s accepted, against 9–11 tx/s before.

Because the bid is a ceiling and prices move every block, a queued transaction can fall below the quote after it was admitted. It is evicted on re-validation against the new tip — and that re-validation is O(1) per transaction, not a full re-apply.

The node and the devnet — #73

The node builds against the local ledger and consensus and runs end-to-end on a three-node Dijkstra proto-devnet, forging both lanes with live published prices.

Load comes from an actor-driven lane feeder: each simulated sender buys urgent or optimistic by the value it retains, or walks away when neither lane pays off. The lanes run as independent workers, so a full lane cannot block the other.

Testing — #69

Unit specs for the controller and the usage accounting on the ledger side, and a property suite for the pure forge selection on the consensus side (ranking block within limit, urgent-only; endorser block optimistic-only; the two disjoint).

The substantive validation so far is observation of the running devnet: a 12 MB endorser block certified (5,434 transactions, 11,998,272 bytes); the optimistic quote climbing a clean staircase from 44 to 209 under a saturated pool once each lane reprices on its own block's verdict; ~300 tx/s sustained while the pool deepened to 27k transactions, with the eviction wave firing on quote-crossing; and the endorser block correctly withheld below the min-fill threshold.


Where the code lives

Branch nicolas/dynamic-pricing on cardano-ledger, ouroboros-consensus, cardano-node and ouroboros-leios (the demo). The design documents — the ledger rules, the two-lane mempool design, the transaction and block lifecycle diagrams — are kept alongside the prototype.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions