Skip to content

Commit e669148

Browse files
authored
docs(examples): add tutorial-style README for leios-testnet (#790)
1 parent 1038c66 commit e669148

2 files changed

Lines changed: 227 additions & 2 deletions

File tree

examples/leios-testnet/README.md

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
# Following the Cardano Leios testnet
2+
3+
This example connects a node to the public Cardano **Leios** ("Musashi Dojo")
4+
testnet and follows its endorsement layer live: it negotiates a Leios-capable
5+
handshake, runs ordinary Praos chain-sync, and reacts to **Endorser Block (EB)**
6+
notifications by fetching their bodies and transactions over Pallas's
7+
[`pallas-network2`](../../pallas-network2) stack.
8+
9+
It's a small, single-file program (`src/main.rs`, ~290 lines) meant to be read
10+
top-to-bottom. By the end of this tutorial you'll understand:
11+
12+
- what the Leios overlay is and how it rides on top of Praos;
13+
- how a single handshake version "turns Leios on";
14+
- the `leios-notify` (server-push) vs `leios-fetch` (client-pull) split;
15+
- how to drive `pallas-network2`'s `Manager` / `InitiatorBehavior` event loop.
16+
17+
## What is Leios?
18+
19+
[Leios (CIP-0164)](https://cips.cardano.org/cip/CIP-0164) is an overlay on top
20+
of Ouroboros Praos. Alongside the normal Praos chain, block producers diffuse
21+
**Endorser Blocks** — compact lists of *transaction references* — which are then
22+
voted on and certified back into a ranking block. The goal is higher throughput:
23+
transactions get endorsed in parallel with normal block production.
24+
25+
Pallas speaks this overlay through two node-to-node mini-protocols that ride the
26+
**same** TCP connection as Praos, once a Leios-capable handshake version is
27+
negotiated:
28+
29+
| Mini-protocol | Direction | What it carries | Surfaced as |
30+
| -------------- | -------------- | ------------------------------------------------------- | ------------------------ |
31+
| `leios-notify` | server → push | announces/offers new EBs and their txs, diffuses votes | `InitiatorEvent::EbNotification` |
32+
| `leios-fetch` | client → pull | request an EB body, or a subset of its transactions | `InitiatorEvent::EbFetched` |
33+
34+
The interaction is a notify-then-fetch loop: the relay *tells you* what it has,
35+
and you *pull* the pieces you want.
36+
37+
```
38+
relay ──leios-notify──▶ BlockOffer(eb) "I have EB e"
39+
you ──leios-fetch───▶ FetchEb(e) "send me its body"
40+
relay ──leios-fetch───▶ Block(body) body = { tx_hash => size } map
41+
you (remember tx count)
42+
relay ──leios-notify──▶ BlockTxsOffer(eb) "I can serve e's transactions"
43+
you ──leios-fetch───▶ FetchEbTxs(e, bitmap) "send me these txs"
44+
relay ──leios-fetch───▶ BlockTxs { txs } the actual transactions
45+
```
46+
47+
## Prerequisites
48+
49+
- A Rust toolchain matching the workspace `rust-version`.
50+
- Network access to the public Leios relay.
51+
52+
> **Heads up — this is a throwaway devnet.** The Musashi Dojo testnet is
53+
> continuously reset. If the connection is refused or sync stalls, the relay
54+
> address, network magic, or intersection point in `src/main.rs` may simply be
55+
> stale. Check the
56+
> [Leios testnet getting-started guide](https://leios.cardano-scaling.org/docs/testnet/getting-started/)
57+
> for current values.
58+
59+
## Running it
60+
61+
From the repository root:
62+
63+
```sh
64+
RUST_LOG=info cargo run -p leios-testnet
65+
```
66+
67+
There are no command-line arguments — everything is configured by the constants
68+
at the top of `src/main.rs` (see [Configuration](#configuration)). For more
69+
detail, including per-transaction fetch logging and otherwise-unhandled events,
70+
bump the log level:
71+
72+
```sh
73+
RUST_LOG=debug cargo run -p leios-testnet
74+
```
75+
76+
## Reading the output
77+
78+
A healthy run prints something like this (abridged). Each line corresponds
79+
directly to an event handled in `src/main.rs`:
80+
81+
```
82+
INFO connecting to Leios testnet relay="leios-node.play.dev.cardano.org:3001" magic=164
83+
INFO peer initialized pid=... version=15 leios=true
84+
INFO intersection found pid=... point=Specific(2812236, ...) tip=...
85+
INFO header received pid=... variant=... tip_block=...
86+
INFO EB offered → fetching body pid=... eb=2812240@9d8a... size=1234
87+
INFO EB body fetched pid=... eb=2812240@9d8a... bytes=1234 txs=42
88+
INFO txs offered → fetching pid=... eb=2812240@9d8a... want=42 total=42
89+
INFO EB transactions fetched pid=... eb=2812240@9d8a... count=42 bytes=98765
90+
INFO votes received pid=... count=7
91+
```
92+
93+
What to look for:
94+
95+
- **`peer initialized ... leios=true`** — the handshake negotiated a
96+
Leios-capable version. If you see `leios=false` (plus a warning), the peer
97+
spoke a pre-Leios version and **no EBs will be diffused**.
98+
- **`intersection found` / `header received`** — ordinary Praos chain-sync,
99+
running underneath the overlay.
100+
- **`EB offered``EB body fetched``txs offered` → `EB transactions
101+
fetched`** — the notify-then-fetch loop in action.
102+
- **`votes received`** — Leios votes diffused inline over `leios-notify`.
103+
104+
## How it works
105+
106+
Everything lives in the `LeiosNode` struct in `src/main.rs`.
107+
108+
### 1. Turning Leios on (the handshake)
109+
110+
This is the only thing that "enables" Leios. The default `InitiatorBehavior`
111+
proposes only a mainnet v13 handshake, which does **not** carry the overlay. The
112+
example swaps in a version table that proposes v11–v15 with the testnet's
113+
network magic, so the peer can negotiate v15 (`LEIOS_MIN_VERSION`, the Dijkstra
114+
era) and bring up `leios-notify` / `leios-fetch`:
115+
116+
```rust
117+
let behavior = InitiatorBehavior {
118+
handshake: HandshakeBehavior::new(HandshakeConfig {
119+
supported_version: VersionTable::v11_and_above_with_query(
120+
LEIOS_TESTNET_MAGIC, // 164
121+
false,
122+
),
123+
}),
124+
..Default::default() // chain-sync, block-fetch, keepalive stay at defaults
125+
};
126+
```
127+
128+
### 2. The event loop
129+
130+
`tick()` uses `tokio::select!` to multiplex two sources: a 3-second housekeeping
131+
timer (which drives `InitiatorCommand::Housekeeping`, keeping the protocols
132+
pumping) and the network's event stream (`Manager::poll_next`). Every event is
133+
dispatched to `handle_event`:
134+
135+
```rust
136+
select! {
137+
_ = self.housekeeping_interval.tick() => {
138+
self.network.execute(InitiatorCommand::Housekeeping);
139+
}
140+
evt = self.network.poll_next() => {
141+
if let Some(evt) = evt { self.handle_event(evt); }
142+
}
143+
}
144+
```
145+
146+
### 3. Praos chain-sync, underneath
147+
148+
`IntersectionFound`, `BlockHeaderReceived`, and `RollbackReceived` are ordinary
149+
Praos events. The example just logs them and asks for more with
150+
`InitiatorCommand::ContinueSync`. Note that chain-sync is started near the tip
151+
(via an intersection point) rather than from origin — the Leios overlay diffuses
152+
EBs over the same connection regardless of where you are in chain-sync.
153+
154+
### 4. Reacting to notifications (`handle_notification`)
155+
156+
This is the heart of the example. Each `leios-notify` notification triggers the
157+
appropriate `leios-fetch` pull:
158+
159+
- **`BlockOffer(eb_id, size)`**`InitiatorCommand::FetchEb(pid, eb_id)`. We
160+
pull the body first, because the body tells us how many transactions the EB
161+
has — which we need to request them correctly later.
162+
- **`BlockTxsOffer(eb_id)`**`InitiatorCommand::FetchEbTxs(pid, eb_id, bitmap)`,
163+
but **only** if we already fetched that EB's body (so we know its tx count).
164+
The transactions are selected with a bitmap, capped at `MAX_TXS_PER_FETCH`
165+
(64) per request:
166+
167+
```rust
168+
let want = n.min(MAX_TXS_PER_FETCH);
169+
self.network.execute(InitiatorCommand::FetchEbTxs(
170+
pid, eb_id, leiosfetch::Bitmaps::all(want),
171+
));
172+
```
173+
174+
Two subtleties worth understanding:
175+
- We only request transactions a peer has **offered**. Requesting txs a peer
176+
hasn't offered makes the prototype relay reset the connection.
177+
- Each request is bounded to one 64-tx bitmap window. Asking for a whole large
178+
EB at once can exceed the relay's per-response limits; a real client would
179+
page across windows.
180+
181+
- **`BlockAnnouncement(raw)`** and **`Votes(votes)`** are simply logged.
182+
183+
### 5. Sizing the transaction request
184+
185+
How do we know how many transactions an EB has? The EB body is a CBOR
186+
`{ tx_hash => size }` map, so the number of entries *is* the transaction count.
187+
`eb_tx_count` decodes the map header (handling both definite- and
188+
indefinite-length encodings) and the result is stashed in the `eb_tx_counts`
189+
map, keyed by `EbId`, so it's ready when the peer later offers the transactions.
190+
191+
## Configuration
192+
193+
All knobs are constants at the top of `src/main.rs`. Because the testnet resets
194+
periodically, expect to update these from time to time:
195+
196+
| Constant | Default | When to change |
197+
| ---------------------- | -------------------------------------- | ----------------------------------------------------------- |
198+
| `LEIOS_RELAY` | `leios-node.play.dev.cardano.org:3001` | Connection refused / relay moved — check the docs. |
199+
| `LEIOS_TESTNET_MAGIC` | `164` | If the testnet's network magic changes. |
200+
| `INTERSECT_SLOT` / `INTERSECT_HASH` | slot `2812236`, hash `9d8a43aa…` | Sync stalls / intersection not found — use a current point. |
201+
| `MAX_TXS_PER_FETCH` | `64` | Tune how many txs to pull per `leios-fetch` request. |
202+
203+
## Troubleshooting
204+
205+
- **Connection refused** — the relay address is likely stale (the devnet was
206+
reset). Get the current address from the
207+
[getting-started guide](https://leios.cardano-scaling.org/docs/testnet/getting-started/)
208+
and update `LEIOS_RELAY`.
209+
- **Sync stalls / "intersection not found"** — the chain was reset past your
210+
`INTERSECT_SLOT`/`INTERSECT_HASH`. Replace them with a current point.
211+
- **`peer negotiated a pre-Leios version`** — you connected, but the peer only
212+
speaks pre-v15; no EBs will be diffused. Confirm you're hitting a Leios relay.
213+
214+
## Going further
215+
216+
- The mini-protocol types live in
217+
[`pallas_network2::protocol::{leiosnotify, leiosfetch}`](../../pallas-network2/src/protocol);
218+
the initiator event loop is in
219+
[`pallas_network2::behavior::initiator`](../../pallas-network2/src/behavior).
220+
- Ideas to extend this example: persist fetched EBs and transactions, page
221+
across **all** transaction windows of a large EB (not just the first 64),
222+
decode and follow the diffused votes, or connect to multiple relays at once.
223+
- Background reading:
224+
[CIP-0164](https://cips.cardano.org/cip/CIP-0164) and the
225+
[Leios testnet docs](https://leios.cardano-scaling.org/docs/testnet/getting-started/).

examples/leios-testnet/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ const LEIOS_TESTNET_MAGIC: u64 = 164;
6262
///
6363
/// The Musashi testnet resets periodically; if sync stalls or the intersection
6464
/// is not found, replace this with a current point from the chain.
65-
const INTERSECT_SLOT: u64 = 2724123;
66-
const INTERSECT_HASH: &str = "daea7e4b9754244bceb713b269771756450d2fe19ee551f7cb24ba65fa841aeb";
65+
const INTERSECT_SLOT: u64 = 2812236;
66+
const INTERSECT_HASH: &str = "9d8a43aa5ddfa5e2e379ad14b38c3edf98cb6898ed480726fec9da9b68aa3d0e";
6767

6868
/// How many of an EB's transactions to fetch per request. We only request txs a
6969
/// peer has offered (so it holds the closure), and bound each request to one

0 commit comments

Comments
 (0)