|
| 1 | +# Prototype demo - October 2025 |
| 2 | + |
| 3 | +Minimum viable demo of Leios network traffic interfering with Praos using a three node setup and prepared Praos and Leios data. |
| 4 | + |
| 5 | +> ![WARNING] |
| 6 | +> TODO: Add overview / architecture diagram. |
| 7 | +
|
| 8 | +See https://github.com/IntersectMBO/ouroboros-consensus/issues/1701 for more context. |
| 9 | + |
| 10 | +## Prepare the shell environment |
| 11 | + |
| 12 | +- If your environment can successfully execute `cabal build exe:cardano-node` from this commit, then it can build this demo's exes. |
| 13 | + |
| 14 | + ``` |
| 15 | + $ git log -1 10.5.1 |
| 16 | + commit ca1ec278070baf4481564a6ba7b4a5b9e3d9f366 (tag: 10.5.1, origin/release/10.5.1, nfrisby/leiosdemo2025-anchor) |
| 17 | + Author: Jordan Millar <jordan.millar@iohk.io> |
| 18 | + Date: Wed Jul 2 08:24:11 2025 -0400 |
| 19 | +
|
| 20 | + Bump node version to 10.5.1 |
| 21 | + ``` |
| 22 | + |
| 23 | +- The Python script needs `pandas` and `matplotlib`. |
| 24 | +- The various commands and bash scripts below needs `toxiproxy`, `sqlite`, `ps` (which on a `nix-shell` might require the `procps` package for matching CLIB, eg), and so on. |
| 25 | +- Set `CONSENSUS_BUILD_DIR` to the absolute path of a directory in which `cabal build exe:immdb-server` will succeed. |
| 26 | +- Set `NODE_BUILD_DIR` to the absolute path of a directory in which `cabal build exe:cardano-node` will succeed. |
| 27 | +- Set `CONSENSUS_REPO_DIR` to the absolute path of the `ouroboros-consensus` repo. |
| 28 | + |
| 29 | +- Checkout a patched version of the `cardano-node` repository, something like the following, eg. |
| 30 | + |
| 31 | +``` |
| 32 | +6119c5cff0 - (HEAD -> nfrisby/leiosdemo2025, origin/nfrisby/leiosdemo2025) WIP add Leios demo Consensus s-r-p (25 hours ago) <Nicolas Frisby> |
| 33 | +``` |
| 34 | + |
| 35 | +- If you're using a `source-repository-package` stanza for the `cabal build exe:cardano-node` command in the `NODE_BUILD_DIR`, confirm that it identifies the `ouroboros-consensus` commit you want to use (eg the one you're reading this file in). |
| 36 | + |
| 37 | +## Build the exes |
| 38 | + |
| 39 | +``` |
| 40 | +$ (cd $CONSENSUS_BUILD_DIR; cabal build exe:immdb-server exe:leiosdemo202510) |
| 41 | +$ IMMDB_SERVER="$(cd $CONSENSUS_BUILD_DIR; cabal list-bin exe:immdb-server)" |
| 42 | +$ DEMO_TOOL="$(cd $CONSENSUS_BUILD_DIR; cabal list-bin exe:leiosdemo202510)" |
| 43 | +$ (cd $CONSENSUS_BUILD_DIR; cabal build exe:cardano-node) |
| 44 | +$ CARDANO_NODE="$(cd $CONSENSUS_BUILD_DIR; cabal list-bin exe:cardano-node)" |
| 45 | +``` |
| 46 | + |
| 47 | +## Prepare the input data files |
| 48 | + |
| 49 | +``` |
| 50 | +$ (cd $CONSENSUS_BUILD_DIR; $DEMO_TOOL generate demoUpstream.db "${CONSENSUS_REPO_DIR}/demoManifest.json" demoBaseSchedule.json) |
| 51 | +$ cp demoBaseSchedule.json demoSchedule.json |
| 52 | +$ # You must now edit demoSchedule.json so that the first number in each array is 182.9 |
| 53 | +$ echo '[]' >emptySchedule.json |
| 54 | +$ # create the following symlinks |
| 55 | +$ (cd $CONSENSUS_REPO_DIR; ls -l $(find nix/ -name genesis-*.json)) |
| 56 | +lrwxrwxrwx 1 nfrisby nifr 30 Oct 24 16:27 nix/leios-mvd/immdb-node/genesis-alonzo.json -> ../genesis/genesis.alonzo.json |
| 57 | +lrwxrwxrwx 1 nfrisby nifr 29 Oct 24 16:27 nix/leios-mvd/immdb-node/genesis-byron.json -> ../genesis/genesis.byron.json |
| 58 | +lrwxrwxrwx 1 nfrisby nifr 30 Oct 24 16:27 nix/leios-mvd/immdb-node/genesis-conway.json -> ../genesis/genesis.conway.json |
| 59 | +lrwxrwxrwx 1 nfrisby nifr 31 Oct 24 16:27 nix/leios-mvd/immdb-node/genesis-shelley.json -> ../genesis/genesis.shelley.json |
| 60 | +lrwxrwxrwx 1 nfrisby nifr 30 Oct 24 16:27 nix/leios-mvd/leios-node/genesis-alonzo.json -> ../genesis/genesis.alonzo.json |
| 61 | +lrwxrwxrwx 1 nfrisby nifr 29 Oct 24 16:27 nix/leios-mvd/leios-node/genesis-byron.json -> ../genesis/genesis.byron.json |
| 62 | +lrwxrwxrwx 1 nfrisby nifr 30 Oct 24 16:27 nix/leios-mvd/leios-node/genesis-conway.json -> ../genesis/genesis.conway.json |
| 63 | +lrwxrwxrwx 1 nfrisby nifr 31 Oct 24 16:27 nix/leios-mvd/leios-node/genesis-shelley.json -> ../genesis/genesis.shelley.json |
| 64 | +``` |
| 65 | + |
| 66 | +## Prepare to run scenarios |
| 67 | + |
| 68 | +Ensure a toxiproxy server is running. |
| 69 | + |
| 70 | +``` |
| 71 | +$ toxiproxy-server 1>toxiproxy.log 2>&1 & |
| 72 | +``` |
| 73 | + |
| 74 | +## Run the scenario |
| 75 | + |
| 76 | +Run the scenario with `emptySchedule.json`, ie no Leios traffic. |
| 77 | + |
| 78 | +``` |
| 79 | +$ LEIOS_UPSTREAM_DB_PATH="$(pwd)/demoUpstream.db" LEIOS_SCHEDULE="$(pwd)/emptySchedule.json" SECONDS_UNTIL_REF_SLOT=5 REF_SLOT=177 CLUSTER_RUN_DATA="${CONSENSUS_REPO_DIR}/nix/leios-mvd" CARDANO_NODE=$CARDANO_NODE IMMDB_SERVER=$IMMDB_SERVER ${CONSENSUS_REPO_DIR}/scripts/leios-demo/leios-october-demo.sh |
| 80 | +$ # wait about ~20 seconds before stopping the execution by pressing any key |
| 81 | +``` |
| 82 | + |
| 83 | +Run the scenario with `demoSchedule.json`. |
| 84 | + |
| 85 | +``` |
| 86 | +$ LEIOS_UPSTREAM_DB_PATH="$(pwd)/demoUpstream.db" LEIOS_SCHEDULE="$(pwd)/demoSchedule.json" SECONDS_UNTIL_REF_SLOT=5 REF_SLOT=177 CLUSTER_RUN_DATA="${CONSENSUS_REPO_DIR}/nix/leios-mvd" CARDANO_NODE=$CARDANO_NODE IMMDB_SERVER=$IMMDB_SERVER ${CONSENSUS_REPO_DIR}/scripts/leios-demo/leios-october-demo.sh |
| 87 | +$ # wait about ~20 seconds before stopping the execution by pressing any key |
| 88 | +``` |
| 89 | + |
| 90 | +## Analysis |
| 91 | + |
| 92 | +Compare and contrast the `latency_ms` column for the rows with a slot that's after the reference slot 177. |
| 93 | +The first few such ros (ie those within a couple seconds of the reference slot) seem to often also be disrupted, because the initial bulk syncing to catch up to the reference slot presumably leaves the node in a disrupted state for a short interval. |
| 94 | + |
| 95 | +**WARNING**. |
| 96 | +Each execution consumes about 0.5 gigabytes of disk. |
| 97 | +The script announces where (eg `Temporary data stored at: /run/user/1000/leios-october-demo.c5Wmxc`), so you can delete each run's data when necessary. |
| 98 | + |
| 99 | +**INFO**. |
| 100 | +If you don't see any data in the 'Extracted and Merged Data Summary' table, then check the log files in the run's temporary directory. |
| 101 | +This is where you might see messages about, eg, the missing `genesis-*.json` files, bad syntax in the `demoSchedule.json` file, etc. |
| 102 | + |
| 103 | +# Details about the demo components |
| 104 | + |
| 105 | +## The topology |
| 106 | + |
| 107 | +For this first iteration, the demo topology is a simple linear graph. |
| 108 | + |
| 109 | +```mermaid |
| 110 | +flowchart TD |
| 111 | + MockedUpstreamPeer --> Node0 --> MockedDownstreamPeer |
| 112 | +``` |
| 113 | + |
| 114 | +**INFO**. |
| 115 | +In this iteration of the demo, the mocked downstream peer (see section below) is simply another node, ie Node1. |
| 116 | + |
| 117 | +## The Praos traffic and Leios traffic |
| 118 | + |
| 119 | +In this iteration of the demo, the data and traffic is very simple. |
| 120 | + |
| 121 | +- The Praos data is a simple chain provided by the Performance&Tracing team. |
| 122 | +- The mocked upstream peer serves each Praos block when the mocked wall-clock reaches the onset of their slots. |
| 123 | +- The Leios data is ten 12.5 megabyte EBs. |
| 124 | + They use the minimal number of txs necessary in order to accumulate 12.5 megabytes in order to minimize the CPU&heap overhead of the patched-in Leios logic, since this iteration of trhe demo is primarily intended to focus on networking. |
| 125 | +- The mocked upstream peer serves those EBs just prior to the onset of one of the Praos block's slot, akin to (relatively minor) ATK-LeiosProtocolBurst attack. |
| 126 | + Thus, the patched nodes are under significant Leios load when that Praos block begins diffusing. |
| 127 | + |
| 128 | +## The demo tool |
| 129 | + |
| 130 | +The `cabal run exe:leiosdemo202510 -- generate ...` command generates a SQLite database with the following schema. |
| 131 | + |
| 132 | +``` |
| 133 | +CREATE TABLE ebPoints ( |
| 134 | + ebSlot INTEGER NOT NULL |
| 135 | + , |
| 136 | + ebHashBytes BLOB NOT NULL |
| 137 | + , |
| 138 | + ebId INTEGER NOT NULL |
| 139 | + , |
| 140 | + PRIMARY KEY (ebSlot, ebHashBytes) |
| 141 | + ) WITHOUT ROWID; |
| 142 | +CREATE TABLE ebTxs ( |
| 143 | + ebId INTEGER NOT NULL -- foreign key ebPoints.ebId |
| 144 | + , |
| 145 | + txOffset INTEGER NOT NULL |
| 146 | + , |
| 147 | + txHashBytes BLOB NOT NULL -- raw bytes |
| 148 | + , |
| 149 | + txBytesSize INTEGER NOT NULL |
| 150 | + , |
| 151 | + txBytes BLOB -- valid CBOR |
| 152 | + , |
| 153 | + PRIMARY KEY (ebId, txOffset) |
| 154 | + ) WITHOUT ROWID; |
| 155 | +``` |
| 156 | + |
| 157 | +The contents of the generated database are determine by the given `manifest.json` file. |
| 158 | +For now, see the `demoManifest.json` file for the primary schema: each "`txRecipe`" is simply the byte size of the transaction. |
| 159 | + |
| 160 | +The `generate` subcommand also generates a default `schedule.json`. |
| 161 | +Each EB will have two array elements in the schedule. |
| 162 | +The first number in an array element is a fractional slot, which determines when the mocked upstream peer will offer the payload. |
| 163 | +The rest of the array element is `MsgLeiosBlockOffer` if the EB's byte size is listed or `MsgLeiosBlockTxsOffer` if `null` is listed. |
| 164 | + |
| 165 | +The secondary schema of the manifest allows for EBs to overlap (which isn't necessary for this demo, despite the pathced node fully supporting it). |
| 166 | +Overlap is created by an alternative "`txRecipe`", an object `{"share": "XYZ", "startIncl": 90, "stopExcl": 105}` where `"nickname": "XYZ"` was included in a preceding _source_ EB recipe. |
| 167 | +The `"startIncl`" and `"stopExcl"` are inclusive and exclusive indices into the source EB (aka a left-closed right-open interval); `"stopExcl"` is optional and defaults to the length of the source EB. |
| 168 | +With this `"share"` syntax, it is possible for an EB to include the same tx multiple times. |
| 169 | +That would not be a well-formed EB, but the prototype's behavior in response to such an EB is undefined---it's fine for the prototype to simply assume all the Leios EBs and txs in their closures are well-formed. |
| 170 | +(TODO check for this one, since it's easy to check for---just in the patched node itself, or also in `generate`?) |
| 171 | + |
| 172 | +## The mocked upstream peer |
| 173 | + |
| 174 | +The mocked upstream peer is a patched variant of `immdb-server`. |
| 175 | + |
| 176 | +- It runs incomplete variants of LeiosNotify and LeiosFetch: just EBs and EB closures, nothing else (no EB announcements, no votes, no range requests). |
| 177 | +- It serves the EBs present in the given `--leios-db`; it sends Leios notificaitons offering the data according to the given `--leios-schedule`. |
| 178 | + See the demo tool section above for how to generate those files. |
| 179 | + |
| 180 | +## The patched node/node-under-test |
| 181 | + |
| 182 | +The patched node is a patched variant of `cardano-node`. |
| 183 | +All of the material changes were made in the `ouroboros-consensus` repo; the `cardano-node` changes are merely for integration. |
| 184 | + |
| 185 | +- It runs the same incomplete variants of LeiosNotify and LeiosFetch as the mocked upstream peer. |
| 186 | +- The Leios fetch request logic is a fully fledged first draft, with following primary shortcomings. |
| 187 | + - It only handles EBs and EB closures, not votes and not range requests. |
| 188 | + - It retains a number of heap objects in proportion with the number of txs in EBs it has acquired. |
| 189 | + The real node---and so subsequent iterations of this prototype---must instead keep that data on disk. |
| 190 | + This first draft was intended to do so, but we struggled to invent the fetch logic algorithm with the constraint that some of its state was on-disk; that's currently presumed to be possible, but has been deferred to a subsequent iteration of the prototype. |
| 191 | + - It never discards any information. |
| 192 | + The real node---and so subsequent iterations of this prototype---must instead discard EBs and EB closures once they're old enough, unless they are needed for the immutable chain. |
| 193 | + - Once it decides to fetch a set of txs from an upstream peer for the sake of some EB closure(s), it does not necessarily compose those into an optimal set of requests for that peer. |
| 194 | + We had not identified the potential for an optimizing algorithm here until writing this first prototype, so it just does something straight-forward and naive for now (which might be sufficient even for the real-node---we'll have to investigate later). |
| 195 | + |
| 196 | +There are no other changes. |
| 197 | +In particular, that means the `ouroboros-network` mux doesn't not deprioritize Leios traffic. |
| 198 | +That change is an example of what this first prototype is intended to potentially demonstrate the need for. |
| 199 | +There are many such changes, from small to large. |
| 200 | +Some examples includes the following. |
| 201 | + |
| 202 | +- The prototype uses SQLite3 with entirely default settings. |
| 203 | + Maybe Write-Ahead Log mode would be much preferable, likely need to VACUUM at some point, and so on. |
| 204 | +- The prototype uses a mutex to completely isolate every SQLite3 invocation---that's probably excessive, but was useful for some debugging during initial development (see the Engineering Notes appendix) |
| 205 | +- The prototype chooses several _magic numbers_ for resource utilization limits (eg max bytes per reqeust, max outsanding bytes per peer, fetch decision logic rate-limiting, txCache disk-bandwidth rate-limiting, etc). |
| 206 | + These all ultimately need to be tuned for the intended behvaiors on `mainnet`. |
| 207 | +- The prototype does not deduplicate the storage of EBs' closures when they share txs. |
| 208 | + This decision makes the LeiosFetch server a trivial single-pass instead of a join. |
| 209 | + However, it "wastes" disk space and disk bandwidth. |
| 210 | + It's left to future work to decide whether that's a worthwhile trade-off. |
| 211 | + |
| 212 | +## The mocked downstream node |
| 213 | + |
| 214 | +For simplicity, this is simply another instance of the patched node. |
| 215 | +In the future, it could be comparatively lightweight and moreover could replay an arbitrary schedule of downstream requests, dual to the mocked upstream peer's arbitrary schedule of upstream notifications. |
| 216 | + |
| 217 | +# Appendix: Engineering Notes |
| 218 | + |
| 219 | +This section summarizes some lessons learned during the development of this prototype. |
| 220 | + |
| 221 | +- Hypothesis: A SQLite connection will continue to hold SQLite's internal EXCLUSIVE lock _even after the transaction is COMMITed_ when the write transaction involved a prepared statement that was accidentally not finalized. |
| 222 | + That hypothesis was inferred from a painstaking debugging session, but I haven't not yet confirmed it in isolation. |
| 223 | + The bugfix unsuprisingly amounted to using `bracket` for all prepare/finalize pairs and all BEGIN/COMMIT pairs; thankfully our DB patterns seem to accommodate such bracketing. |
| 224 | +- The SQLite query plan optimizer might need more information in order to be reliable. |
| 225 | + Therefore at least one join (the one that copies out of `txCache` for the EbTxs identified in an in-memory table) was replaced with application-level iteration. |
| 226 | + It's not yet clear whether a one-time ANALYZE call might suffice, for example. |
| 227 | + Even if it did, it's also not yet clear how much bandwidth usage/latency/jitter/etc might be reduced. |
0 commit comments