Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[submodule "cardano-blueprint"]
path = cardano-blueprint
url = https://github.com/cardano-scaling/cardano-blueprint.git
branch = leios-prototype
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ members = [
"examples/p2p-discovery",
"examples/p2p-responder",
"examples/p2p-initiator",
"examples/leios-testnet",
]

[workspace.dependencies]
Expand Down
2 changes: 1 addition & 1 deletion cardano-blueprint
Submodule cardano-blueprint updated 75 files
+12 −0 .github/CODEOWNERS
+34 −0 .github/workflows/formatting.yaml
+1 −1 .github/workflows/link-check-config.json
+1 −1 .github/workflows/pages.yaml
+53 −0 .github/workflows/sync-ledger-cddls.yaml
+28 −0 .github/workflows/validate-cddl.yaml
+0 −3 .gitignore
+15 −0 .mdbook/custom.css
+78 −6 CONTRIBUTING.md
+13 −59 README.md
+2 −7 book.toml
+61 −0 flake.lock
+89 −18 flake.nix
+15 −7 src/SUMMARY.md
+4 −4 src/client/README.md
+4 −4 src/client/node-to-client/README.md
+21 −13 src/client/node-to-client/state-query/README.md
+43 −23 src/codecs/base.cddl
+43 −37 src/consensus/README.md
+8 −11 src/consensus/chainsel.md
+3 −2 src/consensus/chainvalid.md
+3 −3 src/consensus/forging.md
+10 −5 src/consensus/multiera.md
+3 −3 src/introduction/README.md
+47 −4 src/ledger/README.md
+33 −23 src/ledger/block-validation.md
+0 −5 src/ledger/cddls.md
+94 −0 src/ledger/concepts/blocks.md
+42 −0 src/ledger/concepts/determinism.md
+3 −0 src/ledger/concepts/transactions.md
+57 −0 src/ledger/constraints/header-body-split.md
+348 −0 src/ledger/eras/allegra.cddl
+533 −0 src/ledger/eras/alonzo.cddl
+572 −0 src/ledger/eras/babbage.cddl
+752 −0 src/ledger/eras/conway.cddl
+824 −0 src/ledger/eras/dijkstra.cddl
+358 −0 src/ledger/eras/mary.cddl
+319 −0 src/ledger/eras/shelley.cddl
+57 −0 src/ledger/plan.md
+44 −0 src/ledger/state-transition.md
+3 −0 src/ledger/state-transition/reading-specs.md
+115 −0 src/ledger/state-transition/validity.md
+14 −10 src/ledger/transaction-fee.md
+1 −1 src/logbook.md
+119 −42 src/mempool/README.md
+17 −7 src/network/README.md
+23 −21 src/network/mini-protocols.md
+13 −13 src/network/multiplexing.md
+23 −21 src/network/node-to-node/blockfetch/README.md
+2 −0 src/network/node-to-node/blockfetch/block.cddl
+3 −3 src/network/node-to-node/blockfetch/messages.cddl
+28 −31 src/network/node-to-node/chainsync/README.md
+8 −2 src/network/node-to-node/chainsync/header.cddl
+2 −2 src/network/node-to-node/chainsync/messages.cddl
+28 −27 src/network/node-to-node/handshake/README.md
+14 −7 src/network/node-to-node/keep-alive/README.md
+83 −0 src/network/node-to-node/leios-fetch/README.md
+42 −0 src/network/node-to-node/leios-fetch/messages.cddl
+82 −0 src/network/node-to-node/leios-notify/README.md
+31 −0 src/network/node-to-node/leios-notify/messages.cddl
+26 −29 src/network/node-to-node/txsubmission2/README.md
+2 −2 src/network/node-to-node/txsubmission2/messages.cddl
+8 −2 src/network/node-to-node/txsubmission2/tx.cddl
+8 −2 src/network/node-to-node/txsubmission2/txid.cddl
+4 −4 src/plutus/README.md
+14 −11 src/plutus/builtin.md
+96 −71 src/plutus/cek.md
+35 −0 src/plutus/serialization.md
+81 −0 src/principles/README.md
+12 −1 src/storage/README.md
+3 −3 src/storage/cardano-node-chaindb/README.md
+15 −13 src/styleguide.md
+28 −0 test/README.md
+94 −0 test/validate-cddl.bats
+13 −0 treefmt.toml
17 changes: 17 additions & 0 deletions examples/leios-testnet/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "leios-testnet"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
publish = false

[package.metadata.release]
release = false

[dependencies]
pallas-network2 = { path = "../../pallas-network2" }
pallas-codec = { path = "../../pallas-codec" }
hex = "0.4.3"
tokio = { version = "1.27.0", features = ["rt-multi-thread", "macros", "time"] }
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
290 changes: 290 additions & 0 deletions examples/leios-testnet/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
//! Connect a node to the Cardano **Leios** ("Musashi Dojo") testnet and follow
//! its endorsement layer.
//!
//! Leios (CIP-0164) is an overlay on top of Ouroboros Praos: alongside the
//! normal Praos chain, producers diffuse **Endorser Blocks (EBs)** — compact
//! lists of transaction references — which are then voted on and certified back
//! into a ranking block. Pallas speaks this overlay through two node-to-node
//! mini-protocols that ride the *same* connection as Praos once a Leios-capable
//! handshake version is negotiated:
//!
//! - **leios-notify** (server-push): the relay announces / offers new EBs and
//! their transactions, and diffuses full votes inline. Surfaced as
//! [`InitiatorEvent::EbNotification`].
//! - **leios-fetch** (client-pull): we request the EB body or a subset of its
//! transactions. Surfaced as [`InitiatorEvent::EbFetched`].
//!
//! The only thing that "turns Leios on" is the handshake: we must propose an
//! N2N version `>= LEIOS_MIN_VERSION` (v15, the Dijkstra era) with the testnet's
//! network magic. The default [`InitiatorBehavior`] only proposes a mainnet v13
//! handshake, so this example swaps in a v15-capable version table.
//!
//! Run with:
//!
//! ```sh
//! RUST_LOG=info cargo run -p leios-testnet
//! ```

use std::collections::HashMap;
use std::time::Duration;

use pallas_codec::minicbor::{Decoder, data::Type};
use pallas_network2::{
Manager, PeerId,
behavior::{
AnyMessage,
initiator::{
Config as HandshakeConfig, HandshakeBehavior, InitiatorBehavior, InitiatorCommand,
InitiatorEvent,
},
},
interface::TcpInterface,
protocol::{
EbId, Point,
handshake::n2n::{LEIOS_MIN_VERSION, VersionTable},
leiosfetch, leiosnotify,
},
};
use tokio::{select, time::Interval};

/// Public bootstrap relay for the Leios "Musashi Dojo" testnet.
///
/// This is a throwaway, continuously-resetting devnet — if the connection is
/// refused, check <https://leios.cardano-scaling.org/docs/testnet/getting-started/>
/// for the current relay address and network magic.
const LEIOS_RELAY: &str = "leios-node.play.dev.cardano.org:3001";

/// Network magic for the Leios testnet (a nod to CIP-0164).
const LEIOS_TESTNET_MAGIC: u64 = 164;

/// Chain-sync intersection point to start following from, so we sync near the
/// tip instead of replaying the chain from origin.
///
/// The Musashi testnet resets periodically; if sync stalls or the intersection
/// is not found, replace this with a current point from the chain.
const INTERSECT_SLOT: u64 = 2724123;
const INTERSECT_HASH: &str = "daea7e4b9754244bceb713b269771756450d2fe19ee551f7cb24ba65fa841aeb";

/// How many of an EB's transactions to fetch per request. We only request txs a
/// peer has offered (so it holds the closure), and bound each request to one
/// 64-tx bitmap window — requesting a whole large EB at once can exceed the
/// relay's per-response limits. A real client pages across windows as needed.
const MAX_TXS_PER_FETCH: usize = 64;

struct LeiosNode {
network: Manager<TcpInterface<AnyMessage>, InitiatorBehavior, AnyMessage>,
housekeeping_interval: Interval,
/// Transaction count per EB, learned by decoding the EB body. Used to size a
/// correct tx bitmap when the peer later offers that EB's transactions.
eb_tx_counts: HashMap<EbId, usize>,
}

impl LeiosNode {
fn new() -> Self {
let interface = TcpInterface::new();

// The default behavior only proposes a mainnet v13 handshake, which does
// not carry the Leios overlay. Swap in a version table that proposes
// v11..=v15 with the testnet magic so the peer can negotiate v15 and
// enable leios-notify / leios-fetch. The rest of the behavior (chain-sync,
// block-fetch, keepalive, ...) is left at its defaults.
let behavior = InitiatorBehavior {
handshake: HandshakeBehavior::new(HandshakeConfig {
supported_version: VersionTable::v11_and_above_with_query(
LEIOS_TESTNET_MAGIC,
false,
),
}),
..Default::default()
};

let network = Manager::new(interface, behavior);

Self {
network,
housekeeping_interval: tokio::time::interval(Duration::from_secs(3)),
eb_tx_counts: HashMap::new(),
}
}

fn handle_event(&mut self, event: InitiatorEvent) {
match event {
InitiatorEvent::PeerInitialized(pid, (version, _data)) => {
let leios = version >= LEIOS_MIN_VERSION;
tracing::info!(%pid, version, leios, "peer initialized");
if !leios {
tracing::warn!(
%pid,
version,
min = LEIOS_MIN_VERSION,
"peer negotiated a pre-Leios version; no EBs will be diffused"
);
}
}

// --- Praos chain-sync (runs underneath Leios) ---
InitiatorEvent::IntersectionFound(pid, point, tip) => {
tracing::info!(%pid, ?point, tip = %fmt_eb(&tip.0), "intersection found");
self.network.execute(InitiatorCommand::ContinueSync(pid));
}
InitiatorEvent::BlockHeaderReceived(pid, header, tip) => {
tracing::info!(%pid, variant = header.variant, tip_block = tip.1, "header received");
self.network.execute(InitiatorCommand::ContinueSync(pid));
}
InitiatorEvent::RollbackReceived(pid, point, tip) => {
tracing::warn!(%pid, ?point, tip_block = tip.1, "rollback received");
self.network.execute(InitiatorCommand::ContinueSync(pid));
}

// --- Leios: server-pushed announcements / offers (leios-notify) ---
InitiatorEvent::EbNotification(pid, notification) => {
self.handle_notification(pid, notification);
}

// --- Leios: pulled bodies / txs (leios-fetch) ---
InitiatorEvent::EbFetched(pid, eb, response) => match response {
leiosfetch::Response::Block(body) => {
// The EB body is a `{ tx_hash => size }` map; its entry count
// is the EB's transaction count. Remember it so we can size a
// correct tx bitmap once the peer offers the transactions.
let n = eb_tx_count(body.raw_bytes());
tracing::info!(%pid, eb = %fmt_eb(&eb), bytes = body.raw_bytes().len(), txs = n, "EB body fetched");
self.eb_tx_counts.insert(eb, n);
}
leiosfetch::Response::BlockTxs { txs } => {
let total: usize = txs.iter().map(|tx| tx.raw_bytes().len()).sum();
tracing::info!(%pid, eb = %fmt_eb(&eb), count = txs.len(), bytes = total, "EB transactions fetched");
for (i, tx) in txs.iter().enumerate() {
tracing::debug!(%pid, index = i, bytes = tx.raw_bytes().len(), " tx");
}
}
},

other => {
tracing::debug!(?other, "unhandled event");
}
}
}

fn handle_notification(&mut self, pid: PeerId, notification: leiosnotify::Notification) {
match notification {
leiosnotify::Notification::BlockOffer(eb_id, size) => {
tracing::info!(%pid, eb = %fmt_eb(&eb_id), size, "EB offered → fetching body");
// Pull the body to learn the EB's tx count; we fetch the txs
// later, when the peer offers them (`BlockTxsOffer`).
self.network.execute(InitiatorCommand::FetchEb(pid, eb_id));
}
leiosnotify::Notification::BlockAnnouncement(raw) => {
tracing::info!(%pid, bytes = raw.raw_bytes().len(), "EB announced");
}
leiosnotify::Notification::BlockTxsOffer(eb_id) => {
// The peer signals it holds this EB's transaction closure, so it
// can serve a BlockTxsRequest. Requesting txs a peer has NOT
// offered makes the prototype relay reset the connection, so we
// only fetch in response to this offer, sized from the body's tx
// count (learned when we fetched the body).
match self.eb_tx_counts.get(&eb_id).copied() {
Some(n) if n > 0 => {
let want = n.min(MAX_TXS_PER_FETCH);
tracing::info!(%pid, eb = %fmt_eb(&eb_id), want, total = n, "txs offered → fetching");
self.network.execute(InitiatorCommand::FetchEbTxs(
pid,
eb_id,
leiosfetch::Bitmaps::all(want),
));
}
_ => {
tracing::info!(%pid, eb = %fmt_eb(&eb_id), "txs offered (body not yet fetched)");
}
}
}
leiosnotify::Notification::Votes(votes) => {
tracing::info!(%pid, count = votes.len(), "votes received");
}
}
}

async fn tick(&mut self) {
select! {
_ = self.housekeeping_interval.tick() => {
tracing::debug!("housekeeping tick");
self.network.execute(InitiatorCommand::Housekeeping);
}
evt = self.network.poll_next() => {
if let Some(evt) = evt {
self.handle_event(evt);
}
}
}
}

async fn run_forever(&mut self) {
loop {
self.tick().await;
}
}
Comment thread
scarmuega marked this conversation as resolved.
}

/// Formats an EB reference (`[slot, hash]`) for logging.
fn fmt_eb(eb: &Point) -> String {
match eb {
Point::Origin => "origin".to_string(),
Point::Specific(slot, hash) => format!("{slot}@{}", hex::encode(hash)),
}
}

/// Counts the transactions in an EB body, which is a `{ tx_hash => size }` CBOR
/// map — the number of entries is the transaction count.
fn eb_tx_count(body: &[u8]) -> usize {
let mut d = Decoder::new(body);
match d.map() {
Ok(Some(n)) => n as usize,
// Indefinite-length map: count key/value pairs until the break marker.
Ok(None) => {
let mut n = 0;
while !matches!(d.datatype(), Ok(Type::Break)) {
if d.skip().is_err() || d.skip().is_err() {
break;
}
n += 1;
}
n
}
Err(_) => 0,
}
}

#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();

let mut node = LeiosNode::new();

let peer = LEIOS_RELAY
.parse()
.expect("LEIOS_RELAY should be a valid host:port");

tracing::info!(
relay = LEIOS_RELAY,
magic = LEIOS_TESTNET_MAGIC,
"connecting to Leios testnet"
);

node.network.execute(InitiatorCommand::IncludePeer(peer));
// Start chain-sync near the tip (not origin) so we follow live blocks without
// replaying the whole chain. The Leios overlay diffuses EBs over the same
// connection independently of where we are in chain-sync.
let intersect = Point::Specific(
INTERSECT_SLOT,
hex::decode(INTERSECT_HASH).expect("INTERSECT_HASH should be valid hex"),
);
node.network
.execute(InitiatorCommand::StartSync(vec![intersect]));

node.run_forever().await;
}
6 changes: 6 additions & 0 deletions examples/p2p-discovery/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ impl<I: Interface<AnyMessage>> MyNode<I> {
InitiatorEvent::TxRequested(pid, _) => {
tracing::info!(%pid, "tx requested");
}
InitiatorEvent::EbNotification(pid, _) => {
tracing::info!(%pid, "leios notification received");
}
InitiatorEvent::EbFetched(pid, _, _) => {
tracing::info!(%pid, "leios fetch received");
}
}

self.enqueue_next_cmds();
Expand Down
9 changes: 9 additions & 0 deletions examples/p2p-responder/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,15 @@ impl MockResponderNode {
ResponderEvent::TxReceived(pid, _tx) => {
tracing::info!(%pid, "tx received");
}
ResponderEvent::EbNotificationRequested(pid) => {
tracing::info!(%pid, "leios notification requested");
}
ResponderEvent::EbRequested(pid, _) => {
tracing::info!(%pid, "leios eb requested");
}
ResponderEvent::EbTxsRequested(pid, _, _) => {
tracing::info!(%pid, "leios eb txs requested");
}
}
}

Expand Down
14 changes: 14 additions & 0 deletions pallas-codec/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1233,6 +1233,13 @@ pub struct AnyCbor {
}

impl AnyCbor {
/// Wraps already-encoded CBOR bytes verbatim, without re-encoding. Use this
/// to embed a pre-serialized CBOR item; contrast with [`AnyCbor::from_encode`],
/// which encodes a value into CBOR.
pub fn from_raw_bytes(inner: Vec<u8>) -> Self {
Self { inner }
}

pub fn raw_bytes(&self) -> &[u8] {
&self.inner
}
Expand All @@ -1257,6 +1264,13 @@ impl AnyCbor {
}
}

impl From<Vec<u8>> for AnyCbor {
/// Wraps already-encoded CBOR bytes verbatim (see [`AnyCbor::from_raw_bytes`]).
fn from(inner: Vec<u8>) -> Self {
Self { inner }
}
}

impl Deref for AnyCbor {
type Target = Vec<u8>;

Expand Down
3 changes: 2 additions & 1 deletion pallas-network2/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ byteorder.workspace = true
socket2.workspace = true
thiserror.workspace = true
hex.workspace = true
cddl = { version = "0.10", optional = true }

[features]
emulation = ["tokio", "rand"]
default = ["emulation"]
blueprint = []
blueprint = ["dep:cddl"]

[dev-dependencies]
tokio = { version = "1.45.1", features = ["full", "test-util"] }
Expand Down
Loading
Loading