From cb0348b47a897f1b0bd64e3f40c732fc45a95638 Mon Sep 17 00:00:00 2001 From: Santiago Carmuega Date: Tue, 11 Apr 2023 00:51:38 +0200 Subject: [PATCH] refactor: Merge multiplexer & miniprotocols into single crate (#244) --- Cargo.toml | 3 +- examples/block-download/Cargo.toml | 3 + examples/block-download/src/main.rs | 39 +- examples/n2c-miniprotocols/Cargo.toml | 3 + examples/n2c-miniprotocols/src/main.rs | 106 ++--- examples/n2n-miniprotocols/Cargo.toml | 3 + examples/n2n-miniprotocols/src/main.rs | 79 +--- pallas-miniprotocols/.gitignore | 2 - pallas-miniprotocols/Cargo.toml | 25 - pallas-miniprotocols/src/machines.rs | 140 ------ pallas-miniprotocols/src/txmonitor/codec.rs | 155 ------- pallas-miniprotocols/src/txmonitor/mod.rs | 213 --------- pallas-miniprotocols/tests/integration.rs | 242 ---------- pallas-multiplexer/.gitignore | 2 - pallas-multiplexer/Cargo.toml | 28 -- pallas-multiplexer/README.md | 88 ---- pallas-multiplexer/docs/diagram.png | Bin 161277 -> 0 bytes pallas-multiplexer/src/agents.rs | 180 -------- pallas-multiplexer/src/bearers.rs | 187 -------- pallas-multiplexer/src/demux.rs | 67 --- pallas-multiplexer/src/lib.rs | 19 - pallas-multiplexer/src/mux.rs | 60 --- pallas-multiplexer/src/std.rs | 183 -------- pallas-multiplexer/src/sync.rs | 55 --- pallas-multiplexer/tests/integration.rs | 59 --- pallas-network/Cargo.toml | 29 ++ pallas-network/README.md | 3 + pallas-network/src/bearer.rs | 201 ++++++++ pallas-network/src/facades.rs | 139 ++++++ pallas-network/src/lib.rs | 4 + .../src/miniprotocols}/README.md | 0 .../src/miniprotocols}/blockfetch/client.rs | 33 +- .../src/miniprotocols}/blockfetch/codec.rs | 0 .../src/miniprotocols}/blockfetch/mod.rs | 0 .../src/miniprotocols}/blockfetch/protocol.rs | 2 +- .../src/miniprotocols}/chainsync/buffer.rs | 5 +- .../src/miniprotocols}/chainsync/client.rs | 35 +- .../src/miniprotocols}/chainsync/codec.rs | 0 .../src/miniprotocols}/chainsync/mod.rs | 0 .../src/miniprotocols}/chainsync/protocol.rs | 2 +- .../src/miniprotocols}/common.rs | 4 +- .../src/miniprotocols}/handshake/README.md | 0 .../src/miniprotocols}/handshake/client.rs | 30 +- .../src/miniprotocols}/handshake/mod.rs | 0 .../src/miniprotocols}/handshake/n2c.rs | 0 .../src/miniprotocols}/handshake/n2n.rs | 0 .../src/miniprotocols}/handshake/protocol.rs | 5 +- .../src/miniprotocols}/handshake/server.rs | 30 +- .../src/miniprotocols}/localstate/client.rs | 30 +- .../src/miniprotocols}/localstate/codec.rs | 0 .../src/miniprotocols}/localstate/mod.rs | 0 .../src/miniprotocols}/localstate/protocol.rs | 2 +- .../src/miniprotocols}/localstate/queries.rs | 0 .../src/miniprotocols/mod.rs | 2 - .../src/miniprotocols/txmonitor/client.rs | 205 ++++++++ .../src/miniprotocols/txmonitor/codec.rs | 114 +++++ .../src/miniprotocols/txmonitor/mod.rs | 7 + .../src/miniprotocols/txmonitor/protocol.rs | 34 ++ .../src/miniprotocols}/txsubmission/README.md | 0 .../src/miniprotocols}/txsubmission/client.rs | 23 +- .../src/miniprotocols}/txsubmission/codec.rs | 0 .../src/miniprotocols}/txsubmission/mod.rs | 0 .../miniprotocols}/txsubmission/protocol.rs | 5 +- .../src/miniprotocols}/txsubmission/server.rs | 23 +- pallas-network/src/plexer.rs | 317 +++++++++++++ pallas-network/tests/plexer.rs | 62 +++ pallas-network/tests/protocols.rs | 146 ++++++ pallas-upstream/Cargo.toml | 6 +- pallas-upstream/src/api.rs | 126 ----- pallas-upstream/src/blockfetch.rs | 106 ----- pallas-upstream/src/chainsync.rs | 176 ------- pallas-upstream/src/framework.rs | 111 +---- pallas-upstream/src/lib.rs | 12 +- pallas-upstream/src/plexer.rs | 436 ------------------ pallas-upstream/src/worker.rs | 197 ++++++++ pallas-upstream/tests/integration.rs | 54 +-- pallas/Cargo.toml | 3 +- pallas/src/ledger.rs | 10 - pallas/src/lib.rs | 16 +- pallas/src/network.rs | 10 - 80 files changed, 1694 insertions(+), 3002 deletions(-) delete mode 100644 pallas-miniprotocols/.gitignore delete mode 100644 pallas-miniprotocols/Cargo.toml delete mode 100644 pallas-miniprotocols/src/machines.rs delete mode 100644 pallas-miniprotocols/src/txmonitor/codec.rs delete mode 100644 pallas-miniprotocols/src/txmonitor/mod.rs delete mode 100644 pallas-miniprotocols/tests/integration.rs delete mode 100644 pallas-multiplexer/.gitignore delete mode 100644 pallas-multiplexer/Cargo.toml delete mode 100644 pallas-multiplexer/README.md delete mode 100644 pallas-multiplexer/docs/diagram.png delete mode 100644 pallas-multiplexer/src/agents.rs delete mode 100644 pallas-multiplexer/src/bearers.rs delete mode 100644 pallas-multiplexer/src/demux.rs delete mode 100644 pallas-multiplexer/src/lib.rs delete mode 100644 pallas-multiplexer/src/mux.rs delete mode 100644 pallas-multiplexer/src/std.rs delete mode 100644 pallas-multiplexer/src/sync.rs delete mode 100644 pallas-multiplexer/tests/integration.rs create mode 100644 pallas-network/Cargo.toml create mode 100644 pallas-network/README.md create mode 100644 pallas-network/src/bearer.rs create mode 100644 pallas-network/src/facades.rs create mode 100644 pallas-network/src/lib.rs rename {pallas-miniprotocols => pallas-network/src/miniprotocols}/README.md (100%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/blockfetch/client.rs (87%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/blockfetch/codec.rs (100%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/blockfetch/mod.rs (100%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/blockfetch/protocol.rs (89%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/chainsync/buffer.rs (98%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/chainsync/client.rs (90%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/chainsync/codec.rs (100%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/chainsync/mod.rs (100%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/chainsync/protocol.rs (96%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/common.rs (97%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/handshake/README.md (100%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/handshake/client.rs (82%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/handshake/mod.rs (100%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/handshake/n2c.rs (100%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/handshake/n2n.rs (100%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/handshake/protocol.rs (98%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/handshake/server.rs (80%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/localstate/client.rs (88%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/localstate/codec.rs (100%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/localstate/mod.rs (100%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/localstate/protocol.rs (94%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/localstate/queries.rs (100%) rename pallas-miniprotocols/src/lib.rs => pallas-network/src/miniprotocols/mod.rs (81%) create mode 100644 pallas-network/src/miniprotocols/txmonitor/client.rs create mode 100644 pallas-network/src/miniprotocols/txmonitor/codec.rs create mode 100644 pallas-network/src/miniprotocols/txmonitor/mod.rs create mode 100644 pallas-network/src/miniprotocols/txmonitor/protocol.rs rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/txsubmission/README.md (100%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/txsubmission/client.rs (88%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/txsubmission/codec.rs (100%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/txsubmission/mod.rs (100%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/txsubmission/protocol.rs (94%) rename {pallas-miniprotocols/src => pallas-network/src/miniprotocols}/txsubmission/server.rs (88%) create mode 100644 pallas-network/src/plexer.rs create mode 100644 pallas-network/tests/plexer.rs create mode 100644 pallas-network/tests/protocols.rs delete mode 100644 pallas-upstream/src/api.rs delete mode 100644 pallas-upstream/src/blockfetch.rs delete mode 100644 pallas-upstream/src/chainsync.rs delete mode 100644 pallas-upstream/src/plexer.rs create mode 100644 pallas-upstream/src/worker.rs delete mode 100644 pallas/src/ledger.rs delete mode 100644 pallas/src/network.rs diff --git a/Cargo.toml b/Cargo.toml index 2112f4d..3f93671 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,8 +3,7 @@ members = [ "pallas-codec", "pallas-addresses", - "pallas-multiplexer", - "pallas-miniprotocols", + "pallas-network", "pallas-crypto", "pallas-primitives", "pallas-traverse", diff --git a/examples/block-download/Cargo.toml b/examples/block-download/Cargo.toml index a313e3d..0b1c739 100644 --- a/examples/block-download/Cargo.toml +++ b/examples/block-download/Cargo.toml @@ -10,3 +10,6 @@ publish = false pallas = { path = "../../pallas" } net2 = "0.2.37" hex = "0.4.3" +tracing = "0.1.37" +tracing-subscriber = "0.3.16" +tokio = { version = "1.27.0", features = ["rt-multi-thread"] } diff --git a/examples/block-download/src/main.rs b/examples/block-download/src/main.rs index ac848f6..33cabd0 100644 --- a/examples/block-download/src/main.rs +++ b/examples/block-download/src/main.rs @@ -1,38 +1,27 @@ use pallas::network::{ - miniprotocols::{ - blockfetch, - handshake::{self, n2n::VersionTable}, - Point, MAINNET_MAGIC, PROTOCOL_N2N_BLOCK_FETCH, PROTOCOL_N2N_HANDSHAKE, - }, - multiplexer::{bearers::Bearer, StdPlexer}, + facades::PeerClient, + miniprotocols::{Point, MAINNET_MAGIC}, }; -fn main() { - env_logger::init(); +#[tokio::main] +async fn main() { + tracing::subscriber::set_global_default( + tracing_subscriber::FmtSubscriber::builder() + .with_max_level(tracing::Level::TRACE) + .finish(), + ) + .unwrap(); - let bearer = Bearer::connect_tcp("relays-new.cardano-mainnet.iohk.io:3001").unwrap(); - - let mut plexer = StdPlexer::new(bearer); - let handshake = plexer.use_client_channel(PROTOCOL_N2N_HANDSHAKE); - let blockfetch = plexer.use_client_channel(PROTOCOL_N2N_BLOCK_FETCH); - - plexer.muxer.spawn(); - plexer.demuxer.spawn(); - - let versions = VersionTable::v4_and_above(MAINNET_MAGIC); - let mut hs_client = handshake::N2NClient::new(handshake); - let handshake = hs_client.handshake(versions).unwrap(); - - assert!(matches!(handshake, handshake::Confirmation::Accepted(..))); + let mut peer = PeerClient::connect("relays-new.cardano-mainnet.iohk.io:3001", MAINNET_MAGIC) + .await + .unwrap(); let point = Point::Specific( 49159253, hex::decode("d034a2d0e4c3076f57368ed59319010c265718f0923057f8ff914a3b6bfd1314").unwrap(), ); - let mut bf_client = blockfetch::Client::new(blockfetch); - - let block = bf_client.fetch_single(point).unwrap(); + let block = peer.blockfetch().fetch_single(point).await.unwrap(); println!("downloaded block of size: {}", block.len()); println!("{}", hex::encode(&block)); diff --git a/examples/n2c-miniprotocols/Cargo.toml b/examples/n2c-miniprotocols/Cargo.toml index bfd3dcd..4761a11 100644 --- a/examples/n2c-miniprotocols/Cargo.toml +++ b/examples/n2c-miniprotocols/Cargo.toml @@ -11,3 +11,6 @@ pallas = { path = "../../pallas" } net2 = "0.2.37" hex = "0.4.3" log = "0.4.16" +tracing = "0.1.37" +tracing-subscriber = "0.3.16" +tokio = { version = "1.27.0", features = ["rt-multi-thread"] } diff --git a/examples/n2c-miniprotocols/src/main.rs b/examples/n2c-miniprotocols/src/main.rs index f40a140..145617f 100644 --- a/examples/n2c-miniprotocols/src/main.rs +++ b/examples/n2c-miniprotocols/src/main.rs @@ -1,56 +1,37 @@ use pallas::network::{ - miniprotocols::{chainsync, handshake, localstate, Point, MAINNET_MAGIC}, - multiplexer, + facades::NodeClient, + miniprotocols::{chainsync, localstate, Point, MAINNET_MAGIC}, }; +use tracing::info; -#[derive(Debug)] -struct LoggingObserver; - -#[allow(dead_code)] -fn do_handshake(channel: multiplexer::StdChannel) { - let mut client = handshake::N2CClient::new(channel); - - let confirmation = client - .handshake(handshake::n2c::VersionTable::v1_and_above(MAINNET_MAGIC)) - .unwrap(); - - match confirmation { - handshake::Confirmation::Accepted(v, _) => { - log::info!("hand-shake accepted, using version {}", v) - } - handshake::Confirmation::Rejected(x) => { - log::info!("hand-shake rejected with reason {:?}", x) - } - } -} - -#[allow(dead_code)] -fn do_localstate_query(channel: multiplexer::StdChannel) { - let mut client = localstate::ClientV10::new(channel); - client.acquire(None).unwrap(); +async fn do_localstate_query(client: &mut NodeClient) { + client.statequery().acquire(None).await.unwrap(); let result = client + .statequery() .query(localstate::queries::RequestV10::GetSystemStart) + .await .unwrap(); - log::info!("system start result: {:?}", result); + info!("system start result: {:?}", result); } -#[allow(dead_code)] -fn do_chainsync(channel: multiplexer::StdChannel) { +async fn do_chainsync(client: &mut NodeClient) { let known_points = vec![Point::Specific( 43847831u64, hex::decode("15b9eeee849dd6386d3770b0745e0450190f7560e5159b1b3ab13b14b2684a45").unwrap(), )]; - let mut client = chainsync::N2CClient::new(channel); + let (point, _) = client + .chainsync() + .find_intersect(known_points) + .await + .unwrap(); - let (point, _) = client.find_intersect(known_points).unwrap(); - - log::info!("intersected point is {:?}", point); + info!("intersected point is {:?}", point); for _ in 0..10 { - let next = client.request_next().unwrap(); + let next = client.chainsync().request_next().await.unwrap(); match next { chainsync::NextResponse::RollForward(h, _) => { @@ -62,44 +43,29 @@ fn do_chainsync(channel: multiplexer::StdChannel) { } } -fn main() { - env_logger::builder() - .filter_level(log::LevelFilter::Trace) - .init(); +#[tokio::main] +async fn main() { + tracing::subscriber::set_global_default( + tracing_subscriber::FmtSubscriber::builder() + .with_max_level(tracing::Level::TRACE) + .finish(), + ) + .unwrap(); - #[cfg(not(target_family = "unix"))] - { - panic!("can't use n2c unix socket on non-unix systems"); - } // we connect to the unix socket of the local node. Make sure you have the right // path for your environment - #[cfg(target_family = "unix")] - { - use pallas::network::{ - miniprotocols::{ - PROTOCOL_N2C_CHAIN_SYNC, PROTOCOL_N2C_HANDSHAKE, PROTOCOL_N2C_STATE_QUERY, - }, - multiplexer::bearers::Bearer, - }; - let bearer = Bearer::connect_unix("/tmp/node.socket").unwrap(); + let mut client = NodeClient::connect("/tmp/node.socket", MAINNET_MAGIC) + .await + .unwrap(); - // setup the multiplexer by specifying the bearer and the IDs of the - // miniprotocols to use - let mut plexer = multiplexer::StdPlexer::new(bearer); - let handshake = plexer.use_client_channel(PROTOCOL_N2C_HANDSHAKE); - let statequery = plexer.use_client_channel(PROTOCOL_N2C_STATE_QUERY); - let chainsync = plexer.use_client_channel(PROTOCOL_N2C_CHAIN_SYNC); + // execute an arbitrary "Local State" query against the node + do_localstate_query(&mut client).await; - plexer.muxer.spawn(); - plexer.demuxer.spawn(); - - // execute the required handshake against the relay - do_handshake(handshake); - - // execute an arbitrary "Local State" query against the node - do_localstate_query(statequery); - - // execute the chainsync flow from an arbitrary point in the chain - do_chainsync(chainsync); - } + // execute the chainsync flow from an arbitrary point in the chain + do_chainsync(&mut client).await; +} + +#[cfg(not(target_family = "unix"))] +fn main() { + panic!("can't use n2c unix socket on non-unix systems"); } diff --git a/examples/n2n-miniprotocols/Cargo.toml b/examples/n2n-miniprotocols/Cargo.toml index ac04c27..353d2b0 100644 --- a/examples/n2n-miniprotocols/Cargo.toml +++ b/examples/n2n-miniprotocols/Cargo.toml @@ -11,3 +11,6 @@ pallas = { path = "../../pallas" } net2 = "0.2.37" hex = "0.4.3" log = "0.4.16" +tracing = "0.1.37" +tracing-subscriber = "0.3.16" +tokio = { version = "1.27.0", features = ["rt-multi-thread"] } diff --git a/examples/n2n-miniprotocols/src/main.rs b/examples/n2n-miniprotocols/src/main.rs index 304156b..4a7fbc6 100644 --- a/examples/n2n-miniprotocols/src/main.rs +++ b/examples/n2n-miniprotocols/src/main.rs @@ -1,32 +1,10 @@ use pallas::network::{ - miniprotocols::{ - blockfetch, chainsync, handshake, Point, MAINNET_MAGIC, PROTOCOL_N2N_BLOCK_FETCH, - PROTOCOL_N2N_CHAIN_SYNC, PROTOCOL_N2N_HANDSHAKE, - }, - multiplexer::{bearers::Bearer, StdChannel, StdPlexer}, + facades::PeerClient, + miniprotocols::{chainsync, Point, MAINNET_MAGIC}, }; +use tracing::info; -#[derive(Debug)] -struct LoggingObserver; - -fn do_handshake(channel: StdChannel) { - let mut client = handshake::N2NClient::new(channel); - - let confirmation = client - .handshake(handshake::n2n::VersionTable::v7_and_above(MAINNET_MAGIC)) - .unwrap(); - - match confirmation { - handshake::Confirmation::Accepted(v, _) => { - log::info!("hand-shake accepted, using version {}", v) - } - handshake::Confirmation::Rejected(x) => { - log::info!("hand-shake rejected with reason {:?}", x) - } - } -} - -fn do_blockfetch(channel: StdChannel) { +async fn do_blockfetch(peer: &mut PeerClient) { let range = ( Point::Specific( 43847831, @@ -40,29 +18,25 @@ fn do_blockfetch(channel: StdChannel) { ), ); - let mut client = blockfetch::Client::new(channel); - - let blocks = client.fetch_range(range).unwrap(); + let blocks = peer.blockfetch().fetch_range(range).await.unwrap(); for block in blocks { - log::info!("received block of size: {}", block.len()); + info!("received block of size: {}", block.len()); } } -fn do_chainsync(channel: StdChannel) { +async fn do_chainsync(peer: &mut PeerClient) { let known_points = vec![Point::Specific( 43847831u64, hex::decode("15b9eeee849dd6386d3770b0745e0450190f7560e5159b1b3ab13b14b2684a45").unwrap(), )]; - let mut client = chainsync::N2NClient::new(channel); + let (point, _) = peer.chainsync().find_intersect(known_points).await.unwrap(); - let (point, _) = client.find_intersect(known_points).unwrap(); - - log::info!("intersected point is {:?}", point); + info!("intersected point is {:?}", point); for _ in 0..10 { - let next = client.request_next().unwrap(); + let next = peer.chainsync().request_next().await.unwrap(); match next { chainsync::NextResponse::RollForward(h, _) => { @@ -74,31 +48,24 @@ fn do_chainsync(channel: StdChannel) { } } -fn main() { - env_logger::builder() - .filter_level(log::LevelFilter::Info) - .init(); +#[tokio::main] +async fn main() { + tracing::subscriber::set_global_default( + tracing_subscriber::FmtSubscriber::builder() + .with_max_level(tracing::Level::TRACE) + .finish(), + ) + .unwrap(); // setup a TCP socket to act as data bearer between our agents and the remote // relay. - let bearer = Bearer::connect_tcp("relays-new.cardano-mainnet.iohk.io:3001").unwrap(); - - // setup the multiplexer by specifying the bearer and the IDs of the - // miniprotocols to use - let mut plexer = StdPlexer::new(bearer); - let handshake = plexer.use_client_channel(PROTOCOL_N2N_HANDSHAKE); - let blockfetch = plexer.use_client_channel(PROTOCOL_N2N_BLOCK_FETCH); - let chainsync = plexer.use_client_channel(PROTOCOL_N2N_CHAIN_SYNC); - - plexer.muxer.spawn(); - plexer.demuxer.spawn(); - - // execute the required handshake against the relay - do_handshake(handshake); + let mut peer = PeerClient::connect("relays-new.cardano-mainnet.iohk.io:3001", MAINNET_MAGIC) + .await + .unwrap(); // fetch an arbitrary batch of block - do_blockfetch(blockfetch); + do_blockfetch(&mut peer).await; // execute the chainsync flow from an arbitrary point in the chain - do_chainsync(chainsync); + do_chainsync(&mut peer).await; } diff --git a/pallas-miniprotocols/.gitignore b/pallas-miniprotocols/.gitignore deleted file mode 100644 index 96ef6c0..0000000 --- a/pallas-miniprotocols/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/target -Cargo.lock diff --git a/pallas-miniprotocols/Cargo.toml b/pallas-miniprotocols/Cargo.toml deleted file mode 100644 index 7b1815a..0000000 --- a/pallas-miniprotocols/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "pallas-miniprotocols" -description = "Implementation of the Ouroboros network mini-protocols state-machines" -version = "0.18.0" -edition = "2021" -repository = "https://github.com/txpipe/pallas" -homepage = "https://github.com/txpipe/pallas" -documentation = "https://docs.rs/pallas-machines" -license = "Apache-2.0" -readme = "README.md" -authors = [ - "Santiago Carmuega ", - "Pi Lanningham " -] - -[dependencies] -pallas-codec = { version = "0.18.0", path = "../pallas-codec/" } -pallas-multiplexer = { version = "0.18.0", path = "../pallas-multiplexer/" } -hex = "0.4.3" -itertools = "0.10.3" -thiserror = "1.0.31" -tracing = "0.1.37" - -[dev-dependencies] -tokio = { version = "1.27.0", features = ["macros", "rt"] } diff --git a/pallas-miniprotocols/src/machines.rs b/pallas-miniprotocols/src/machines.rs deleted file mode 100644 index ce3f9e8..0000000 --- a/pallas-miniprotocols/src/machines.rs +++ /dev/null @@ -1,140 +0,0 @@ -use pallas_codec::Fragment; -use pallas_multiplexer::agents::{Channel, ChannelBuffer, ChannelError}; -use std::{cell::Cell, fmt::Debug}; -use thiserror::Error; -use tracing::trace; - -#[derive(Debug, Error)] -pub enum MachineError { - #[error("invalid message for state [{0}]: {1}")] - InvalidMsgForState(String, String), - - #[error("channel error communicating with multiplexer: {0}")] - ChannelError(ChannelError), - - #[error("downstream error while processing business logic {0}")] - DownstreamError(Box), -} - -impl MachineError { - pub fn channel(err: ChannelError) -> Self { - Self::ChannelError(err) - } - - pub fn downstream(err: Box) -> Self { - Self::DownstreamError(err) - } - - pub fn invalid_msg(state: &A::State, msg: &A::Message) -> Self { - Self::InvalidMsgForState(format!("{state:?}"), format!("{msg:?}")) - } -} - -pub type Transition = Result; - -pub trait Agent: Sized { - type Message: std::fmt::Debug; - type State: std::fmt::Debug; - - fn state(&self) -> &Self::State; - fn is_done(&self) -> bool; - fn has_agency(&self) -> bool; - fn build_next(&self) -> Self::Message; - fn apply_start(self) -> Transition; - fn apply_outbound(self, msg: Self::Message) -> Transition; - fn apply_inbound(self, msg: Self::Message) -> Transition; -} - -pub struct Runner -where - A: Agent, - C: Channel, -{ - agent: Cell>, - buffer: ChannelBuffer, -} - -impl Runner -where - A: Agent, - A::Message: Fragment + std::fmt::Debug, - C: Channel, -{ - pub fn new(agent: A, channel: C) -> Self { - Self { - agent: Cell::new(Some(agent)), - buffer: ChannelBuffer::new(channel), - } - } - - pub fn start(&mut self) -> Result<(), MachineError> { - let prev = self.agent.take().unwrap(); - let next = prev.apply_start()?; - self.agent.set(Some(next)); - Ok(()) - } - - pub async fn run_step(&mut self) -> Result { - let prev = self.agent.take().unwrap(); - let next = run_agent_step(prev, &mut self.buffer).await?; - let is_done = next.is_done(); - - self.agent.set(Some(next)); - - Ok(is_done) - } - - pub async fn fulfill(mut self) -> Result<(), MachineError> { - self.start()?; - - while self.run_step().await? {} - - Ok(()) - } -} - -pub async fn run_agent_step(agent: A, channel: &mut ChannelBuffer) -> Transition -where - A: Agent, - A::Message: Fragment + std::fmt::Debug, - C: Channel, -{ - match agent.has_agency() { - true => { - let msg = agent.build_next(); - trace!(?msg, "processing outbound msg"); - - channel - .send_msg_chunks(&msg) - .await - .map_err(MachineError::channel)?; - - agent.apply_outbound(msg) - } - false => { - let msg = channel - .recv_full_msg() - .await - .map_err(MachineError::channel)?; - - trace!(?msg, "processing inbound msg"); - - agent.apply_inbound(msg) - } - } -} - -pub async fn run_agent(agent: A, buffer: &mut ChannelBuffer) -> Transition -where - A: Agent, - A::Message: Fragment + std::fmt::Debug, - C: Channel, -{ - let mut agent = agent.apply_start()?; - - while !agent.is_done() { - agent = run_agent_step(agent, buffer).await?; - } - - Ok(agent) -} diff --git a/pallas-miniprotocols/src/txmonitor/codec.rs b/pallas-miniprotocols/src/txmonitor/codec.rs deleted file mode 100644 index 424b857..0000000 --- a/pallas-miniprotocols/src/txmonitor/codec.rs +++ /dev/null @@ -1,155 +0,0 @@ -use super::{MempoolSizeAndCapacity, Message, MsgRequest, MsgResponse}; -use pallas_codec::minicbor::{decode, encode, Decode, Encode, Encoder}; - -impl Encode<()> for Message { - fn encode( - &self, - e: &mut Encoder, - ctx: &mut (), - ) -> Result<(), encode::Error> { - match self { - Message::MsgDone => { - e.array(1)?.u16(0)?; - } - Message::MsgAcquire => { - e.array(1)?.u16(1)?; - } - Message::MsgAcquired(slot) => { - e.array(2)?.u16(2)?; - e.encode(slot)?; - } - Message::MsgQuery(query) => { - query.encode(e, ctx)?; - } - Message::MsgResponse(response) => { - response.encode(e, ctx)?; - } - } - - Ok(()) - } -} - -impl<'b> Decode<'b, ()> for Message { - fn decode( - d: &mut pallas_codec::minicbor::Decoder<'b>, - _ctx: &mut (), - ) -> Result { - d.array()?; - let label = d.u16()?; - - match label { - 0 => Ok(Message::MsgDone), - 1 => Ok(Message::MsgAcquire), - 2 => { - let slot = d.decode()?; - Ok(Message::MsgAcquired(slot)) - } - 3 => Ok(Message::MsgQuery(MsgRequest::MsgRelease)), - 5 => Ok(Message::MsgQuery(MsgRequest::MsgNextTx)), - 6 => { - d.array()?; - let tag: Result = d.u8(); - let mut tx = None; - - if tag.is_ok() { - d.tag()?; - let cbor = d.bytes()?; - tx = Some(hex::encode(cbor)); - } - Ok(Message::MsgResponse(MsgResponse::MsgReplyNextTx(tx))) - } - 7 => { - let txid = d.decode()?; - Ok(Message::MsgQuery(MsgRequest::MsgHasTx(txid))) - } - 8 => { - let has = d.decode()?; - Ok(Message::MsgResponse(MsgResponse::MsgReplyHasTx(has))) - } - 9 => Ok(Message::MsgQuery(MsgRequest::MsgGetSizes)), - 10 => { - d.array()?; - let capacity_in_bytes = d.decode()?; - let size_in_bytes = d.decode()?; - let number_of_txs = d.decode()?; - - Ok(Message::MsgResponse(MsgResponse::MsgReplyGetSizes( - MempoolSizeAndCapacity { - capacity_in_bytes, - size_in_bytes, - number_of_txs, - }, - ))) - } - _ => Err(decode::Error::message("can't decode Message")), - } - } - - fn nil() -> Option { - None - } -} - -impl Encode<()> for MsgRequest { - fn encode( - &self, - e: &mut Encoder, - _ctx: &mut (), - ) -> Result<(), encode::Error> { - match self { - MsgRequest::MsgAwaitAcquire => { - e.array(1)?.u16(1)?; - } - MsgRequest::MsgGetSizes => { - e.array(1)?.u16(9)?; - } - MsgRequest::MsgHasTx(tx) => { - e.array(2)?.u16(7)?; - e.encode(tx)?; - } - MsgRequest::MsgNextTx => { - e.array(1)?.u16(5)?; - } - MsgRequest::MsgRelease => { - e.array(1)?.u16(3)?; - } - } - - Ok(()) - } -} - -impl Encode<()> for MsgResponse { - fn encode( - &self, - e: &mut Encoder, - _ctx: &mut (), - ) -> Result<(), encode::Error> { - match self { - MsgResponse::MsgReplyGetSizes(sz) => { - e.array(2)?.u16(10)?; - e.array(3)?; - e.encode(sz.capacity_in_bytes)?; - e.encode(sz.size_in_bytes)?; - e.encode(sz.number_of_txs)?; - } - MsgResponse::MsgReplyHasTx(tx) => { - e.array(2)?.u16(8)?; - e.encode(tx)?; - } - MsgResponse::MsgReplyNextTx(None) => { - e.array(1)?.u16(6)?; - } - MsgResponse::MsgReplyNextTx(Some(tx)) => { - e.array(2)?.u16(6)?; - e.encode(tx.to_string())?; - } - } - Ok(()) - } - - fn is_nil(&self) -> bool { - false - } -} diff --git a/pallas-miniprotocols/src/txmonitor/mod.rs b/pallas-miniprotocols/src/txmonitor/mod.rs deleted file mode 100644 index a08645a..0000000 --- a/pallas-miniprotocols/src/txmonitor/mod.rs +++ /dev/null @@ -1,213 +0,0 @@ -mod codec; - -use crate::machines::{Agent, MachineError, Transition}; -use pallas_codec::Fragment; -use std::fmt::Debug; - -type Slot = u64; -type TxId = String; -type Tx = String; - -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum StBusyKind { - NextTx, - HasTx, - GetSizes, -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum State { - StIdle, - StAcquiring, - StAcquired, - StBusy(StBusyKind), - StDone, -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct MempoolSizeAndCapacity { - pub capacity_in_bytes: u32, - pub size_in_bytes: u32, - pub number_of_txs: u32, -} - -#[derive(Debug, Clone)] -pub enum Message { - MsgAcquire, - MsgAcquired(Slot), - MsgQuery(MsgRequest), - MsgResponse(MsgResponse), - MsgDone, -} - -#[derive(Debug, Clone)] -pub enum MsgRequest { - MsgAwaitAcquire, - MsgNextTx, - MsgHasTx(TxId), - MsgGetSizes, - MsgRelease, -} -#[derive(Debug, Clone)] -pub enum MsgResponse { - MsgReplyNextTx(Option), - MsgReplyHasTx(bool), - MsgReplyGetSizes(MempoolSizeAndCapacity), -} - -#[derive(Debug, Clone)] -pub struct LocalTxMonitor { - pub state: State, - pub snapshot: Option, - pub request: Option, - pub output: Option, -} - -impl LocalTxMonitor -where - Message: Fragment, -{ - pub fn initial(state: State) -> Self { - Self { - state, - snapshot: None, - request: None, - output: None, - } - } - - fn on_acquired(self, slot: Slot) -> Transition { - Ok(Self { - state: State::StAcquired, - snapshot: Some(slot), - output: None, - ..self - }) - } - - fn on_reply_next_tx(self, tx: Option) -> Transition { - Ok(Self { - output: Some(MsgResponse::MsgReplyNextTx(tx)), - ..self - }) - } - - fn on_reply_has_tx(self, arg: bool) -> Transition { - Ok(Self { - output: Some(MsgResponse::MsgReplyHasTx(arg)), - ..self - }) - } - - fn on_reply_get_size(self, status: MempoolSizeAndCapacity) -> Transition { - Ok(Self { - output: Some(MsgResponse::MsgReplyGetSizes(status)), - ..self - }) - } -} - -impl Agent for LocalTxMonitor -where - Message: Fragment, -{ - type Message = Message; - type State = State; - - fn state(&self) -> &Self::State { - &self.state - } - - fn is_done(&self) -> bool { - self.state == State::StDone - } - - fn has_agency(&self) -> bool { - match &self.state { - State::StIdle => true, - State::StAcquiring => false, - State::StAcquired => true, - State::StBusy(..) => false, - State::StDone => false, - } - } - - fn build_next(&self) -> Self::Message { - match (&self.state, &self.request, &self.output) { - (State::StIdle, None, None) => Message::MsgAcquire, - (State::StAcquired, None, None) => Message::MsgAcquire, - (State::StAcquired, Some(MsgRequest::MsgAwaitAcquire), None) => Message::MsgAcquire, - (State::StAcquired, Some(MsgRequest::MsgNextTx), None) => { - Message::MsgQuery(MsgRequest::MsgNextTx) - } - (State::StAcquired, Some(MsgRequest::MsgHasTx(tx)), None) => { - Message::MsgQuery(MsgRequest::MsgHasTx(tx.clone())) - } - (State::StAcquired, Some(MsgRequest::MsgGetSizes), None) => { - Message::MsgQuery(MsgRequest::MsgGetSizes) - } - (State::StAcquired, None, Some(_)) => Message::MsgAcquire, - (State::StAcquired, Some(req), Some(_)) => Message::MsgQuery(req.to_owned()), - _ => panic!("I do not have agency, don't know what to do"), - } - } - - fn apply_start(self) -> Transition { - Ok(self) - } - - fn apply_outbound(self, msg: Self::Message) -> Transition { - match (self.state, msg) { - (State::StIdle, Message::MsgAcquire) => Ok(Self { - state: State::StAcquiring, - ..self - }), - (State::StAcquired, Message::MsgQuery(MsgRequest::MsgNextTx)) => Ok(Self { - state: State::StBusy(StBusyKind::NextTx), - ..self - }), - (State::StAcquired, Message::MsgQuery(MsgRequest::MsgHasTx(_))) => Ok(Self { - state: State::StBusy(StBusyKind::HasTx), - ..self - }), - - (State::StAcquired, Message::MsgQuery(MsgRequest::MsgGetSizes)) => Ok(Self { - state: State::StBusy(StBusyKind::GetSizes), - ..self - }), - (State::StAcquired, Message::MsgAcquire) => Ok(Self { - state: State::StAcquiring, - ..self - }), - (State::StAcquired, Message::MsgQuery(MsgRequest::MsgRelease)) => Ok(Self { - state: State::StIdle, - ..self - }), - (State::StIdle, Message::MsgDone) => Ok(Self { - state: State::StDone, - ..self - }), - - _ => panic!("PANIC! Cannot match outbound"), - } - } - - fn apply_inbound(self, msg: Self::Message) -> Transition { - match (&self.state, msg) { - (State::StAcquiring, Message::MsgAcquired(s)) => self.on_acquired(s), - ( - State::StBusy(StBusyKind::NextTx), - Message::MsgResponse(MsgResponse::MsgReplyNextTx(tx)), - ) => self.on_reply_next_tx(tx), - ( - State::StBusy(StBusyKind::HasTx), - Message::MsgResponse(MsgResponse::MsgReplyHasTx(arg)), - ) => self.on_reply_has_tx(arg), - ( - State::StBusy(StBusyKind::GetSizes), - Message::MsgResponse(MsgResponse::MsgReplyGetSizes(msc)), - ) => self.on_reply_get_size(msc), - (state, msg) => Err(MachineError::invalid_msg::(state, &msg)), - } - } -} diff --git a/pallas-miniprotocols/tests/integration.rs b/pallas-miniprotocols/tests/integration.rs deleted file mode 100644 index bd7800c..0000000 --- a/pallas-miniprotocols/tests/integration.rs +++ /dev/null @@ -1,242 +0,0 @@ -use pallas_miniprotocols::{ - blockfetch, - chainsync::{self, NextResponse}, - handshake::{self, Confirmation}, - txsubmission::{self, EraTxId, Reply, TxIdAndSize}, - Point, PROTOCOL_N2N_BLOCK_FETCH, PROTOCOL_N2N_CHAIN_SYNC, PROTOCOL_N2N_HANDSHAKE, - PROTOCOL_N2N_TX_SUBMISSION, -}; -use pallas_multiplexer::{bearers::Bearer, StdChannel, StdPlexer}; - -struct N2NChannels { - chainsync: StdChannel, - blockfetch: StdChannel, - txsubmission: StdChannel, -} - -async fn setup_n2n_client_connection() -> N2NChannels { - let bearer = Bearer::connect_tcp("preview-node.world.dev.cardano.org:30002").unwrap(); - let mut plexer = StdPlexer::new(bearer); - - let handshake = plexer.use_channel(PROTOCOL_N2N_HANDSHAKE); - let chainsync = plexer.use_channel(PROTOCOL_N2N_CHAIN_SYNC); - let blockfetch = plexer.use_channel(PROTOCOL_N2N_BLOCK_FETCH); - let txsubmission = plexer.use_channel(PROTOCOL_N2N_TX_SUBMISSION); - - plexer.muxer.spawn(); - plexer.demuxer.spawn(); - - let mut client = handshake::N2NClient::new(handshake); - - let confirmation = client - .handshake(handshake::n2n::VersionTable::v7_and_above(2)) - .await - .unwrap(); - - assert!(matches!(confirmation, Confirmation::Accepted(..))); - - if let Confirmation::Accepted(v, _) = confirmation { - assert!(v >= 7); - } - - N2NChannels { - chainsync, - blockfetch, - txsubmission, - } -} - -#[tokio::test] -#[ignore] -pub async fn chainsync_history_happy_path() { - let N2NChannels { chainsync, .. } = setup_n2n_client_connection().await; - - let known_point = Point::Specific( - 1654413, - hex::decode("7de1f036df5a133ce68a82877d14354d0ba6de7625ab918e75f3e2ecb29771c2").unwrap(), - ); - - let mut client = chainsync::N2NClient::new(chainsync); - - let (point, _) = client - .find_intersect(vec![known_point.clone()]) - .await - .unwrap(); - - assert!(matches!(client.state(), chainsync::State::Idle)); - - match point { - Some(point) => assert_eq!(point, known_point), - None => panic!("expected point"), - } - - let next = client.request_next().await.unwrap(); - - match next { - NextResponse::RollBackward(point, _) => assert_eq!(point, known_point), - _ => panic!("expected rollback"), - } - - assert!(matches!(client.state(), chainsync::State::Idle)); - - for _ in 0..10 { - let next = client.request_next().await.unwrap(); - - match next { - NextResponse::RollForward(_, _) => (), - _ => panic!("expected roll-forward"), - } - - assert!(matches!(client.state(), chainsync::State::Idle)); - } - - client.send_done().await.unwrap(); - - assert!(matches!(client.state(), chainsync::State::Done)); -} - -#[tokio::test] -#[ignore] -pub async fn chainsync_tip_happy_path() { - let N2NChannels { chainsync, .. } = setup_n2n_client_connection().await; - - let mut client = chainsync::N2NClient::new(chainsync); - - client.intersect_tip().await.unwrap(); - - assert!(matches!(client.state(), chainsync::State::Idle)); - - let next = client.request_next().await.unwrap(); - - assert!(matches!(next, NextResponse::RollBackward(..))); - - let mut await_count = 0; - - for _ in 0..4 { - let next = if client.has_agency() { - client.request_next().await.unwrap() - } else { - await_count += 1; - client.recv_while_must_reply().await.unwrap() - }; - - match next { - NextResponse::RollForward(_, _) => (), - NextResponse::Await => (), - _ => panic!("expected roll-forward or await"), - } - } - - assert!(await_count > 0, "tip was never reached"); - - client.send_done().await.unwrap(); - - assert!(matches!(client.state(), chainsync::State::Done)); -} - -#[tokio::test] -#[ignore] -pub async fn blockfetch_happy_path() { - let N2NChannels { blockfetch, .. } = setup_n2n_client_connection().await; - - let known_point = Point::Specific( - 1654413, - hex::decode("7de1f036df5a133ce68a82877d14354d0ba6de7625ab918e75f3e2ecb29771c2").unwrap(), - ); - - let mut client = blockfetch::Client::new(blockfetch); - - let range_ok = client - .request_range((known_point.clone(), known_point)) - .await; - - assert!(matches!(client.state(), blockfetch::State::Streaming)); - - assert!(matches!(range_ok, Ok(_))); - - for _ in 0..1 { - let next = client.recv_while_streaming().await.unwrap(); - - match next { - Some(body) => assert_eq!(body.len(), 3251), - _ => panic!("expected block body"), - } - - assert!(matches!(client.state(), blockfetch::State::Streaming)); - } - - let next = client.recv_while_streaming().await.unwrap(); - - assert!(matches!(next, None)); - - client.send_done().await.unwrap(); - - assert!(matches!(client.state(), blockfetch::State::Done)); -} - -#[tokio::test] -#[ignore] -pub async fn txsubmission_server_happy_path() { - // TODO(pi): Note that the below doesn't work; we need a node to connect *to us* - // during the integration test which seems awkward; - // Alternatively, we can just set up both a client and server connecting to - // themselves for testing! - - let N2NChannels { txsubmission, .. } = setup_n2n_client_connection().await; - - let mut server = txsubmission::Server::new(txsubmission); - - assert!(matches!(server.wait_for_init().await, Ok(_))); - - assert!(matches!( - server.acknowledge_and_request_tx_ids(false, 0, 3).await, - Ok(_) - )); - - let reply: Result<_, _> = server.receive_next_reply().await; - assert!(matches!(reply, Ok(Reply::TxIds(_)))); - let Ok(Reply::TxIds(tx_ids)) = reply else { unreachable!() }; - - assert!(tx_ids.len() <= 3); - - assert!(matches!( - server - .request_txs( - tx_ids - .into_iter() - .map(|txid: TxIdAndSize| txid.0) - .collect() - ) - .await, - Ok(_) - )); - - let reply = server.receive_next_reply().await; - assert!(matches!(reply, Ok(Reply::Txs(_)))); - let Ok(Reply::Txs(first_txs)) = reply else { unreachable!() }; - - assert!(matches!( - server.acknowledge_and_request_tx_ids(false, 1, 3).await, - Ok(_) - )); - - let reply = server.receive_next_reply().await; - assert!(matches!(reply, Ok(Reply::Txs(_)))); - let Ok(Reply::Txs(second_txs)) = reply else { unreachable!() }; - - // Make sure we receive the second and third tx again, indicating we sent the - // `acknowledge 1` bit correctly - assert_eq!(second_txs[0], first_txs[1]); - assert_eq!(second_txs[1], first_txs[2]); - - assert!(matches!( - server.acknowledge_and_request_tx_ids(true, 3, 3).await, - Ok(_) - )); - - match server.receive_next_reply().await { - Ok(Reply::Done) => (), // Server aint havin none of our sh*t - Ok(Reply::TxIds(tx_ids)) => assert_eq!(tx_ids.len(), 3), - Ok(_) | Err(_) => unreachable!(), - } -} diff --git a/pallas-multiplexer/.gitignore b/pallas-multiplexer/.gitignore deleted file mode 100644 index 96ef6c0..0000000 --- a/pallas-multiplexer/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/target -Cargo.lock diff --git a/pallas-multiplexer/Cargo.toml b/pallas-multiplexer/Cargo.toml deleted file mode 100644 index e3772c7..0000000 --- a/pallas-multiplexer/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "pallas-multiplexer" -description = "Multithreaded Ouroboros multiplexer implementation using mpsc channels" -version = "0.18.0" -edition = "2021" -repository = "https://github.com/txpipe/pallas" -homepage = "https://github.com/txpipe/pallas" -documentation = "https://docs.rs/pallas-multiplexer" -license = "Apache-2.0" -readme = "README.md" -authors = ["Santiago Carmuega "] - -[dependencies] -pallas-codec = { version = "0.18.0", path = "../pallas-codec/" } -log = "0.4.14" -byteorder = "1.4.3" -hex = "0.4.3" -rand = "0.8.4" -thiserror = "1.0.31" -tracing = "0.1.37" - -[features] -std = [] -sync = [] -default = ["std", "sync"] - -[dev-dependencies] -tokio = { version = "1.27.0", features = ["macros", "rt"] } diff --git a/pallas-multiplexer/README.md b/pallas-multiplexer/README.md deleted file mode 100644 index b9fd7da..0000000 --- a/pallas-multiplexer/README.md +++ /dev/null @@ -1,88 +0,0 @@ -# Pallas Multiplexer - -This is an implementation of the Ouroboros multiplexer logic as defined in the [The Shelley Networking Protocol](https://hydra.iohk.io/build/1070091/download/1/network.pdf#chapter.3) specs. - -## Architectural Decisions - -The following architectural decisions were made for this particular Rust implementation: - -- each mini-protocol state machine should be able to work in its own thread -- a bounded queue should serve as buffer to decouple mini-protocol logic from multiplexer work -- the implementation should pipelining-friendly, even if we don't have a current use-case -- the multiplexer should be agnostic of the mini-protocols implementation details. - -## Implementation Details - -Given the above definitions, Rust's _mpsc channels_ seem like the correct artifact to orchestrate the communication between the different threads in the multiplexer process. - -The following diagram provides an overview of the components involved: - -![Multiplexer Diagram](docs/diagram.png) - -## Usage - -The following code provides a very rough example of how to setup a client that connects to a node and spawns two concurrent threads running independently, both communication over the same bearer using _Pallas_ multiplexer. - -```rust -// Setup a new bearer. In this case, we use a unix socket to connect -// to a node running on the local machine. -let bearer = UnixStream::connect("/tmp/pallas").unwrap(); - -// Setup a new multiplexer using the created bearer and a specification -// of the mini-protocol IDs that we'll be using for our session. In this case, we -// pass id #0 (handshake) and #2 (chainsync). -let muxer = Multiplexer::setup(tcp, &[0, 2]) - -// Ask the multiplexer to provide us with the channel for the miniprotocol #0. -let mut handshake = muxer.use_client_channel(PROTOCOL_N2N_HANDSHAKE); - -// Spawn a thread and pass the ownership of the channel. -thread::spawn(move || { - // Deconstruct the channel to get a handle for sending data into the muxer - // ingress and a handle to receive data from the demuxer egress. - let Channel(mux_tx, demux_rx) = handshake; - - // Do something with the channel. In this case, we just keep sending - // dumb data every 50 millis. - loop { - let payload = vec![1; 65545]; - tx.send(payload).unwrap(); - thread::sleep(Duration::from_millis(50)); - } -}); - -// Ask the multiplexer to provide us with the channel for the chainsync miniprotocol. -let mut chainsync = muxer.use_client_channel(PROTOCOL_N2N_CHAINSYNC); - -// Spawn a different thread and pass the ownership of the 2nd channel. -thread::spawn(move || { - // Deconstruct the channel to get a handle for sending data into the muxer - // ingress and a handle to receive data from the demuxer egress. - let Channel(mux_tx, demux_rx) = chainsync; - - // Do something with the channel. In this case, we just print in stdout - // whatever get received for this mini-protocol. - loop { - let payload = rx.recv().unwrap(); - println!("id:{protocol}, length:{}", payload.len()); - } -}); -``` - -## Run Examples - -For a working example of a two peers communicating (a sender and a listener), check the [examples folder](examples). To run the examples, open two different terminals and run a different peer in each one: - -```sh -# on terminal 1, start the listener -RUST_LOG=info cargo run --example listener -``` - -```sh -# on terminal 2, start the sender -RUST_LOG=info cargo run --example sender -``` - -## Real World Usage - -For a more complex, real-world example, check the [Oura](https://github.com/txpipe/oura) repo, it provides a full-blown client tool designed to live-stream block data from a local or remote node. \ No newline at end of file diff --git a/pallas-multiplexer/docs/diagram.png b/pallas-multiplexer/docs/diagram.png deleted file mode 100644 index b91aabc95c2361d84ded5bad9a3b5c26057dcdda..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 161277 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�^2o0sQj7~c!Bgyj=eGMnF< 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 GkN*$o4%gWL diff --git a/pallas-multiplexer/src/agents.rs b/pallas-multiplexer/src/agents.rs deleted file mode 100644 index 64121d0..0000000 --- a/pallas-multiplexer/src/agents.rs +++ /dev/null @@ -1,180 +0,0 @@ -//! Interface to interact with the multiplexer as an agent - -use crate::Payload; -use pallas_codec::{minicbor, Fragment}; -use thiserror::Error; -use tracing::{debug, error, trace}; - -#[derive(Debug, Error)] -pub enum ChannelError { - #[error("channel is not connected, failed to send payload")] - NotConnected(Option), - - #[error("failure encoding message into CBOR")] - Encoding(String), - - #[error("failure decoding message from CBOR")] - Decoding(String), -} - -/// A raw link to the ingress / egress of the multiplexer -pub trait Channel { - async fn enqueue_chunk(&mut self, chunk: Payload) -> Result<(), ChannelError>; - async fn dequeue_chunk(&mut self) -> Result; -} - -/// Protocol value that defines max segment length -pub const MAX_SEGMENT_PAYLOAD_LENGTH: usize = 65535; - -fn try_decode_message(buffer: &mut Vec) -> Result, ChannelError> -where - M: Fragment, -{ - let mut decoder = minicbor::Decoder::new(buffer); - let maybe_msg = decoder.decode(); - - match maybe_msg { - Ok(msg) => { - let pos = decoder.position(); - buffer.drain(0..pos); - Ok(Some(msg)) - } - Err(err) if err.is_end_of_input() => Ok(None), - Err(err) => { - error!(?err); - error!("{}", hex::encode(buffer)); - Err(ChannelError::Decoding(err.to_string())) - } - } -} - -/// A channel abstraction to hide the complexity of partial payloads -pub struct ChannelBuffer { - channel: C, - temp: Vec, -} - -impl ChannelBuffer { - pub fn new(channel: C) -> Self { - Self { - channel, - temp: Vec::new(), - } - } - - /// Enqueues a msg as a sequence payload chunks - pub async fn send_msg_chunks(&mut self, msg: &M) -> Result<(), ChannelError> - where - M: Fragment, - { - let mut payload = Vec::new(); - minicbor::encode(msg, &mut payload) - .map_err(|err| ChannelError::Encoding(err.to_string()))?; - - let chunks = payload.chunks(MAX_SEGMENT_PAYLOAD_LENGTH); - - for chunk in chunks { - self.channel.enqueue_chunk(Vec::from(chunk)).await?; - } - - Ok(()) - } - - /// Reads from the channel until a complete message is found - pub async fn recv_full_msg(&mut self) -> Result - where - M: Fragment, - { - if !self.temp.is_empty() { - if let Some(msg) = try_decode_message::(&mut self.temp)? { - debug!("decoding done"); - return Ok(msg); - } - } - - loop { - let chunk = self.channel.dequeue_chunk().await?; - self.temp.extend(chunk); - - if let Some(msg) = try_decode_message::(&mut self.temp)? { - debug!("decoding done"); - return Ok(msg); - } - - trace!("not enough data"); - } - } - - pub fn unwrap(self) -> C { - self.channel - } -} - -impl From for ChannelBuffer { - fn from(channel: C) -> Self { - ChannelBuffer::new(channel) - } -} - -#[cfg(test)] -mod tests { - use std::collections::VecDeque; - - use super::*; - - impl Channel for VecDeque { - async fn enqueue_chunk(&mut self, chunk: Payload) -> Result<(), ChannelError> { - self.push_back(chunk); - Ok(()) - } - - async fn dequeue_chunk(&mut self) -> Result { - let chunk = self.pop_front().ok_or(ChannelError::NotConnected(None))?; - Ok(chunk) - } - } - - #[tokio::test] - async fn multiple_messages_in_same_payload() { - let mut input = Vec::new(); - let in_part1 = (1u8, 2u8, 3u8); - let in_part2 = (6u8, 5u8, 4u8); - - minicbor::encode(in_part1, &mut input).unwrap(); - minicbor::encode(in_part2, &mut input).unwrap(); - - let mut channel = VecDeque::::new(); - channel.push_back(input); - - let mut buf = ChannelBuffer::new(channel); - - let out_part1 = buf.recv_full_msg::<(u8, u8, u8)>().await.unwrap(); - let out_part2 = buf.recv_full_msg::<(u8, u8, u8)>().await.unwrap(); - - assert_eq!(in_part1, out_part1); - assert_eq!(in_part2, out_part2); - } - - #[tokio::test] - async fn fragmented_message_in_multiple_payloads() { - let mut input = Vec::new(); - let msg = (11u8, 12u8, 13u8, 14u8, 15u8, 16u8, 17u8); - minicbor::encode(msg, &mut input).unwrap(); - - let mut channel = VecDeque::::new(); - - while !input.is_empty() { - let chunk = Vec::from(input.drain(0..2).as_slice()); - channel.push_back(chunk); - } - - let mut buf = ChannelBuffer::new(channel); - - let out_msg = buf - .recv_full_msg::<(u8, u8, u8, u8, u8, u8, u8)>() - .await - .unwrap(); - - assert_eq!(msg, out_msg); - } -} diff --git a/pallas-multiplexer/src/bearers.rs b/pallas-multiplexer/src/bearers.rs deleted file mode 100644 index 64d1d20..0000000 --- a/pallas-multiplexer/src/bearers.rs +++ /dev/null @@ -1,187 +0,0 @@ -use byteorder::{ByteOrder, NetworkEndian, WriteBytesExt}; -use std::io::{Read, Write}; -use std::net::{SocketAddr, TcpListener, ToSocketAddrs}; -use std::{net::TcpStream, time::Instant}; -use tracing::{debug, event_enabled, trace}; - -use crate::Payload; - -#[cfg(target_family = "unix")] -use std::os::unix::net::UnixStream; -use std::time::Duration; - -pub struct Segment { - pub protocol: u16, - pub timestamp: u32, - pub payload: Payload, -} -impl Segment { - pub fn new(clock: Instant, protocol: u16, payload: Payload) -> Self { - Segment { - timestamp: clock.elapsed().as_micros() as u32, - protocol, - payload, - } - } -} - -fn write_segment(writer: &mut impl Write, segment: Segment) -> Result<(), std::io::Error> { - let Segment { - timestamp, - protocol, - payload, - } = segment; - - let mut msg = Vec::new(); - msg.write_u32::(timestamp)?; - msg.write_u16::(protocol)?; - msg.write_u16::(payload.len() as u16)?; - msg.write_all(&payload)?; - - if event_enabled!(tracing::Level::TRACE) { - trace!( - protocol, - length = payload.len(), - message = hex::encode(&msg), - "writing segment" - ); - } - - writer.write_all(&msg)?; - writer.flush() -} - -fn read_segment(reader: &mut impl Read) -> Result { - let mut header = [0u8; 8]; - - reader.read_exact(&mut header)?; - - if event_enabled!(tracing::Level::TRACE) { - trace!(header = hex::encode(header), "segment header read"); - } - - let length = NetworkEndian::read_u16(&header[6..]) as usize; - let protocol = NetworkEndian::read_u16(&header[4..6]) as usize ^ 0x8000; - let timestamp = NetworkEndian::read_u32(&header[0..4]); - - debug!(protocol, timestamp, length, "parsed inbound msg"); - - let mut payload = vec![0u8; length]; - reader.read_exact(&mut payload)?; - - if event_enabled!(tracing::Level::TRACE) { - trace!(payload = hex::encode(&payload), "segment payload read"); - } - - Ok(Segment { - protocol: protocol as u16, - timestamp, - payload, - }) -} - -// This snippet will be useful if we want to switch TCP streams into -// non-blocking mode, but that's not likely (if we want async, we'll probably go -// with Tokio instead of a handcrafted approach). -/* -fn read_segment_with_timeout(reader: &mut impl Read) -> Result, std::io::Error> { - match read_segment(reader) { - Ok(s) => Ok(Some(s)), - Err(err) => match err.kind() { - std::io::ErrorKind::WouldBlock => Ok(None), - std::io::ErrorKind::TimedOut => Ok(None), - std::io::ErrorKind::Interrupted => Ok(None), - _ => Err(err), - }, - } -} - */ - -#[derive(Debug)] -pub enum Bearer { - Tcp(TcpStream), - - #[cfg(target_family = "unix")] - Unix(UnixStream), -} - -impl Bearer { - pub fn connect_tcp(addr: A) -> Result { - let bearer = TcpStream::connect(addr)?; - bearer.set_nodelay(true)?; - - Ok(Bearer::Tcp(bearer)) - } - - pub fn connect_tcp_timeout( - addr: &SocketAddr, - timeout: Duration, - ) -> Result { - let bearer = TcpStream::connect_timeout(addr, timeout)?; - bearer.set_nodelay(true)?; - - Ok(Bearer::Tcp(bearer)) - } - - pub fn accept_tcp(server: TcpListener) -> Result<(Self, SocketAddr), std::io::Error> { - let (bearer, remote_addr) = server.accept().unwrap(); - bearer.set_nodelay(true)?; - - Ok((Bearer::Tcp(bearer), remote_addr)) - } - - #[cfg(target_family = "unix")] - pub fn connect_unix>(path: P) -> Result { - let bearer = UnixStream::connect(path)?; - - Ok(Bearer::Unix(bearer)) - } - - pub fn read_segment(&mut self) -> Result, std::io::Error> { - match self { - Bearer::Tcp(s) => { - // std tcp streams won't be supporting timeout / async. We don't handle - // specific timeout-related errors, these will remain unhandled and bubble up - // to the consumer lib. The Option wrapper is here just for compatiblity with - // other future bearers that might support timeouts - read_segment(s).map(Some) - } - - #[cfg(target_family = "unix")] - Bearer::Unix(s) => read_segment(s).map(Some), - } - } - - pub fn write_segment(&mut self, segment: Segment) -> Result<(), std::io::Error> { - match self { - Bearer::Tcp(s) => write_segment(s, segment), - - #[cfg(target_family = "unix")] - Bearer::Unix(s) => write_segment(s, segment), - } - } -} - -impl From for Bearer { - fn from(stream: TcpStream) -> Self { - Bearer::Tcp(stream) - } -} - -#[cfg(target_family = "unix")] -impl From for Bearer { - fn from(stream: UnixStream) -> Self { - Bearer::Unix(stream) - } -} - -impl Clone for Bearer { - fn clone(&self) -> Self { - match self { - Bearer::Tcp(s) => Bearer::Tcp(s.try_clone().expect("error cloning tcp stream")), - - #[cfg(target_family = "unix")] - Bearer::Unix(s) => Bearer::Unix(s.try_clone().expect("error cloning unix stream")), - } - } -} diff --git a/pallas-multiplexer/src/demux.rs b/pallas-multiplexer/src/demux.rs deleted file mode 100644 index ce9cf2e..0000000 --- a/pallas-multiplexer/src/demux.rs +++ /dev/null @@ -1,67 +0,0 @@ -use std::collections::HashMap; - -use crate::{bearers::Bearer, Payload}; - -pub struct EgressError(pub Payload); - -pub trait Egress { - fn send(&mut self, payload: Payload) -> Result<(), EgressError>; -} - -pub enum DemuxError { - BearerError(std::io::Error), - EgressDisconnected(u16, Payload), - EgressUnknown(u16, Payload), -} - -pub enum TickOutcome { - Busy, - Idle, -} - -/// A demuxer that reads from a bearer into the corresponding egress -pub struct Demuxer { - bearer: Bearer, - egress: HashMap, -} - -impl Demuxer -where - E: Egress, -{ - pub fn new(bearer: Bearer) -> Self { - Demuxer { - bearer, - egress: Default::default(), - } - } - - pub fn register(&mut self, id: u16, tx: E) { - self.egress.insert(id, tx); - } - - pub fn unregister(&mut self, id: u16) -> Option { - self.egress.remove(&id) - } - - fn dispatch(&mut self, protocol: u16, payload: Payload) -> Result<(), DemuxError> { - match self.egress.get_mut(&protocol) { - Some(tx) => match tx.send(payload) { - Err(EgressError(p)) => Err(DemuxError::EgressDisconnected(protocol, p)), - Ok(_) => Ok(()), - }, - None => Err(DemuxError::EgressUnknown(protocol, payload)), - } - } - - pub fn tick(&mut self) -> Result { - match self.bearer.read_segment() { - Err(err) => Err(DemuxError::BearerError(err)), - Ok(None) => Ok(TickOutcome::Idle), - Ok(Some(segment)) => match self.dispatch(segment.protocol, segment.payload) { - Err(err) => Err(err), - Ok(()) => Ok(TickOutcome::Busy), - }, - } - } -} diff --git a/pallas-multiplexer/src/lib.rs b/pallas-multiplexer/src/lib.rs deleted file mode 100644 index dbad307..0000000 --- a/pallas-multiplexer/src/lib.rs +++ /dev/null @@ -1,19 +0,0 @@ -#![feature(async_fn_in_trait)] - -pub mod agents; -pub mod bearers; -pub mod demux; -pub mod mux; - -#[cfg(feature = "std")] -mod std; - -#[cfg(feature = "sync")] -pub mod sync; - -#[cfg(feature = "std")] -pub use crate::std::*; - -pub type Payload = Vec; - -pub type Message = (u16, Payload); diff --git a/pallas-multiplexer/src/mux.rs b/pallas-multiplexer/src/mux.rs deleted file mode 100644 index 4d7f779..0000000 --- a/pallas-multiplexer/src/mux.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::time::{Duration, Instant}; - -use crate::{ - bearers::{Bearer, Segment}, - Message, -}; - -pub enum IngressError { - Disconnected, - Empty, -} - -/// Source of payloads for a particular protocol -/// -/// To be implemented by any mechanism that allows to submit a payloads from a -/// particular protocol that need to be muxed by the multiplexer. -pub trait Ingress { - fn recv_timeout(&mut self, duration: Duration) -> Result; -} - -pub enum TickOutcome { - BearerError(std::io::Error), - IngressDisconnected, - Idle, - Busy, -} - -pub struct Muxer { - bearer: Bearer, - ingress: I, - clock: Instant, -} - -impl Muxer -where - I: Ingress, -{ - pub fn new(bearer: Bearer, ingress: I) -> Self { - Self { - bearer, - ingress, - clock: Instant::now(), - } - } - - pub fn tick(&mut self) -> TickOutcome { - match self.ingress.recv_timeout(Duration::from_millis(1)) { - Ok((id, payload)) => { - let segment = Segment::new(self.clock, id, payload); - - match self.bearer.write_segment(segment) { - Err(err) => TickOutcome::BearerError(err), - _ => TickOutcome::Busy, - } - } - Err(IngressError::Empty) => TickOutcome::Idle, - Err(IngressError::Disconnected) => TickOutcome::IngressDisconnected, - } - } -} diff --git a/pallas-multiplexer/src/std.rs b/pallas-multiplexer/src/std.rs deleted file mode 100644 index 7806683..0000000 --- a/pallas-multiplexer/src/std.rs +++ /dev/null @@ -1,183 +0,0 @@ -use crate::{ - agents::{self, ChannelBuffer}, - bearers::Bearer, - demux, mux, Message, Payload, -}; - -use std::{ - sync::{ - atomic::{AtomicBool, Ordering}, - mpsc::{channel, Receiver, RecvTimeoutError, SendError, Sender}, - Arc, - }, - thread::{spawn, JoinHandle}, - time::Duration, -}; - -pub type StdIngress = Receiver; - -impl mux::Ingress for StdIngress { - fn recv_timeout(&mut self, duration: Duration) -> Result { - match Receiver::recv_timeout(self, duration) { - Ok(x) => Ok(x), - Err(RecvTimeoutError::Disconnected) => Err(mux::IngressError::Disconnected), - Err(RecvTimeoutError::Timeout) => Err(mux::IngressError::Empty), - } - } -} - -pub type StdEgress = Sender; - -impl demux::Egress for StdEgress { - fn send(&mut self, payload: Payload) -> Result<(), demux::EgressError> { - match Sender::send(self, payload) { - Ok(_) => Ok(()), - Err(SendError(p)) => Err(demux::EgressError(p)), - } - } -} - -pub struct StdPlexer { - pub muxer: mux::Muxer, - pub demuxer: demux::Demuxer, - pub mux_tx: Sender, -} - -const PROTOCOL_SERVER_BIT: u16 = 0x8000; - -impl StdPlexer { - pub fn new(bearer: Bearer) -> Self { - let (mux_tx, mux_rx) = channel::(); - - Self { - muxer: mux::Muxer::new(bearer.clone(), mux_rx), - demuxer: demux::Demuxer::new(bearer), - mux_tx, - } - } - - pub fn use_channel(&mut self, protocol: u16) -> StdChannel { - let (demux_tx, demux_rx) = channel::(); - self.demuxer.register(protocol, demux_tx); - - let mux_tx = self.mux_tx.clone(); - - (protocol, mux_tx, demux_rx) - } - - /// Use the client-side channel for a given protocol - /// Explicitly unsets the most significant bit, forcing use of the client - /// side channel - pub fn use_client_channel(&mut self, protocol: u16) -> StdChannel { - self.use_channel(protocol & !PROTOCOL_SERVER_BIT) - } - - /// Use the server-side channel for a given protocol - /// Explicitly sets the most significant bit, forcing use of the server side - /// channel - pub fn use_server_channel(&mut self, protocol: u16) -> StdChannel { - self.use_channel(protocol | PROTOCOL_SERVER_BIT) - } -} - -impl mux::Muxer { - pub fn block(&mut self, cancel: Cancel) -> Result<(), std::io::Error> { - loop { - match self.tick() { - mux::TickOutcome::BearerError(err) => return Err(err), - mux::TickOutcome::Idle => match cancel.is_set() { - true => break Ok(()), - false => (), - }, - mux::TickOutcome::Busy => (), - mux::TickOutcome::IngressDisconnected => break Ok(()), - } - } - } - - pub fn spawn(mut self) -> Loop { - let cancel = Cancel::default(); - let cancel2 = cancel.clone(); - let thread = spawn(move || self.block(cancel2)); - - Loop { cancel, thread } - } -} - -impl demux::Demuxer { - pub fn block(&mut self, cancel: Cancel) -> Result<(), std::io::Error> { - loop { - match self.tick() { - Ok(demux::TickOutcome::Busy) => (), - Ok(demux::TickOutcome::Idle) => match cancel.is_set() { - true => break Ok(()), - false => (), - }, - Err(demux::DemuxError::BearerError(err)) => return Err(err), - Err(demux::DemuxError::EgressDisconnected(id, _)) => { - log::warn!("disconnected protocol {}", id) - } - Err(demux::DemuxError::EgressUnknown(id, _)) => { - log::warn!("unknown protocol {}", id) - } - } - } - } - - pub fn spawn(mut self) -> Loop { - let cancel = Cancel::default(); - let cancel2 = cancel.clone(); - let thread = spawn(move || self.block(cancel2)); - - Loop { cancel, thread } - } -} - -pub type StdChannel = (u16, Sender, Receiver); - -pub type StdChannelBuffer = ChannelBuffer; - -impl agents::Channel for StdChannel { - async fn enqueue_chunk(&mut self, payload: Payload) -> Result<(), agents::ChannelError> { - match self.1.send((self.0, payload)) { - Ok(_) => Ok(()), - Err(SendError((_, payload))) => Err(agents::ChannelError::NotConnected(Some(payload))), - } - } - - async fn dequeue_chunk(&mut self) -> Result { - match self.2.recv() { - Ok(payload) => Ok(payload), - Err(_) => Err(agents::ChannelError::NotConnected(None)), - } - } -} - -#[derive(Clone, Debug, Default)] -pub struct Cancel(Arc); - -impl Cancel { - pub fn set(&self) { - self.0.store(true, Ordering::SeqCst); - } - - pub fn is_set(&self) -> bool { - self.0.load(Ordering::SeqCst) - } -} - -#[derive(Debug)] -pub struct Loop { - cancel: Cancel, - thread: JoinHandle>, -} - -impl Loop { - pub fn cancel(&self) { - self.cancel.set(); - } - - pub fn join(self) -> Result<(), std::io::Error> { - self.thread.join().unwrap() - } -} diff --git a/pallas-multiplexer/src/sync.rs b/pallas-multiplexer/src/sync.rs deleted file mode 100644 index 3d4ff94..0000000 --- a/pallas-multiplexer/src/sync.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::{ - agents::{self, ChannelBuffer}, - bearers::{Bearer, Segment}, - Payload, -}; - -use std::time::Instant; - -pub struct SyncPlexer { - bearer: Bearer, - protocol: u16, - clock: Instant, -} - -impl SyncPlexer { - pub fn new(bearer: Bearer, protocol: u16) -> Self { - Self { - bearer, - protocol, - clock: Instant::now(), - } - } - - pub fn unwrap(self) -> Bearer { - self.bearer - } -} - -pub type SyncChannel = ChannelBuffer; - -impl agents::Channel for SyncPlexer { - async fn enqueue_chunk(&mut self, payload: Payload) -> Result<(), agents::ChannelError> { - let segment = Segment::new(self.clock, self.protocol, payload); - - self.bearer - .write_segment(segment) - .map_err(|_| agents::ChannelError::NotConnected(None)) - } - - async fn dequeue_chunk(&mut self) -> Result { - match self.bearer.read_segment() { - Ok(segment) => match segment { - Some(x) => { - assert_eq!( - x.protocol, self.protocol, - "sync plexer received payload for wrong protocol" - ); - Ok(x.payload) - } - None => Err(agents::ChannelError::NotConnected(None)), - }, - Err(_) => Err(agents::ChannelError::NotConnected(None)), - } - } -} diff --git a/pallas-multiplexer/tests/integration.rs b/pallas-multiplexer/tests/integration.rs deleted file mode 100644 index 0d46fee..0000000 --- a/pallas-multiplexer/tests/integration.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::{ - net::{Ipv4Addr, SocketAddrV4, TcpListener}, - thread::{self, JoinHandle}, -}; - -use log::info; -use pallas_multiplexer::{agents::Channel, bearers::Bearer, StdPlexer}; -use rand::{distributions::Uniform, Rng}; - -fn setup_passive_muxer() -> JoinHandle { - thread::spawn(|| { - let server = TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, P)).unwrap(); - info!("listening for connections on port {}", P); - - let (bearer, _) = Bearer::accept_tcp(server).unwrap(); - - StdPlexer::new(bearer) - }) -} - -fn setup_active_muxer() -> JoinHandle { - thread::spawn(|| { - let bearer = Bearer::connect_tcp(SocketAddrV4::new(Ipv4Addr::LOCALHOST, P)).unwrap(); - - StdPlexer::new(bearer) - }) -} - -fn random_payload(size: usize) -> Vec { - let range = Uniform::from(0..255); - rand::thread_rng().sample_iter(&range).take(size).collect() -} - -#[tokio::test] -async fn one_way_small_sequence_of_payloads() { - let passive = setup_passive_muxer::<50301>(); - - // HACK: a small sleep seems to be required for Github actions runner to - // formally expose the port - thread::sleep(std::time::Duration::from_secs(1)); - - let active = setup_active_muxer::<50301>(); - - let mut active_plexer = active.join().unwrap(); - let mut passive_plexer = passive.join().unwrap(); - - let mut sender_channel = active_plexer.use_client_channel(0x0003u16); - let mut receiver_channel = passive_plexer.use_server_channel(0x0003u16); - - active_plexer.muxer.spawn(); - passive_plexer.demuxer.spawn(); - - for _ in 0..100 { - let payload = random_payload(50); - sender_channel.enqueue_chunk(payload.clone()).await.unwrap(); - let received_payload = receiver_channel.dequeue_chunk().await.unwrap(); - assert_eq!(payload, received_payload); - } -} diff --git a/pallas-network/Cargo.toml b/pallas-network/Cargo.toml new file mode 100644 index 0000000..6f623a7 --- /dev/null +++ b/pallas-network/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "pallas-network" +description = "Ouroboros networking stack using async IO" +version = "0.18.0" +edition = "2021" +repository = "https://github.com/txpipe/pallas" +homepage = "https://github.com/txpipe/pallas" +documentation = "https://docs.rs/pallas-upstream" +license = "Apache-2.0" +readme = "README.md" +authors = [ + "Santiago Carmuega ", + "Pi Lanningham ", +] + +[dependencies] +byteorder = "1.4.3" +hex = "0.4.3" +itertools = "0.10.5" +pallas-codec = { version = "0.18.0", path = "../pallas-codec" } +pallas-crypto = { version = "0.18.0", path = "../pallas-crypto" } +thiserror = "1.0.31" +tokio = { version = "1", features = ["net", "io-util", "time", "sync"] } +tracing = "0.1.37" + +[dev-dependencies] +tracing-subscriber = "0.3.16" +tokio = { version = "1", features = ["full"] } +rand = "0.8.5" diff --git a/pallas-network/README.md b/pallas-network/README.md new file mode 100644 index 0000000..6d4a3ab --- /dev/null +++ b/pallas-network/README.md @@ -0,0 +1,3 @@ +# Pallas Network + +An implementation of the Ouroboros networking stack. It provides a generic multiplexer and state-machines for the different mini-protocols. It uses async and tokio under the hood. \ No newline at end of file diff --git a/pallas-network/src/bearer.rs b/pallas-network/src/bearer.rs new file mode 100644 index 0000000..bebf602 --- /dev/null +++ b/pallas-network/src/bearer.rs @@ -0,0 +1,201 @@ +use std::net::SocketAddr; +use std::path::Path; + +use byteorder::{ByteOrder, NetworkEndian}; +use thiserror::Error; +use tokio::io::AsyncWriteExt; +use tokio::net::{TcpListener, TcpStream, ToSocketAddrs, UnixStream}; +use tokio::time::Instant; +use tracing::trace; + +const HEADER_LEN: usize = 8; + +pub type Timestamp = u32; + +pub type Payload = Vec; + +pub type Protocol = u16; + +#[derive(Debug)] +pub struct Header { + pub protocol: Protocol, + pub timestamp: Timestamp, + pub payload_len: u16, +} + +impl From<&[u8]> for Header { + fn from(value: &[u8]) -> Self { + let timestamp = NetworkEndian::read_u32(&value[0..4]); + let protocol = NetworkEndian::read_u16(&value[4..6]); + let payload_len = NetworkEndian::read_u16(&value[6..8]); + + Self { + timestamp, + protocol, + payload_len, + } + } +} + +impl From
for [u8; 8] { + fn from(value: Header) -> Self { + let mut out = [0u8; 8]; + NetworkEndian::write_u32(&mut out[0..4], value.timestamp); + NetworkEndian::write_u16(&mut out[4..6], value.protocol); + NetworkEndian::write_u16(&mut out[6..8], value.payload_len); + + out + } +} + +pub struct Segment { + pub header: Header, + pub payload: Payload, +} + +pub enum Bearer { + Tcp(TcpStream), + Unix(UnixStream), +} + +const BUFFER_LEN: usize = 1024 * 10; + +impl Bearer { + pub async fn connect_tcp(addr: impl ToSocketAddrs) -> Result { + let stream = TcpStream::connect(addr).await?; + Ok(Self::Tcp(stream)) + } + + pub async fn accept_tcp(listener: TcpListener) -> tokio::io::Result<(Self, SocketAddr)> { + let (stream, addr) = listener.accept().await?; + Ok((Self::Tcp(stream), addr)) + } + + pub async fn connect_unix(path: impl AsRef) -> Result { + let stream = UnixStream::connect(path).await?; + Ok(Self::Unix(stream)) + } + + pub async fn readable(&self) -> tokio::io::Result<()> { + match self { + Bearer::Tcp(x) => x.readable().await, + Bearer::Unix(x) => x.readable().await, + } + } + + fn try_read(&mut self, buf: &mut [u8]) -> tokio::io::Result { + match self { + Bearer::Tcp(x) => x.try_read(buf), + Bearer::Unix(x) => x.try_read(buf), + } + } + + async fn write_all(&mut self, buf: &[u8]) -> tokio::io::Result<()> { + match self { + Bearer::Tcp(x) => x.write_all(buf).await, + Bearer::Unix(x) => x.write_all(buf).await, + } + } + + async fn flush(&mut self) -> tokio::io::Result<()> { + match self { + Bearer::Tcp(x) => x.flush().await, + Bearer::Unix(x) => x.flush().await, + } + } +} + +#[derive(Debug, Error)] +pub enum Error { + #[error("no data available in bearer to complete segment")] + NoData, + + #[error("unexpected I/O error")] + Io(#[source] tokio::io::Error), +} + +pub struct SegmentBuffer(Bearer, Vec); + +impl SegmentBuffer { + pub fn new(bearer: Bearer) -> Self { + Self(bearer, Vec::with_capacity(BUFFER_LEN)) + } + + /// Cancel-safe loop that reads from bearer until certain len + async fn cancellable_read(&mut self, required: usize) -> Result<(), Error> { + loop { + self.0.readable().await.map_err(Error::Io)?; + trace!("bearer is readable"); + + let remaining = required - self.1.len(); + let mut buf = vec![0u8; remaining]; + + match self.0.try_read(&mut buf) { + Ok(0) => break Err(Error::NoData), + Ok(n) => { + trace!(n, "found data on bearer"); + self.1.extend_from_slice(&buf[0..n]); + + if self.1.len() >= required { + break Ok(()); + } + } + Err(ref e) if e.kind() == tokio::io::ErrorKind::WouldBlock => { + trace!("reading from bearer would block"); + continue; + } + Err(e) => { + return Err(Error::Io(e)); + } + } + } + } + + /// Peek the available data in search for a frame header + async fn peek_header(&mut self) -> Result { + trace!("waiting for header buf"); + self.cancellable_read(HEADER_LEN).await?; + + trace!("found enough data for header"); + let header = &self.1[..HEADER_LEN]; + + Ok(Header::from(header)) + } + + // Cancel-safe read of a full segment from the bearer + pub async fn read_segment(&mut self) -> Result<(Protocol, Payload), Error> { + let header = self.peek_header().await?; + + trace!("waiting for full segment buf"); + let segment_size = HEADER_LEN + header.payload_len as usize; + + self.cancellable_read(segment_size).await?; + + trace!("draining segment buffer"); + let segment = self.1.drain(..segment_size); + let payload = segment.skip(HEADER_LEN).collect(); + + Ok((header.protocol, payload)) + } + + pub async fn write_segment( + &mut self, + protocol: u16, + clock: &Instant, + payload: &[u8], + ) -> Result<(), std::io::Error> { + let header = Header { + protocol, + timestamp: clock.elapsed().as_micros() as u32, + payload_len: payload.len() as u16, + }; + + let buf: [u8; 8] = header.into(); + self.0.write_all(&buf).await?; + self.0.write_all(payload).await?; + + self.0.flush().await?; + + Ok(()) + } +} diff --git a/pallas-network/src/facades.rs b/pallas-network/src/facades.rs new file mode 100644 index 0000000..5bbba2b --- /dev/null +++ b/pallas-network/src/facades.rs @@ -0,0 +1,139 @@ +use std::path::Path; + +use thiserror::Error; +use tokio::task::JoinHandle; +use tracing::{debug, error}; + +use crate::{ + bearer, + miniprotocols::{ + blockfetch, chainsync, handshake, localstate, PROTOCOL_N2C_CHAIN_SYNC, + PROTOCOL_N2C_HANDSHAKE, PROTOCOL_N2C_STATE_QUERY, + }, + plexer, +}; + +#[derive(Debug, Error)] +pub enum Error { + #[error("error connecting bearer")] + ConnectFailure(#[source] tokio::io::Error), + + #[error("handshake protocol error")] + HandshakeProtocol(handshake::Error), + + #[error("handshake version not accepted")] + IncompatibleVersion, +} + +pub struct PeerClient { + plexer_handle: JoinHandle>, + pub handshake: handshake::Confirmation, + chainsync: chainsync::N2NClient, + blockfetch: blockfetch::Client, +} + +impl PeerClient { + pub async fn connect(address: &str, magic: u64) -> Result { + debug!("connecting"); + let bearer = bearer::Bearer::connect_tcp(address) + .await + .map_err(Error::ConnectFailure)?; + + let mut plexer = plexer::Plexer::new(bearer); + + let channel0 = plexer.subscribe_client(0); + let channel2 = plexer.subscribe_client(2); + let channel3 = plexer.subscribe_client(3); + + let plexer_handle = tokio::spawn(async move { plexer.run().await }); + + let versions = handshake::n2n::VersionTable::v7_and_above(magic); + let mut client = handshake::Client::new(channel0); + + let handshake = client + .handshake(versions) + .await + .map_err(Error::HandshakeProtocol)?; + + if let handshake::Confirmation::Rejected(reason) = handshake { + error!(?reason, "handshake refused"); + return Err(Error::IncompatibleVersion); + } + + Ok(Self { + plexer_handle, + handshake, + chainsync: chainsync::Client::new(channel2), + blockfetch: blockfetch::Client::new(channel3), + }) + } + + pub fn chainsync(&mut self) -> &mut chainsync::N2NClient { + &mut self.chainsync + } + + pub fn blockfetch(&mut self) -> &mut blockfetch::Client { + &mut self.blockfetch + } + + pub fn abort(&mut self) { + self.plexer_handle.abort(); + } +} + +pub struct NodeClient { + plexer_handle: JoinHandle>, + pub handshake: handshake::Confirmation, + chainsync: chainsync::N2CClient, + statequery: localstate::ClientV10, +} + +impl NodeClient { + pub async fn connect(path: impl AsRef, magic: u64) -> Result { + debug!("connecting"); + + let bearer = bearer::Bearer::connect_unix(path) + .await + .map_err(Error::ConnectFailure)?; + + let mut plexer = plexer::Plexer::new(bearer); + + let hs_channel = plexer.subscribe_client(PROTOCOL_N2C_HANDSHAKE); + let cs_channel = plexer.subscribe_client(PROTOCOL_N2C_CHAIN_SYNC); + let sq_channel = plexer.subscribe_client(PROTOCOL_N2C_STATE_QUERY); + + let plexer_handle = tokio::spawn(async move { plexer.run().await }); + + let versions = handshake::n2c::VersionTable::v10_and_above(magic); + let mut client = handshake::Client::new(hs_channel); + + let handshake = client + .handshake(versions) + .await + .map_err(Error::HandshakeProtocol)?; + + if let handshake::Confirmation::Rejected(reason) = handshake { + error!(?reason, "handshake refused"); + return Err(Error::IncompatibleVersion); + } + + Ok(Self { + plexer_handle, + handshake, + chainsync: chainsync::Client::new(cs_channel), + statequery: localstate::Client::new(sq_channel), + }) + } + + pub fn chainsync(&mut self) -> &mut chainsync::N2CClient { + &mut self.chainsync + } + + pub fn statequery(&mut self) -> &mut localstate::ClientV10 { + &mut self.statequery + } + + pub fn abort(&mut self) { + self.plexer_handle.abort(); + } +} diff --git a/pallas-network/src/lib.rs b/pallas-network/src/lib.rs new file mode 100644 index 0000000..750af8f --- /dev/null +++ b/pallas-network/src/lib.rs @@ -0,0 +1,4 @@ +pub mod bearer; +pub mod facades; +pub mod miniprotocols; +pub mod plexer; diff --git a/pallas-miniprotocols/README.md b/pallas-network/src/miniprotocols/README.md similarity index 100% rename from pallas-miniprotocols/README.md rename to pallas-network/src/miniprotocols/README.md diff --git a/pallas-miniprotocols/src/blockfetch/client.rs b/pallas-network/src/miniprotocols/blockfetch/client.rs similarity index 87% rename from pallas-miniprotocols/src/blockfetch/client.rs rename to pallas-network/src/miniprotocols/blockfetch/client.rs index 17b03e3..09a4388 100644 --- a/pallas-miniprotocols/src/blockfetch/client.rs +++ b/pallas-network/src/miniprotocols/blockfetch/client.rs @@ -1,9 +1,8 @@ -use pallas_codec::Fragment; -use pallas_multiplexer::agents::{Channel, ChannelBuffer, ChannelError}; use thiserror::Error; use tracing::{debug, info, warn}; -use crate::common::Point; +use crate::miniprotocols::common::Point; +use crate::plexer; use super::{Message, State}; @@ -24,8 +23,8 @@ pub enum Error { #[error("requested range doesn't contain any blocks")] NoBlocks, - #[error("error while sending or receiving data through the channel")] - ChannelError(ChannelError), + #[error("error while sending or receiving data through the multiplexer")] + Plexer(plexer::Error), } pub type Body = Vec; @@ -34,18 +33,11 @@ pub type Range = (Point, Point); pub type HasBlocks = Option<()>; -pub struct Client(State, ChannelBuffer) -where - H: Channel, - Message: Fragment; +pub struct Client(State, plexer::ChannelBuffer); -impl Client -where - H: Channel, - Message: Fragment, -{ - pub fn new(channel: H) -> Self { - Self(State::Idle, ChannelBuffer::new(channel)) +impl Client { + pub fn new(channel: plexer::AgentChannel) -> Self { + Self(State::Idle, plexer::ChannelBuffer::new(channel)) } pub fn state(&self) -> &State { @@ -102,17 +94,14 @@ where pub async fn send_message(&mut self, msg: &Message) -> Result<(), Error> { self.assert_agency_is_ours()?; self.assert_outbound_state(msg)?; - self.1 - .send_msg_chunks(msg) - .await - .map_err(Error::ChannelError)?; + self.1.send_msg_chunks(msg).await.map_err(Error::Plexer)?; Ok(()) } pub async fn recv_message(&mut self) -> Result { self.assert_agency_is_theirs()?; - let msg = self.1.recv_full_msg().await.map_err(Error::ChannelError)?; + let msg = self.1.recv_full_msg().await.map_err(Error::Plexer)?; self.assert_inbound_state(&msg)?; Ok(msg) @@ -149,6 +138,8 @@ where } pub async fn recv_while_streaming(&mut self) -> Result, Error> { + debug!("waiting for stream"); + match self.recv_message().await? { Message::Block { body } => Ok(Some(body)), Message::BatchDone => { diff --git a/pallas-miniprotocols/src/blockfetch/codec.rs b/pallas-network/src/miniprotocols/blockfetch/codec.rs similarity index 100% rename from pallas-miniprotocols/src/blockfetch/codec.rs rename to pallas-network/src/miniprotocols/blockfetch/codec.rs diff --git a/pallas-miniprotocols/src/blockfetch/mod.rs b/pallas-network/src/miniprotocols/blockfetch/mod.rs similarity index 100% rename from pallas-miniprotocols/src/blockfetch/mod.rs rename to pallas-network/src/miniprotocols/blockfetch/mod.rs diff --git a/pallas-miniprotocols/src/blockfetch/protocol.rs b/pallas-network/src/miniprotocols/blockfetch/protocol.rs similarity index 89% rename from pallas-miniprotocols/src/blockfetch/protocol.rs rename to pallas-network/src/miniprotocols/blockfetch/protocol.rs index 594626e..b3958b4 100644 --- a/pallas-miniprotocols/src/blockfetch/protocol.rs +++ b/pallas-network/src/miniprotocols/blockfetch/protocol.rs @@ -1,4 +1,4 @@ -use crate::Point; +use crate::miniprotocols::Point; #[derive(Debug, PartialEq, Eq, Clone)] pub enum State { diff --git a/pallas-miniprotocols/src/chainsync/buffer.rs b/pallas-network/src/miniprotocols/chainsync/buffer.rs similarity index 98% rename from pallas-miniprotocols/src/chainsync/buffer.rs rename to pallas-network/src/miniprotocols/chainsync/buffer.rs index 05fe62b..159c6ce 100644 --- a/pallas-miniprotocols/src/chainsync/buffer.rs +++ b/pallas-network/src/miniprotocols/chainsync/buffer.rs @@ -1,6 +1,6 @@ use std::collections::{vec_deque::Iter, VecDeque}; -use crate::Point; +use crate::miniprotocols::Point; /// A memory buffer to handle chain rollbacks /// @@ -98,7 +98,8 @@ impl RollbackBuffer { #[cfg(test)] mod tests { - use crate::{chainsync::RollbackEffect, Point}; + use super::RollbackEffect; + use crate::miniprotocols::Point; use super::RollbackBuffer; diff --git a/pallas-miniprotocols/src/chainsync/client.rs b/pallas-network/src/miniprotocols/chainsync/client.rs similarity index 90% rename from pallas-miniprotocols/src/chainsync/client.rs rename to pallas-network/src/miniprotocols/chainsync/client.rs index 747d682..6b985d9 100644 --- a/pallas-miniprotocols/src/chainsync/client.rs +++ b/pallas-network/src/miniprotocols/chainsync/client.rs @@ -1,10 +1,10 @@ use pallas_codec::Fragment; -use pallas_multiplexer::agents::{Channel, ChannelBuffer, ChannelError}; use std::marker::PhantomData; use thiserror::Error; use tracing::debug; -use crate::common::Point; +use crate::miniprotocols::Point; +use crate::plexer; use super::{BlockContent, HeaderContent, Message, State, Tip}; @@ -26,7 +26,7 @@ pub enum Error { IntersectionNotFound, #[error("error while sending or receiving data through the channel")] - ChannelError(ChannelError), + Plexer(plexer::Error), } pub type IntersectResponse = (Option, Tip); @@ -38,18 +38,20 @@ pub enum NextResponse { Await, } -pub struct Client(State, ChannelBuffer, PhantomData) +pub struct Client(State, plexer::ChannelBuffer, PhantomData) where - H: Channel, Message: Fragment; -impl Client +impl Client where - H: Channel, Message: Fragment, { - pub fn new(channel: H) -> Self { - Self(State::Idle, ChannelBuffer::new(channel), PhantomData {}) + pub fn new(channel: plexer::AgentChannel) -> Self { + Self( + State::Idle, + plexer::ChannelBuffer::new(channel), + PhantomData {}, + ) } pub fn state(&self) -> &State { @@ -112,10 +114,7 @@ where self.assert_agency_is_ours()?; self.assert_outbound_state(msg)?; - self.1 - .send_msg_chunks(msg) - .await - .map_err(Error::ChannelError)?; + self.1.send_msg_chunks(msg).await.map_err(Error::Plexer)?; Ok(()) } @@ -123,7 +122,7 @@ where pub async fn recv_message(&mut self) -> Result, Error> { self.assert_agency_is_theirs()?; - let msg = self.1.recv_full_msg().await.map_err(Error::ChannelError)?; + let msg = self.1.recv_full_msg().await.map_err(Error::Plexer)?; self.assert_inbound_state(&msg)?; @@ -135,10 +134,14 @@ where self.send_message(&msg).await?; self.0 = State::Intersect; + debug!("send find intersect"); + Ok(()) } pub async fn recv_intersect_response(&mut self) -> Result { + debug!("waiting for intersect response"); + match self.recv_message().await? { Message::IntersectFound(point, tip) => { self.0 = State::Idle; @@ -232,6 +235,6 @@ where } } -pub type N2NClient = Client; +pub type N2NClient = Client; -pub type N2CClient = Client; +pub type N2CClient = Client; diff --git a/pallas-miniprotocols/src/chainsync/codec.rs b/pallas-network/src/miniprotocols/chainsync/codec.rs similarity index 100% rename from pallas-miniprotocols/src/chainsync/codec.rs rename to pallas-network/src/miniprotocols/chainsync/codec.rs diff --git a/pallas-miniprotocols/src/chainsync/mod.rs b/pallas-network/src/miniprotocols/chainsync/mod.rs similarity index 100% rename from pallas-miniprotocols/src/chainsync/mod.rs rename to pallas-network/src/miniprotocols/chainsync/mod.rs diff --git a/pallas-miniprotocols/src/chainsync/protocol.rs b/pallas-network/src/miniprotocols/chainsync/protocol.rs similarity index 96% rename from pallas-miniprotocols/src/chainsync/protocol.rs rename to pallas-network/src/miniprotocols/chainsync/protocol.rs index 8b8e984..ee1eef5 100644 --- a/pallas-miniprotocols/src/chainsync/protocol.rs +++ b/pallas-network/src/miniprotocols/chainsync/protocol.rs @@ -1,6 +1,6 @@ use std::{fmt::Debug, ops::Deref}; -use crate::common::Point; +use crate::miniprotocols::Point; #[derive(Debug, Clone)] pub struct Tip(pub Point, pub u64); diff --git a/pallas-miniprotocols/src/common.rs b/pallas-network/src/miniprotocols/common.rs similarity index 97% rename from pallas-miniprotocols/src/common.rs rename to pallas-network/src/miniprotocols/common.rs index 180783b..562da29 100644 --- a/pallas-miniprotocols/src/common.rs +++ b/pallas-network/src/miniprotocols/common.rs @@ -17,7 +17,7 @@ pub const PRE_PRODUCTION_MAGIC: u64 = 1; /// Bitflag for client-side version of a known protocol /// # Example /// ``` -/// use pallas_miniprotocols::*; +/// use pallas_network::miniprotocols::*; /// let channel = PROTOCOL_CLIENT | PROTOCOL_N2N_HANDSHAKE; /// ``` pub const PROTOCOL_CLIENT: u16 = 0x0; @@ -25,7 +25,7 @@ pub const PROTOCOL_CLIENT: u16 = 0x0; /// Bitflag for server-side version of a known protocol /// # Example /// ``` -/// use pallas_miniprotocols::*; +/// use pallas_network::miniprotocols::*; /// let channel = PROTOCOL_SERVER | PROTOCOL_N2N_CHAIN_SYNC; /// ``` pub const PROTOCOL_SERVER: u16 = 0x8000; diff --git a/pallas-miniprotocols/src/handshake/README.md b/pallas-network/src/miniprotocols/handshake/README.md similarity index 100% rename from pallas-miniprotocols/src/handshake/README.md rename to pallas-network/src/miniprotocols/handshake/README.md diff --git a/pallas-miniprotocols/src/handshake/client.rs b/pallas-network/src/miniprotocols/handshake/client.rs similarity index 82% rename from pallas-miniprotocols/src/handshake/client.rs rename to pallas-network/src/miniprotocols/handshake/client.rs index eb1a0c8..cb15321 100644 --- a/pallas-miniprotocols/src/handshake/client.rs +++ b/pallas-network/src/miniprotocols/handshake/client.rs @@ -1,9 +1,9 @@ use pallas_codec::Fragment; -use pallas_multiplexer::agents::{Channel, ChannelBuffer}; use std::marker::PhantomData; use tracing::debug; use super::{Error, Message, RefuseReason, State, VersionNumber, VersionTable}; +use crate::plexer; #[derive(Debug)] pub enum Confirmation { @@ -11,18 +11,19 @@ pub enum Confirmation { Rejected(RefuseReason), } -pub struct Client(State, ChannelBuffer, PhantomData) -where - H: Channel; +pub struct Client(State, plexer::ChannelBuffer, PhantomData); -impl Client +impl Client where - H: Channel, D: std::fmt::Debug + Clone, Message: Fragment, { - pub fn new(channel: H) -> Self { - Self(State::Propose, ChannelBuffer::new(channel), PhantomData {}) + pub fn new(channel: plexer::AgentChannel) -> Self { + Self( + State::Propose, + plexer::ChannelBuffer::new(channel), + PhantomData {}, + ) } pub fn state(&self) -> &State { @@ -75,17 +76,14 @@ where pub async fn send_message(&mut self, msg: &Message) -> Result<(), Error> { self.assert_agency_is_ours()?; self.assert_outbound_state(msg)?; - self.1 - .send_msg_chunks(msg) - .await - .map_err(Error::ChannelError)?; + self.1.send_msg_chunks(msg).await.map_err(Error::Plexer)?; Ok(()) } pub async fn recv_message(&mut self) -> Result, Error> { self.assert_agency_is_theirs()?; - let msg = self.1.recv_full_msg().await.map_err(Error::ChannelError)?; + let msg = self.1.recv_full_msg().await.map_err(Error::Plexer)?; self.assert_inbound_state(&msg)?; Ok(msg) @@ -124,11 +122,11 @@ where self.recv_while_confirm().await } - pub fn unwrap(self) -> H { + pub fn unwrap(self) -> plexer::AgentChannel { self.1.unwrap() } } -pub type N2NClient = Client; +pub type N2NClient = Client; -pub type N2CClient = Client; +pub type N2CClient = Client; diff --git a/pallas-miniprotocols/src/handshake/mod.rs b/pallas-network/src/miniprotocols/handshake/mod.rs similarity index 100% rename from pallas-miniprotocols/src/handshake/mod.rs rename to pallas-network/src/miniprotocols/handshake/mod.rs diff --git a/pallas-miniprotocols/src/handshake/n2c.rs b/pallas-network/src/miniprotocols/handshake/n2c.rs similarity index 100% rename from pallas-miniprotocols/src/handshake/n2c.rs rename to pallas-network/src/miniprotocols/handshake/n2c.rs diff --git a/pallas-miniprotocols/src/handshake/n2n.rs b/pallas-network/src/miniprotocols/handshake/n2n.rs similarity index 100% rename from pallas-miniprotocols/src/handshake/n2n.rs rename to pallas-network/src/miniprotocols/handshake/n2n.rs diff --git a/pallas-miniprotocols/src/handshake/protocol.rs b/pallas-network/src/miniprotocols/handshake/protocol.rs similarity index 98% rename from pallas-miniprotocols/src/handshake/protocol.rs rename to pallas-network/src/miniprotocols/handshake/protocol.rs index 05a0912..845c283 100644 --- a/pallas-miniprotocols/src/handshake/protocol.rs +++ b/pallas-network/src/miniprotocols/handshake/protocol.rs @@ -1,9 +1,10 @@ use itertools::Itertools; use pallas_codec::minicbor::{decode, encode, Decode, Decoder, Encode, Encoder}; -use pallas_multiplexer::agents::ChannelError; use std::{collections::HashMap, fmt::Debug}; use thiserror::*; +use crate::plexer; + #[derive(Error, Debug)] pub enum Error { #[error("attempted to receive message while agency is ours")] @@ -19,7 +20,7 @@ pub enum Error { InvalidOutbound, #[error("error while sending or receiving data through the channel")] - ChannelError(ChannelError), + Plexer(plexer::Error), } #[derive(Debug, Clone)] diff --git a/pallas-miniprotocols/src/handshake/server.rs b/pallas-network/src/miniprotocols/handshake/server.rs similarity index 80% rename from pallas-miniprotocols/src/handshake/server.rs rename to pallas-network/src/miniprotocols/handshake/server.rs index 5c619f2..e29968e 100644 --- a/pallas-miniprotocols/src/handshake/server.rs +++ b/pallas-network/src/miniprotocols/handshake/server.rs @@ -1,22 +1,23 @@ use std::marker::PhantomData; use pallas_codec::Fragment; -use pallas_multiplexer::agents::{Channel, ChannelBuffer}; use super::{Error, Message, RefuseReason, State, VersionNumber, VersionTable}; +use crate::plexer; -pub struct Server(State, ChannelBuffer, PhantomData) -where - H: Channel; +pub struct Server(State, plexer::ChannelBuffer, PhantomData); -impl Server +impl Server where - H: Channel, D: std::fmt::Debug + Clone, Message: Fragment, { - pub fn new(channel: H) -> Self { - Self(State::Propose, ChannelBuffer::new(channel), PhantomData {}) + pub fn new(channel: plexer::AgentChannel) -> Self { + Self( + State::Propose, + plexer::ChannelBuffer::new(channel), + PhantomData {}, + ) } pub fn state(&self) -> &State { @@ -65,17 +66,14 @@ where pub async fn send_message(&mut self, msg: &Message) -> Result<(), Error> { self.assert_agency_is_ours()?; self.assert_outbound_state(msg)?; - self.1 - .send_msg_chunks(msg) - .await - .map_err(Error::ChannelError)?; + self.1.send_msg_chunks(msg).await.map_err(Error::Plexer)?; Ok(()) } pub async fn recv_message(&mut self) -> Result, Error> { self.assert_agency_is_theirs()?; - let msg = self.1.recv_full_msg().await.map_err(Error::ChannelError)?; + let msg = self.1.recv_full_msg().await.map_err(Error::Plexer)?; self.assert_inbound_state(&msg)?; Ok(msg) @@ -111,11 +109,11 @@ where Ok(()) } - pub fn unwrap(self) -> H { + pub fn unwrap(self) -> plexer::AgentChannel { self.1.unwrap() } } -pub type N2NServer = Server; +pub type N2NServer = Server; -pub type N2CServer = Server; +pub type N2CServer = Server; diff --git a/pallas-miniprotocols/src/localstate/client.rs b/pallas-network/src/miniprotocols/localstate/client.rs similarity index 88% rename from pallas-miniprotocols/src/localstate/client.rs rename to pallas-network/src/miniprotocols/localstate/client.rs index f84d724..21af2e6 100644 --- a/pallas-miniprotocols/src/localstate/client.rs +++ b/pallas-network/src/miniprotocols/localstate/client.rs @@ -2,13 +2,12 @@ use std::fmt::Debug; use pallas_codec::Fragment; -use crate::common::Point; - -use pallas_multiplexer::agents::{Channel, ChannelBuffer, ChannelError}; use std::marker::PhantomData; use thiserror::*; use super::{AcquireFailure, Message, Query, State}; +use crate::miniprotocols::Point; +use crate::plexer; #[derive(Error, Debug)] pub enum Error { @@ -25,7 +24,7 @@ pub enum Error { #[error("failure acquiring point, too old")] AcquirePointTooOld, #[error("error while sending or receiving data through the channel")] - ChannelError(ChannelError), + Plexer(plexer::Error), } impl From for Error { @@ -37,20 +36,22 @@ impl From for Error { } } -pub struct Client(State, ChannelBuffer, PhantomData) +pub struct Client(State, plexer::ChannelBuffer, PhantomData) where - H: Channel, Q: Query, Message: Fragment; -impl Client +impl Client where - H: Channel, Q: Query, Message: Fragment, { - pub fn new(channel: H) -> Self { - Self(State::Idle, ChannelBuffer::new(channel), PhantomData {}) + pub fn new(channel: plexer::AgentChannel) -> Self { + Self( + State::Idle, + plexer::ChannelBuffer::new(channel), + PhantomData {}, + ) } pub fn state(&self) -> &State { @@ -108,17 +109,14 @@ where pub async fn send_message(&mut self, msg: &Message) -> Result<(), Error> { self.assert_agency_is_ours()?; self.assert_outbound_state(msg)?; - self.1 - .send_msg_chunks(msg) - .await - .map_err(Error::ChannelError)?; + self.1.send_msg_chunks(msg).await.map_err(Error::Plexer)?; Ok(()) } pub async fn recv_message(&mut self) -> Result, Error> { self.assert_agency_is_theirs()?; - let msg = self.1.recv_full_msg().await.map_err(Error::ChannelError)?; + let msg = self.1.recv_full_msg().await.map_err(Error::Plexer)?; self.assert_inbound_state(&msg)?; Ok(msg) @@ -175,4 +173,4 @@ where } } -pub type ClientV10 = Client; +pub type ClientV10 = Client; diff --git a/pallas-miniprotocols/src/localstate/codec.rs b/pallas-network/src/miniprotocols/localstate/codec.rs similarity index 100% rename from pallas-miniprotocols/src/localstate/codec.rs rename to pallas-network/src/miniprotocols/localstate/codec.rs diff --git a/pallas-miniprotocols/src/localstate/mod.rs b/pallas-network/src/miniprotocols/localstate/mod.rs similarity index 100% rename from pallas-miniprotocols/src/localstate/mod.rs rename to pallas-network/src/miniprotocols/localstate/mod.rs diff --git a/pallas-miniprotocols/src/localstate/protocol.rs b/pallas-network/src/miniprotocols/localstate/protocol.rs similarity index 94% rename from pallas-miniprotocols/src/localstate/protocol.rs rename to pallas-network/src/miniprotocols/localstate/protocol.rs index a40b0ca..1c82106 100644 --- a/pallas-miniprotocols/src/localstate/protocol.rs +++ b/pallas-network/src/miniprotocols/localstate/protocol.rs @@ -1,6 +1,6 @@ use std::fmt::Debug; -use crate::common::Point; +use crate::miniprotocols::Point; #[derive(Debug, PartialEq, Eq, Clone)] pub enum State { diff --git a/pallas-miniprotocols/src/localstate/queries.rs b/pallas-network/src/miniprotocols/localstate/queries.rs similarity index 100% rename from pallas-miniprotocols/src/localstate/queries.rs rename to pallas-network/src/miniprotocols/localstate/queries.rs diff --git a/pallas-miniprotocols/src/lib.rs b/pallas-network/src/miniprotocols/mod.rs similarity index 81% rename from pallas-miniprotocols/src/lib.rs rename to pallas-network/src/miniprotocols/mod.rs index be3b7f2..f7bea7b 100644 --- a/pallas-miniprotocols/src/lib.rs +++ b/pallas-network/src/miniprotocols/mod.rs @@ -1,5 +1,4 @@ mod common; -mod machines; pub mod blockfetch; pub mod chainsync; @@ -9,4 +8,3 @@ pub mod txmonitor; pub mod txsubmission; pub use common::*; -pub use machines::*; diff --git a/pallas-network/src/miniprotocols/txmonitor/client.rs b/pallas-network/src/miniprotocols/txmonitor/client.rs new file mode 100644 index 0000000..79eaec9 --- /dev/null +++ b/pallas-network/src/miniprotocols/txmonitor/client.rs @@ -0,0 +1,205 @@ +use std::fmt::Debug; +use thiserror::*; + +use super::protocol::*; +use crate::plexer; + +#[derive(Error, Debug)] +pub enum Error { + #[error("attempted to receive message while agency is ours")] + AgencyIsOurs, + + #[error("attempted to send message while agency is theirs")] + AgencyIsTheirs, + + #[error("inbound message is not valid for current state")] + InvalidInbound, + + #[error("outbound message is not valid for current state")] + InvalidOutbound, + + #[error("error while sending or receiving data through the channel")] + Plexer(plexer::Error), +} + +pub struct Client(State, plexer::ChannelBuffer); + +impl Client { + pub fn new(channel: plexer::AgentChannel) -> Self { + Self(State::Idle, plexer::ChannelBuffer::new(channel)) + } + + pub fn state(&self) -> &State { + &self.0 + } + + pub fn is_done(&self) -> bool { + self.0 == State::Done + } + + fn has_agency(&self) -> bool { + match &self.0 { + State::Idle => true, + State::Acquiring => false, + State::Acquired => true, + State::Busy => false, + State::Done => false, + } + } + + fn assert_agency_is_ours(&self) -> Result<(), Error> { + if !self.has_agency() { + Err(Error::AgencyIsTheirs) + } else { + Ok(()) + } + } + + fn assert_agency_is_theirs(&self) -> Result<(), Error> { + if self.has_agency() { + Err(Error::AgencyIsOurs) + } else { + Ok(()) + } + } + + fn assert_outbound_state(&self, msg: &Message) -> Result<(), Error> { + match (&self.0, msg) { + (State::Idle, Message::Acquire) => Ok(()), + (State::Idle, Message::Done) => Ok(()), + (State::Acquired, Message::Acquire) => Ok(()), + (State::Acquired, Message::RequestHasTx(..)) => Ok(()), + (State::Acquired, Message::RequestNextTx) => Ok(()), + (State::Acquired, Message::RequestSizeAndCapacity) => Ok(()), + _ => Err(Error::InvalidOutbound), + } + } + + fn assert_inbound_state(&self, msg: &Message) -> Result<(), Error> { + match (&self.0, msg) { + (State::Acquiring, Message::Acquired(..)) => Ok(()), + (State::Busy, Message::ResponseHasTx(..)) => Ok(()), + (State::Busy, Message::ResponseNextTx(..)) => Ok(()), + (State::Busy, Message::ResponseSizeAndCapacity(..)) => Ok(()), + _ => Err(Error::InvalidInbound), + } + } + + pub async fn send_message(&mut self, msg: &Message) -> Result<(), Error> { + self.assert_agency_is_ours()?; + self.assert_outbound_state(msg)?; + self.1.send_msg_chunks(msg).await.map_err(Error::Plexer)?; + + Ok(()) + } + + pub async fn recv_message(&mut self) -> Result { + self.assert_agency_is_theirs()?; + let msg = self.1.recv_full_msg().await.map_err(Error::Plexer)?; + self.assert_inbound_state(&msg)?; + + Ok(msg) + } + + async fn send_acquire(&mut self) -> Result<(), Error> { + let msg = Message::Acquire; + self.send_message(&msg).await?; + self.0 = State::Acquiring; + + Ok(()) + } + + async fn recv_while_acquiring(&mut self) -> Result { + match self.recv_message().await? { + Message::Acquired(slot) => { + self.0 = State::Acquired; + Ok(slot) + } + _ => Err(Error::InvalidInbound), + } + } + + pub async fn acquire(&mut self) -> Result { + self.send_acquire().await?; + self.recv_while_acquiring().await + } + + async fn send_request_has_tx(&mut self, id: TxId) -> Result<(), Error> { + let msg = Message::RequestHasTx(id); + self.send_message(&msg).await?; + self.0 = State::Busy; + + Ok(()) + } + + async fn recv_while_requesting_has_tx(&mut self) -> Result { + match self.recv_message().await? { + Message::ResponseHasTx(x) => { + self.0 = State::Acquired; + Ok(x) + } + _ => Err(Error::InvalidInbound), + } + } + + pub async fn query_has_tx(&mut self, id: TxId) -> Result { + self.send_request_has_tx(id).await?; + self.recv_while_requesting_has_tx().await + } + + async fn send_request_next_tx(&mut self) -> Result<(), Error> { + let msg = Message::RequestNextTx; + self.send_message(&msg).await?; + self.0 = State::Busy; + + Ok(()) + } + + async fn recv_while_requesting_next_tx(&mut self) -> Result, Error> { + match self.recv_message().await? { + Message::ResponseNextTx(x) => { + self.0 = State::Acquired; + Ok(x) + } + _ => Err(Error::InvalidInbound), + } + } + + pub async fn query_next_tx(&mut self) -> Result, Error> { + self.send_request_next_tx().await?; + self.recv_while_requesting_next_tx().await + } + + async fn send_request_size_and_capacity(&mut self) -> Result<(), Error> { + let msg = Message::RequestSizeAndCapacity; + self.send_message(&msg).await?; + self.0 = State::Busy; + + Ok(()) + } + + async fn recv_while_requesting_size_and_capacity( + &mut self, + ) -> Result { + match self.recv_message().await? { + Message::ResponseSizeAndCapacity(x) => { + self.0 = State::Acquired; + Ok(x) + } + _ => Err(Error::InvalidInbound), + } + } + + pub async fn query_size_and_capacity(&mut self) -> Result { + self.send_request_size_and_capacity().await?; + self.recv_while_requesting_size_and_capacity().await + } + + pub async fn release(&mut self) -> Result<(), Error> { + let msg = Message::Release; + self.send_message(&msg).await?; + self.0 = State::Idle; + + Ok(()) + } +} diff --git a/pallas-network/src/miniprotocols/txmonitor/codec.rs b/pallas-network/src/miniprotocols/txmonitor/codec.rs new file mode 100644 index 0000000..4faccbf --- /dev/null +++ b/pallas-network/src/miniprotocols/txmonitor/codec.rs @@ -0,0 +1,114 @@ +use super::protocol::*; +use pallas_codec::minicbor::{decode, encode, Decode, Encode, Encoder}; + +impl Encode<()> for Message { + fn encode( + &self, + e: &mut Encoder, + _ctx: &mut (), + ) -> Result<(), encode::Error> { + match self { + Message::Done => { + e.array(1)?.u16(0)?; + } + Message::Acquire => { + e.array(1)?.u16(1)?; + } + Message::Acquired(slot) => { + e.array(2)?.u16(2)?; + e.encode(slot)?; + } + Message::Release => { + e.array(1)?.u16(3)?; + } + // TODO: confirm if this is valid, I'm just assuming that label 4 is AwaitAcquire, can't + // find the specs + Message::AwaitAcquire => { + e.array(1)?.u16(4)?; + } + Message::RequestNextTx => { + e.array(1)?.u16(5)?; + } + Message::ResponseNextTx(None) => { + e.array(1)?.u16(6)?; + } + Message::ResponseNextTx(Some(tx)) => { + e.array(2)?.u16(6)?; + e.encode(tx)?; + } + Message::RequestHasTx(tx) => { + e.array(2)?.u16(7)?; + e.encode(tx)?; + } + Message::ResponseHasTx(tx) => { + e.array(2)?.u16(8)?; + e.encode(tx)?; + } + Message::RequestSizeAndCapacity => { + e.array(1)?.u16(9)?; + } + Message::ResponseSizeAndCapacity(sz) => { + e.array(2)?.u16(10)?; + e.array(3)?; + e.encode(sz.capacity_in_bytes)?; + e.encode(sz.size_in_bytes)?; + e.encode(sz.number_of_txs)?; + } + } + + Ok(()) + } +} + +impl<'b> Decode<'b, ()> for Message { + fn decode( + d: &mut pallas_codec::minicbor::Decoder<'b>, + _ctx: &mut (), + ) -> Result { + d.array()?; + let label = d.u16()?; + + match label { + 0 => Ok(Message::Done), + 1 => Ok(Message::Acquire), + 2 => { + let slot = d.decode()?; + Ok(Message::Acquired(slot)) + } + 3 => Ok(Message::Release), + // TODO: confirm if this is valid, I'm just assuming that label 4 is AwaitAcquire, can't + // find the specs + 4 => Ok(Message::AwaitAcquire), + 5 => Ok(Message::RequestNextTx), + 6 => match d.array()? { + Some(_) => { + let cbor: pallas_codec::utils::CborWrap = d.decode()?; + Ok(Message::ResponseNextTx(Some(cbor.unwrap()))) + } + None => Ok(Message::ResponseNextTx(None)), + }, + 7 => { + let id = d.decode()?; + Ok(Message::RequestHasTx(id)) + } + 8 => { + let has = d.decode()?; + Ok(Message::ResponseHasTx(has)) + } + 9 => Ok(Message::RequestSizeAndCapacity), + 10 => { + d.array()?; + let capacity_in_bytes = d.decode()?; + let size_in_bytes = d.decode()?; + let number_of_txs = d.decode()?; + + Ok(Message::ResponseSizeAndCapacity(MempoolSizeAndCapacity { + capacity_in_bytes, + size_in_bytes, + number_of_txs, + })) + } + _ => Err(decode::Error::message("can't decode Message")), + } + } +} diff --git a/pallas-network/src/miniprotocols/txmonitor/mod.rs b/pallas-network/src/miniprotocols/txmonitor/mod.rs new file mode 100644 index 0000000..a9eaa04 --- /dev/null +++ b/pallas-network/src/miniprotocols/txmonitor/mod.rs @@ -0,0 +1,7 @@ +mod client; +mod codec; +mod protocol; + +pub use client::*; +pub use codec::*; +pub use protocol::*; diff --git a/pallas-network/src/miniprotocols/txmonitor/protocol.rs b/pallas-network/src/miniprotocols/txmonitor/protocol.rs new file mode 100644 index 0000000..06c91bd --- /dev/null +++ b/pallas-network/src/miniprotocols/txmonitor/protocol.rs @@ -0,0 +1,34 @@ +pub type Slot = u64; +pub type TxId = String; +pub type Tx = Vec; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum State { + Idle, + Acquiring, + Acquired, + Busy, + Done, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct MempoolSizeAndCapacity { + pub capacity_in_bytes: u32, + pub size_in_bytes: u32, + pub number_of_txs: u32, +} + +#[derive(Debug, Clone)] +pub enum Message { + Acquire, + AwaitAcquire, + Acquired(Slot), + RequestHasTx(TxId), + RequestNextTx, + RequestSizeAndCapacity, + ResponseHasTx(bool), + ResponseNextTx(Option), + ResponseSizeAndCapacity(MempoolSizeAndCapacity), + Release, + Done, +} diff --git a/pallas-miniprotocols/src/txsubmission/README.md b/pallas-network/src/miniprotocols/txsubmission/README.md similarity index 100% rename from pallas-miniprotocols/src/txsubmission/README.md rename to pallas-network/src/miniprotocols/txsubmission/README.md diff --git a/pallas-miniprotocols/src/txsubmission/client.rs b/pallas-network/src/miniprotocols/txsubmission/client.rs similarity index 88% rename from pallas-miniprotocols/src/txsubmission/client.rs rename to pallas-network/src/miniprotocols/txsubmission/client.rs index 4b44c16..1d7577f 100644 --- a/pallas-miniprotocols/src/txsubmission/client.rs +++ b/pallas-network/src/miniprotocols/txsubmission/client.rs @@ -1,7 +1,7 @@ use std::marker::PhantomData; +use crate::plexer; use pallas_codec::Fragment; -use pallas_multiplexer::agents::{Channel, ChannelBuffer}; use super::{ protocol::{Error, Message, State, TxIdAndSize}, @@ -16,28 +16,26 @@ pub enum Request { /// A generic Ouroboros client for submitting a generic notion of "transactions" /// to another server -pub struct GenericClient( +pub struct GenericClient( State, - ChannelBuffer, + plexer::ChannelBuffer, PhantomData, PhantomData, ) where - H: Channel, Message: Fragment; /// A cardano specific instantiation of the ouroboros protocol -pub type Client = GenericClient; +pub type Client = GenericClient; -impl GenericClient +impl GenericClient where - H: Channel, Message: Fragment, { - pub fn new(channel: H) -> Self { + pub fn new(channel: plexer::AgentChannel) -> Self { Self( State::Init, - ChannelBuffer::new(channel), + plexer::ChannelBuffer::new(channel), PhantomData {}, PhantomData {}, ) @@ -95,17 +93,14 @@ where pub async fn send_message(&mut self, msg: &Message) -> Result<(), Error> { self.assert_agency_is_ours()?; self.assert_outbound_state(msg)?; - self.1 - .send_msg_chunks(msg) - .await - .map_err(Error::ChannelError)?; + self.1.send_msg_chunks(msg).await.map_err(Error::Plexer)?; Ok(()) } pub async fn recv_message(&mut self) -> Result, Error> { self.assert_agency_is_theirs()?; - let msg = self.1.recv_full_msg().await.map_err(Error::ChannelError)?; + let msg = self.1.recv_full_msg().await.map_err(Error::Plexer)?; self.assert_inbound_state(&msg)?; Ok(msg) diff --git a/pallas-miniprotocols/src/txsubmission/codec.rs b/pallas-network/src/miniprotocols/txsubmission/codec.rs similarity index 100% rename from pallas-miniprotocols/src/txsubmission/codec.rs rename to pallas-network/src/miniprotocols/txsubmission/codec.rs diff --git a/pallas-miniprotocols/src/txsubmission/mod.rs b/pallas-network/src/miniprotocols/txsubmission/mod.rs similarity index 100% rename from pallas-miniprotocols/src/txsubmission/mod.rs rename to pallas-network/src/miniprotocols/txsubmission/mod.rs diff --git a/pallas-miniprotocols/src/txsubmission/protocol.rs b/pallas-network/src/miniprotocols/txsubmission/protocol.rs similarity index 94% rename from pallas-miniprotocols/src/txsubmission/protocol.rs rename to pallas-network/src/miniprotocols/txsubmission/protocol.rs index db38009..ba46a2b 100644 --- a/pallas-miniprotocols/src/txsubmission/protocol.rs +++ b/pallas-network/src/miniprotocols/txsubmission/protocol.rs @@ -1,6 +1,7 @@ -use pallas_multiplexer::agents::ChannelError; use thiserror::Error; +use crate::plexer; + #[derive(Debug, PartialEq, Eq, Clone)] pub enum State { Init, @@ -46,7 +47,7 @@ pub enum Error { AlreadyInitialized, #[error("error while sending or receiving data through the channel")] - ChannelError(ChannelError), + Plexer(plexer::Error), } #[derive(Debug)] diff --git a/pallas-miniprotocols/src/txsubmission/server.rs b/pallas-network/src/miniprotocols/txsubmission/server.rs similarity index 88% rename from pallas-miniprotocols/src/txsubmission/server.rs rename to pallas-network/src/miniprotocols/txsubmission/server.rs index 4cb3f8b..eed9050 100644 --- a/pallas-miniprotocols/src/txsubmission/server.rs +++ b/pallas-network/src/miniprotocols/txsubmission/server.rs @@ -1,12 +1,12 @@ use std::marker::PhantomData; use pallas_codec::Fragment; -use pallas_multiplexer::agents::{Channel, ChannelBuffer}; use super::{ protocol::{Blocking, Error, Message, State, TxCount, TxIdAndSize}, EraTxBody, EraTxId, }; +use crate::plexer; pub enum Reply { TxIds(Vec>), @@ -16,28 +16,26 @@ pub enum Reply { /// A generic implementation of an ouroboros server protocol ready to request /// and receive transactions from a client -pub struct GenericServer( +pub struct GenericServer( State, - ChannelBuffer, + plexer::ChannelBuffer, PhantomData, PhantomData, ) where - H: Channel, Message: Fragment; /// A Cardano specific server for the ouroboros TxSubmission protocol -pub type Server = GenericServer; +pub type Server = GenericServer; -impl GenericServer +impl GenericServer where - H: Channel, Message: Fragment, { - pub fn new(channel: H) -> Self { + pub fn new(channel: plexer::AgentChannel) -> Self { Self( State::Init, - ChannelBuffer::new(channel), + plexer::ChannelBuffer::new(channel), PhantomData {}, PhantomData {}, ) @@ -95,17 +93,14 @@ where pub async fn send_message(&mut self, msg: &Message) -> Result<(), Error> { self.assert_agency_is_ours()?; self.assert_outbound_state(msg)?; - self.1 - .send_msg_chunks(msg) - .await - .map_err(Error::ChannelError)?; + self.1.send_msg_chunks(msg).await.map_err(Error::Plexer)?; Ok(()) } pub async fn recv_message(&mut self) -> Result, Error> { self.assert_agency_is_theirs()?; - let msg = self.1.recv_full_msg().await.map_err(Error::ChannelError)?; + let msg = self.1.recv_full_msg().await.map_err(Error::Plexer)?; self.assert_inbound_state(&msg)?; Ok(msg) diff --git a/pallas-network/src/plexer.rs b/pallas-network/src/plexer.rs new file mode 100644 index 0000000..b77a1f8 --- /dev/null +++ b/pallas-network/src/plexer.rs @@ -0,0 +1,317 @@ +use pallas_codec::{minicbor, Fragment}; +use thiserror::Error; +use tokio::sync::mpsc::error::SendError; +use tokio::{select, time::Instant}; +use tracing::{debug, error, trace}; + +use crate::bearer::{Bearer, Payload, Protocol, SegmentBuffer}; + +#[derive(Error, Debug)] +pub enum Error { + #[error("failure to encode channel message")] + Decoding(String), + + #[error("failure to decode channel message")] + Encoding(String), + + #[error("agent failed to enqueue chunk for protocol {0}")] + AgentEnqueue(Protocol, Payload), + + #[error("agent failed to dequeue chunk")] + AgentDequeue, + + #[error("plexer failed to dumux chunk for protocol {0}")] + PlexerDemux(Protocol, Payload), + + #[error("plexer failed to mux chunk")] + PlexerMux, + + #[error("bearer IO error")] + Bearer(tokio::io::Error), +} + +pub struct AgentChannel { + enqueue_protocol: crate::bearer::Protocol, + dequeue_protocol: crate::bearer::Protocol, + to_plexer: tokio::sync::mpsc::Sender<(Protocol, Payload)>, + from_plexer: tokio::sync::broadcast::Receiver<(Protocol, Payload)>, +} + +impl AgentChannel { + fn for_client(protocol: crate::bearer::Protocol, ingress: &Ingress, egress: &Egress) -> Self { + Self { + enqueue_protocol: protocol, + dequeue_protocol: protocol ^ 0x8000, + to_plexer: ingress.0.clone(), + from_plexer: egress.0.subscribe(), + } + } + + fn for_server(protocol: crate::bearer::Protocol, ingress: &Ingress, egress: &Egress) -> Self { + Self { + enqueue_protocol: protocol ^ 0x8000, + dequeue_protocol: protocol, + to_plexer: ingress.0.clone(), + from_plexer: egress.0.subscribe(), + } + } + + pub async fn enqueue_chunk(&mut self, chunk: Payload) -> Result<(), Error> { + self.to_plexer + .send((self.enqueue_protocol, chunk)) + .await + .map_err(|SendError((protocol, payload))| Error::AgentEnqueue(protocol, payload)) + } + + pub async fn dequeue_chunk(&mut self) -> Result { + loop { + let (protocol, payload) = self + .from_plexer + .recv() + .await + .map_err(|_| Error::AgentDequeue)?; + + if protocol == self.dequeue_protocol { + trace!(protocol, "message for our protocol"); + break Ok(payload); + } + } + } +} + +type Ingress = ( + tokio::sync::mpsc::Sender<(Protocol, Payload)>, + tokio::sync::mpsc::Receiver<(Protocol, Payload)>, +); + +type Egress = ( + tokio::sync::broadcast::Sender<(Protocol, Payload)>, + tokio::sync::broadcast::Receiver<(Protocol, Payload)>, +); + +pub struct Plexer { + clock: Instant, + bearer: SegmentBuffer, + ingress: Ingress, + egress: Egress, +} + +impl Plexer { + pub fn new(bearer: Bearer) -> Self { + Self { + clock: Instant::now(), + bearer: SegmentBuffer::new(bearer), + ingress: tokio::sync::mpsc::channel(100), // TODO: define buffer + egress: tokio::sync::broadcast::channel(100), + } + } + + async fn mux(&mut self, msg: (Protocol, Payload)) -> tokio::io::Result<()> { + self.bearer + .write_segment(msg.0, &self.clock, &msg.1) + .await?; + + if tracing::event_enabled!(tracing::Level::TRACE) { + trace!( + protocol = msg.0, + data = hex::encode(&msg.1), + "write to bearer" + ); + } + + Ok(()) + } + + async fn demux(&mut self, protocol: Protocol, payload: Payload) -> tokio::io::Result<()> { + if tracing::event_enabled!(tracing::Level::TRACE) { + trace!(protocol, data = hex::encode(&payload), "read from bearer"); + } + + self.egress.0.send((protocol, payload)).unwrap(); + + Ok(()) + } + + pub fn subscribe_client(&mut self, protocol: Protocol) -> AgentChannel { + AgentChannel::for_client(protocol, &self.ingress, &self.egress) + } + + pub fn subscribe_server(&mut self, protocol: Protocol) -> AgentChannel { + AgentChannel::for_server(protocol, &self.ingress, &self.egress) + } + + pub async fn run(&mut self) -> tokio::io::Result<()> { + loop { + trace!("selecting"); + select! { + Ok(x) = self.bearer.read_segment() => { + trace!("demux selected"); + self.demux(x.0, x.1).await? + }, + Some(x) = self.ingress.1.recv() => { + trace!("mux selected"); + self.mux(x).await? + }, + _ = tokio::time::sleep(tokio::time::Duration::from_secs(5)) => { + trace!("idle plexer"); + } + else => { + error!("something else happened"); + } + } + } + } +} + +/// Protocol value that defines max segment length +pub const MAX_SEGMENT_PAYLOAD_LENGTH: usize = 65535; + +fn try_decode_message(buffer: &mut Vec) -> Result, Error> +where + M: Fragment, +{ + let mut decoder = minicbor::Decoder::new(buffer); + let maybe_msg = decoder.decode(); + + match maybe_msg { + Ok(msg) => { + let pos = decoder.position(); + buffer.drain(0..pos); + Ok(Some(msg)) + } + Err(err) if err.is_end_of_input() => Ok(None), + Err(err) => { + error!(?err); + trace!("{}", hex::encode(buffer)); + Err(Error::Decoding(err.to_string())) + } + } +} + +/// A channel abstraction to hide the complexity of partial payloads +pub struct ChannelBuffer { + channel: AgentChannel, + temp: Vec, +} + +impl ChannelBuffer { + pub fn new(channel: AgentChannel) -> Self { + Self { + channel, + temp: Vec::new(), + } + } + + /// Enqueues a msg as a sequence payload chunks + pub async fn send_msg_chunks(&mut self, msg: &M) -> Result<(), Error> + where + M: Fragment, + { + let mut payload = Vec::new(); + minicbor::encode(msg, &mut payload).map_err(|err| Error::Encoding(err.to_string()))?; + + let chunks = payload.chunks(MAX_SEGMENT_PAYLOAD_LENGTH); + + for chunk in chunks { + self.channel.enqueue_chunk(Vec::from(chunk)).await?; + } + + Ok(()) + } + + /// Reads from the channel until a complete message is found + pub async fn recv_full_msg(&mut self) -> Result + where + M: Fragment, + { + trace!(len = self.temp.len(), "waiting for full message"); + + if !self.temp.is_empty() { + trace!("buffer has data from previous payload"); + + if let Some(msg) = try_decode_message::(&mut self.temp)? { + debug!("decoding done"); + return Ok(msg); + } + } + + loop { + let chunk = self.channel.dequeue_chunk().await?; + self.temp.extend(chunk); + + if let Some(msg) = try_decode_message::(&mut self.temp)? { + debug!("decoding done"); + return Ok(msg); + } + + trace!("not enough data"); + } + } + + pub fn unwrap(self) -> AgentChannel { + self.channel + } +} + +impl From for ChannelBuffer { + fn from(channel: AgentChannel) -> Self { + ChannelBuffer::new(channel) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pallas_codec::minicbor; + + #[tokio::test] + async fn multiple_messages_in_same_payload() { + let mut input = Vec::new(); + let in_part1 = (1u8, 2u8, 3u8); + let in_part2 = (6u8, 5u8, 4u8); + + minicbor::encode(in_part1, &mut input).unwrap(); + minicbor::encode(in_part2, &mut input).unwrap(); + + let ingress = tokio::sync::mpsc::channel(100); + let egress = tokio::sync::broadcast::channel(100); + + let channel = AgentChannel::for_client(0, &ingress, &egress); + + egress.0.send((0 ^ 0x8000, input)).unwrap(); + + let mut buf = ChannelBuffer::new(channel); + + let out_part1 = buf.recv_full_msg::<(u8, u8, u8)>().await.unwrap(); + let out_part2 = buf.recv_full_msg::<(u8, u8, u8)>().await.unwrap(); + + assert_eq!(in_part1, out_part1); + assert_eq!(in_part2, out_part2); + } + + #[tokio::test] + async fn fragmented_message_in_multiple_payloads() { + let mut input = Vec::new(); + let msg = (11u8, 12u8, 13u8, 14u8, 15u8, 16u8, 17u8); + minicbor::encode(msg, &mut input).unwrap(); + + let ingress = tokio::sync::mpsc::channel(100); + let egress = tokio::sync::broadcast::channel(100); + + let channel = AgentChannel::for_client(0, &ingress, &egress); + + while !input.is_empty() { + let chunk = Vec::from(input.drain(0..2).as_slice()); + egress.0.send((0 ^ 0x8000, chunk)).unwrap(); + } + + let mut buf = ChannelBuffer::new(channel); + + let out_msg = buf + .recv_full_msg::<(u8, u8, u8, u8, u8, u8, u8)>() + .await + .unwrap(); + + assert_eq!(msg, out_msg); + } +} diff --git a/pallas-network/tests/plexer.rs b/pallas-network/tests/plexer.rs new file mode 100644 index 0000000..2f5f70c --- /dev/null +++ b/pallas-network/tests/plexer.rs @@ -0,0 +1,62 @@ +use std::net::{Ipv4Addr, SocketAddrV4}; + +use pallas_network::{bearer::Bearer, plexer::Plexer}; +use rand::{distributions::Uniform, Rng}; +use tokio::net::TcpListener; + +async fn setup_passive_muxer() -> Plexer { + let server = TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, P)) + .await + .unwrap(); + + println!("listening for connections on port {}", P); + + let (bearer, _) = Bearer::accept_tcp(server).await.unwrap(); + + Plexer::new(bearer) +} + +async fn setup_active_muxer() -> Plexer { + let bearer = Bearer::connect_tcp(SocketAddrV4::new(Ipv4Addr::LOCALHOST, P)) + .await + .unwrap(); + + println!("active plexer connected"); + + Plexer::new(bearer) +} + +fn random_payload(size: usize) -> Vec { + let range = Uniform::from(0..255); + rand::thread_rng().sample_iter(&range).take(size).collect() +} + +#[tokio::test] +async fn one_way_small_sequence_of_payloads() { + let passive = tokio::spawn(setup_passive_muxer::<50301>()); + + // HACK: a small sleep seems to be required for Github actions runner to + // formally expose the port + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let mut active = setup_active_muxer::<50301>().await; + + let mut passive = passive.await.unwrap(); + + let mut sender_channel = active.subscribe_client(3); + let mut receiver_channel = passive.subscribe_server(3); + + let passive_run = tokio::spawn(async move { passive.run().await }); + let active_run = tokio::spawn(async move { active.run().await }); + + for _ in 0..100 { + let payload = random_payload(50); + println!("sending chunk"); + sender_channel.enqueue_chunk(payload.clone()).await.unwrap(); + let received_payload = receiver_channel.dequeue_chunk().await.unwrap(); + assert_eq!(payload, received_payload); + } + + passive_run.abort(); + active_run.abort(); +} diff --git a/pallas-network/tests/protocols.rs b/pallas-network/tests/protocols.rs new file mode 100644 index 0000000..8f6d922 --- /dev/null +++ b/pallas-network/tests/protocols.rs @@ -0,0 +1,146 @@ +use pallas_network::facades::PeerClient; +use pallas_network::miniprotocols::{ + blockfetch, + chainsync::{self, NextResponse}, + Point, +}; + +#[tokio::test] +#[ignore] +pub async fn chainsync_history_happy_path() { + let mut peer = PeerClient::connect("preview-node.world.dev.cardano.org:30002", 2) + .await + .unwrap(); + + let client = peer.chainsync(); + + let known_point = Point::Specific( + 1654413, + hex::decode("7de1f036df5a133ce68a82877d14354d0ba6de7625ab918e75f3e2ecb29771c2").unwrap(), + ); + + let (point, _) = client + .find_intersect(vec![known_point.clone()]) + .await + .unwrap(); + + println!("{:?}", point); + + assert!(matches!(client.state(), chainsync::State::Idle)); + + match point { + Some(point) => assert_eq!(point, known_point), + None => panic!("expected point"), + } + + let next = client.request_next().await.unwrap(); + + match next { + NextResponse::RollBackward(point, _) => assert_eq!(point, known_point), + _ => panic!("expected rollback"), + } + + assert!(matches!(client.state(), chainsync::State::Idle)); + + for _ in 0..10 { + let next = client.request_next().await.unwrap(); + + match next { + NextResponse::RollForward(_, _) => (), + _ => panic!("expected roll-forward"), + } + + assert!(matches!(client.state(), chainsync::State::Idle)); + } + + client.send_done().await.unwrap(); + + assert!(matches!(client.state(), chainsync::State::Done)); +} + +#[tokio::test] +#[ignore] +pub async fn chainsync_tip_happy_path() { + let mut peer = PeerClient::connect("preview-node.world.dev.cardano.org:30002", 2) + .await + .unwrap(); + + let client = peer.chainsync(); + + client.intersect_tip().await.unwrap(); + + assert!(matches!(client.state(), chainsync::State::Idle)); + + let next = client.request_next().await.unwrap(); + + assert!(matches!(next, NextResponse::RollBackward(..))); + + let mut await_count = 0; + + for _ in 0..4 { + let next = if client.has_agency() { + client.request_next().await.unwrap() + } else { + await_count += 1; + client.recv_while_must_reply().await.unwrap() + }; + + match next { + NextResponse::RollForward(_, _) => (), + NextResponse::Await => (), + _ => panic!("expected roll-forward or await"), + } + } + + assert!(await_count > 0, "tip was never reached"); + + client.send_done().await.unwrap(); + + assert!(matches!(client.state(), chainsync::State::Done)); +} + +#[tokio::test] +#[ignore] +pub async fn blockfetch_happy_path() { + let mut peer = PeerClient::connect("preview-node.world.dev.cardano.org:30002", 2) + .await + .unwrap(); + + let client = peer.blockfetch(); + + let known_point = Point::Specific( + 1654413, + hex::decode("7de1f036df5a133ce68a82877d14354d0ba6de7625ab918e75f3e2ecb29771c2").unwrap(), + ); + + let range_ok = client + .request_range((known_point.clone(), known_point)) + .await; + + assert!(matches!(client.state(), blockfetch::State::Streaming)); + + println!("streaming..."); + + assert!(matches!(range_ok, Ok(_))); + + for _ in 0..1 { + let next = client.recv_while_streaming().await.unwrap(); + + match next { + Some(body) => assert_eq!(body.len(), 3251), + _ => panic!("expected block body"), + } + + assert!(matches!(client.state(), blockfetch::State::Streaming)); + } + + let next = client.recv_while_streaming().await.unwrap(); + + assert!(matches!(next, None)); + + client.send_done().await.unwrap(); + + assert!(matches!(client.state(), blockfetch::State::Done)); +} + +// TODO: redo txsubmission client test diff --git a/pallas-upstream/Cargo.toml b/pallas-upstream/Cargo.toml index 18be522..16aba42 100644 --- a/pallas-upstream/Cargo.toml +++ b/pallas-upstream/Cargo.toml @@ -11,18 +11,16 @@ readme = "README.md" authors = ["Santiago Carmuega "] [dependencies] +async-trait = "0.1.68" byteorder = "1.4.3" gasket = { git = "https://github.com/construkts/gasket-rs" } # gasket = { path = "../../../construkts/gasket-rs" } hex = "0.4.3" -mio = { version = "0.8.6", features = ["net", "os-poll"] } # gasket = { version = "0.1.0", path = "../../../construkts/gasket-rs" } pallas-codec = { version = "0.18.0", path = "../pallas-codec" } pallas-crypto = { version = "0.18.0", path = "../pallas-crypto" } -pallas-miniprotocols = { version = "0.18.0", path = "../pallas-miniprotocols" } -pallas-multiplexer = { version = "0.18.0", path = "../pallas-multiplexer" } +pallas-network = { version = "0.18.0", path = "../pallas-network" } pallas-traverse = { version = "0.18.0", path = "../pallas-traverse" } -rayon = "1.7.0" serde = { version = "1.0.154", features = ["derive"] } thiserror = "1.0.31" tokio = { version = "1", features = ["net", "macros", "io-util"] } diff --git a/pallas-upstream/src/api.rs b/pallas-upstream/src/api.rs deleted file mode 100644 index 1193875..0000000 --- a/pallas-upstream/src/api.rs +++ /dev/null @@ -1,126 +0,0 @@ -pub use crate::framework::{BlockFetchEvent, Cursor, DownstreamPort, Intersection}; - -pub mod n2n { - use crate::{blockfetch, chainsync, framework::*, plexer}; - - use gasket::{ - messaging::{SendAdapter, SendPort}, - runtime::Tether, - }; - - pub struct Runtime { - pub plexer_tether: Tether, - pub chainsync_tether: Tether, - pub blockfetch_tether: Tether, - } - - pub struct Bootstrapper - where - A: SendAdapter, - C: Cursor, - { - cursor: C, - peer_address: String, - network_magic: u64, - output: super::DownstreamPort, - } - - impl Bootstrapper - where - A: SendAdapter + 'static, - C: Cursor + 'static, - { - pub fn new(cursor: C, peer_address: String, network_magic: u64) -> Self { - Bootstrapper { - cursor, - peer_address, - network_magic, - output: Default::default(), - } - } - - pub fn connect_output(&mut self, adapter: A) { - self.output.connect(adapter); - } - - pub fn spawn(self) -> Result { - /* - TODO: this is how we envision the setup of complex pipelines leveraging Rust macros: - - pipeline!( - plexer = plexer::Worker::new(xx), - chainsync = chainsync::Worker::new(yy), - blockfetch = blockfetch::Worker::new(yy), - reducer = reducer::Worker::new(yy), - plexer.demux2 => chainsync.demux2, - plexer.demux3 => blockfetch.demux3, - chainsync.mux2 + blockfetch.mux3 => plexer.mux, - chainsync.downstream => blockfetch.upstream, - blockfetch.downstream => reducer.upstream, - ); - - The above snippet would replace the rest of the code in this function, which is just a more verbose, manual way of saying the same thing. - */ - - let mut mux_input = MuxInputPort::default(); - - let mut demux2_out = DemuxOutputPort::default(); - let mut demux2_in = DemuxInputPort::default(); - gasket::messaging::tokio::connect_ports(&mut demux2_out, &mut demux2_in, 1000); - - let mut demux3_out = DemuxOutputPort::default(); - let mut demux3_in = DemuxInputPort::default(); - gasket::messaging::tokio::connect_ports(&mut demux3_out, &mut demux3_in, 1000); - - let mut mux2_out = MuxOutputPort::default(); - let mut mux3_out = MuxOutputPort::default(); - gasket::messaging::tokio::funnel_ports( - vec![&mut mux2_out, &mut mux3_out], - &mut mux_input, - 1000, - ); - - let mut chainsync_downstream = chainsync::DownstreamPort::default(); - let mut blockfetch_upstream = blockfetch::UpstreamPort::default(); - gasket::messaging::tokio::connect_ports( - &mut chainsync_downstream, - &mut blockfetch_upstream, - 100, - ); - - let plexer_tether = gasket::runtime::spawn_stage( - plexer::Worker::new( - self.peer_address, - self.network_magic, - mux_input, - Some(demux2_out), - Some(demux3_out), - ), - gasket::runtime::Policy::default(), - Some("plexer"), - ); - - let channel2 = ProtocolChannel(2, mux2_out, demux2_in); - - let chainsync_tether = gasket::runtime::spawn_stage( - chainsync::Worker::new(self.cursor, channel2, chainsync_downstream), - gasket::runtime::Policy::default(), - Some("chainsync"), - ); - - let channel3 = ProtocolChannel(3, mux3_out, demux3_in); - - let blockfetch_tether = gasket::runtime::spawn_stage( - blockfetch::Worker::new(channel3, blockfetch_upstream, self.output), - gasket::runtime::Policy::default(), - Some("blockfetch"), - ); - - Ok(Runtime { - plexer_tether, - chainsync_tether, - blockfetch_tether, - }) - } - } -} diff --git a/pallas-upstream/src/blockfetch.rs b/pallas-upstream/src/blockfetch.rs deleted file mode 100644 index 3df0b27..0000000 --- a/pallas-upstream/src/blockfetch.rs +++ /dev/null @@ -1,106 +0,0 @@ -use gasket::messaging::SendAdapter; -use gasket::runtime::WorkSchedule; -use tracing::{error, info, instrument}; - -use pallas_crypto::hash::Hash; -use pallas_miniprotocols::blockfetch; -use pallas_miniprotocols::Point; - -use crate::framework::*; - -pub type UpstreamPort = gasket::messaging::tokio::InputPort; -pub type OuroborosClient = blockfetch::Client; - -pub struct Worker -where - T: Send + Sync, -{ - client: OuroborosClient, - upstream: UpstreamPort, - downstream: DownstreamPort, - block_count: gasket::metrics::Counter, -} - -impl Worker -where - T: Send + Sync, -{ - pub fn new( - plexer: ProtocolChannel, - upstream: UpstreamPort, - downstream: DownstreamPort, - ) -> Self { - let client = OuroborosClient::new(plexer); - - Self { - client, - upstream, - downstream, - block_count: Default::default(), - } - } - - #[instrument(skip(self), fields(slot, %hash))] - async fn fetch_block( - &mut self, - slot: u64, - hash: &Hash<32>, - ) -> Result, gasket::error::Error> { - info!("fetching block"); - - match self - .client - .fetch_single(Point::Specific(slot, hash.to_vec())) - .await - { - Ok(x) => { - info!("block fetch succeeded"); - Ok(x) - } - Err(blockfetch::Error::ChannelError(x)) => { - error!("plexer channel error: {}", x); - Err(gasket::error::Error::RetryableError) - } - Err(x) => { - error!("unrecoverable block fetch error: {}", x); - Err(gasket::error::Error::WorkPanic) - } - } - } -} - -impl gasket::runtime::Worker for Worker -where - A: SendAdapter, -{ - fn metrics(&self) -> gasket::metrics::Registry { - gasket::metrics::Builder::new() - .with_counter("fetched_blocks", &self.block_count) - .build() - } - - type WorkUnit = ChainSyncEvent; - - async fn schedule(&mut self) -> gasket::runtime::ScheduleResult { - let msg = self.upstream.recv().await?; - info!("scheduling block betch"); - Ok(WorkSchedule::Unit(msg.payload)) - } - - async fn execute(&mut self, unit: &Self::WorkUnit) -> Result<(), gasket::error::Error> { - let output = match unit { - ChainSyncEvent::RollForward(s, h) => { - let body = self.fetch_block(*s, h).await?; - - self.block_count.inc(1); - - BlockFetchEvent::RollForward(*s, h.clone(), body) - } - ChainSyncEvent::Rollback(x) => BlockFetchEvent::Rollback(x.clone()), - }; - - self.downstream.send(output.into()).await?; - - Ok(()) - } -} diff --git a/pallas-upstream/src/chainsync.rs b/pallas-upstream/src/chainsync.rs deleted file mode 100644 index 4db09ba..0000000 --- a/pallas-upstream/src/chainsync.rs +++ /dev/null @@ -1,176 +0,0 @@ -use gasket::error::AsWorkError; -use tracing::{debug, info}; - -use pallas_miniprotocols::chainsync::{HeaderContent, NextResponse, Tip}; -use pallas_miniprotocols::{chainsync, Point}; -use pallas_traverse::MultiEraHeader; - -use crate::framework::*; - -fn to_traverse(header: &chainsync::HeaderContent) -> Result, Error> { - let out = match header.byron_prefix { - Some((subtag, _)) => MultiEraHeader::decode(header.variant, Some(subtag), &header.cbor), - None => MultiEraHeader::decode(header.variant, None, &header.cbor), - }; - - out.map_err(Error::parse) -} - -pub type DownstreamPort = gasket::messaging::tokio::OutputPort; - -pub type OuroborosClient = chainsync::N2NClient; - -pub struct Worker -where - C: Cursor, -{ - chain_cursor: C, - client: OuroborosClient, - downstream: DownstreamPort, - block_count: gasket::metrics::Counter, - chain_tip: gasket::metrics::Gauge, -} - -impl Worker -where - C: Cursor, -{ - pub fn new(chain_cursor: C, plexer: ProtocolChannel, downstream: DownstreamPort) -> Self { - let client = OuroborosClient::new(plexer); - - Self { - chain_cursor, - client, - downstream, - block_count: Default::default(), - chain_tip: Default::default(), - } - } - - fn notify_tip(&self, tip: Tip) { - self.chain_tip.set(tip.0.slot_or_default() as i64); - } - - async fn intersect(&mut self) -> Result<(), gasket::error::Error> { - let value = self.chain_cursor.intersection(); - - let intersect = match value { - Intersection::Origin => { - info!("intersecting origin"); - self.client.intersect_origin().await.or_restart()?.into() - } - Intersection::Tip => { - info!("intersecting tip"); - self.client.intersect_tip().await.or_restart()?.into() - } - Intersection::Breadcrumbs(points) => { - info!("intersecting breadcrumbs"); - let (point, tip) = self - .client - .find_intersect(Vec::from(points)) - .await - .or_restart()?; - - self.notify_tip(tip); - - point - } - }; - - info!(?intersect, "intersected"); - - Ok(()) - } - - async fn process_next( - &mut self, - next: NextResponse, - ) -> Result<(), gasket::error::Error> { - match next { - chainsync::NextResponse::RollForward(header, tip) => { - let header = to_traverse(&header).or_panic()?; - - debug!(slot = header.slot(), hash = %header.hash(), "chain sync roll forward"); - - self.downstream - .send(ChainSyncEvent::RollForward(header.slot(), header.hash()).into()) - .await?; - - self.notify_tip(tip); - - Ok(()) - } - chainsync::NextResponse::RollBackward(point, tip) => { - match &point { - Point::Origin => debug!("rollback to origin"), - Point::Specific(slot, _) => debug!(slot, "rollback"), - }; - - self.downstream - .send(ChainSyncEvent::Rollback(point).into()) - .await?; - - self.notify_tip(tip); - - Ok(()) - } - chainsync::NextResponse::Await => { - info!("chain-sync reached the tip of the chain"); - Ok(()) - } - } - } - - async fn request_next(&mut self) -> Result<(), gasket::error::Error> { - info!("requesting next block"); - let next = self.client.request_next().await.or_restart()?; - self.process_next(next).await - } - - async fn await_next(&mut self) -> Result<(), gasket::error::Error> { - info!("awaiting next block (blocking)"); - let next = self.client.recv_while_must_reply().await.or_restart()?; - self.process_next(next).await - } -} - -pub enum WorkUnit { - Intersect, - RequestNext, - AwaitNext, -} - -impl gasket::runtime::Worker for Worker -where - C: Cursor + Sync + Send, -{ - type WorkUnit = WorkUnit; - - fn metrics(&self) -> gasket::metrics::Registry { - gasket::metrics::Builder::new() - .with_counter("received_blocks", &self.block_count) - .with_gauge("chain_tip", &self.chain_tip) - .build() - } - - async fn bootstrap(&mut self) -> gasket::runtime::ScheduleResult { - Ok(gasket::runtime::WorkSchedule::Unit(WorkUnit::Intersect)) - } - - async fn schedule(&mut self) -> gasket::runtime::ScheduleResult { - match self.client.has_agency() { - true => Ok(gasket::runtime::WorkSchedule::Unit(WorkUnit::RequestNext)), - false => Ok(gasket::runtime::WorkSchedule::Unit(WorkUnit::AwaitNext)), - } - } - - async fn execute(&mut self, unit: &Self::WorkUnit) -> Result<(), gasket::error::Error> { - match unit { - WorkUnit::Intersect => self.intersect().await?, - WorkUnit::RequestNext => self.request_next().await?, - WorkUnit::AwaitNext => self.await_next().await?, - }; - - Ok(()) - } -} diff --git a/pallas-upstream/src/framework.rs b/pallas-upstream/src/framework.rs index 9c02397..a81d229 100644 --- a/pallas-upstream/src/framework.rs +++ b/pallas-upstream/src/framework.rs @@ -1,8 +1,5 @@ use pallas_crypto::hash::Hash; -use pallas_miniprotocols::Point; -use pallas_multiplexer as multiplexer; -use thiserror::Error; -use tracing::{error, trace}; +use pallas_network::miniprotocols::Point; pub type BlockSlot = u64; pub type BlockHash = Hash<32>; @@ -20,112 +17,10 @@ pub trait Cursor: Send + Sync { } #[derive(Debug, Clone)] -pub enum ChainSyncEvent { - RollForward(BlockSlot, BlockHash), - Rollback(Point), -} - -#[derive(Debug, Clone)] -pub enum BlockFetchEvent { +pub enum UpstreamEvent { RollForward(BlockSlot, BlockHash, RawBlock), Rollback(Point), } -// ports used by plexer -pub type MuxOutputPort = gasket::messaging::tokio::OutputPort<(u16, multiplexer::Payload)>; -pub type DemuxInputPort = gasket::messaging::tokio::InputPort; - -// ports used by mini-protocols -pub type MuxInputPort = gasket::messaging::tokio::InputPort<(u16, multiplexer::Payload)>; -pub type DemuxOutputPort = gasket::messaging::tokio::OutputPort; - // final output port -pub type DownstreamPort = gasket::messaging::OutputPort; - -pub struct ProtocolChannel(pub u16, pub MuxOutputPort, pub DemuxInputPort); - -impl multiplexer::agents::Channel for ProtocolChannel { - async fn enqueue_chunk( - &mut self, - payload: multiplexer::Payload, - ) -> Result<(), multiplexer::agents::ChannelError> { - trace!( - protocol = self.0, - payload = hex::encode(&payload), - "enqueing" - ); - - let res = self - .1 - .send(gasket::messaging::Message::from((self.0, payload))) - .await; - - match res { - Ok(_) => Ok(()), - Err(error) => { - error!(?error, "enqueue chunk failed"); - Err(multiplexer::agents::ChannelError::NotConnected(None)) - } - } - } - - async fn dequeue_chunk( - &mut self, - ) -> Result { - let res = self.2.recv().await; - - match res { - Ok(msg) => Ok(msg.payload), - Err(error) => { - error!(?error, "dequeue chunk failed"); - Err(multiplexer::agents::ChannelError::NotConnected(None)) - } - } - } -} - -#[derive(Error, Debug)] -pub enum Error { - #[error("{0}")] - Client(String), - - #[error("{0}")] - Parse(String), - - #[error("{0}")] - Server(String), - - #[error("{0}")] - Message(String), - - #[error("{0}")] - Custom(String), -} - -impl Error { - pub fn client(error: impl ToString) -> Error { - Error::Client(error.to_string()) - } - - pub fn parse(error: impl ToString) -> Error { - Error::Parse(error.to_string()) - } - - pub fn server(error: impl ToString) -> Error { - Error::Server(error.to_string()) - } - - pub fn message(error: impl ToString) -> Error { - Error::Message(error.to_string()) - } - - pub fn custom(error: impl Into>) -> Error { - Error::Custom(format!("{}", error.into())) - } -} - -impl From> for Error { - fn from(err: Box) -> Self { - Error::custom(err) - } -} +pub type DownstreamPort = gasket::messaging::OutputPort; diff --git a/pallas-upstream/src/lib.rs b/pallas-upstream/src/lib.rs index f0a274c..bf7234b 100644 --- a/pallas-upstream/src/lib.rs +++ b/pallas-upstream/src/lib.rs @@ -1,10 +1,8 @@ -#![feature(async_fn_in_trait)] - -pub(crate) mod blockfetch; -pub(crate) mod chainsync; pub(crate) mod framework; -pub(crate) mod plexer; +pub(crate) mod worker; -mod api; +pub use crate::framework::{Cursor, DownstreamPort, Intersection, UpstreamEvent}; -pub use api::*; +pub mod n2n { + pub use crate::worker::Worker; +} diff --git a/pallas-upstream/src/plexer.rs b/pallas-upstream/src/plexer.rs deleted file mode 100644 index b0f51b9..0000000 --- a/pallas-upstream/src/plexer.rs +++ /dev/null @@ -1,436 +0,0 @@ -use std::future::ready; - -use byteorder::{ByteOrder, NetworkEndian}; -use gasket::error::AsWorkError; -use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf, ReadHalf, WriteHalf}; -use tokio::net::{TcpStream, ToSocketAddrs}; -use tokio::select; -use tokio::time::Instant; -use tracing::{debug, error, info, trace, warn}; - -use pallas_miniprotocols::handshake; - -use crate::framework::*; - -const HEADER_LEN: usize = 8; - -pub type Timestamp = u32; - -pub type Payload = Vec; - -pub type Protocol = u16; - -/// A `Header` struct represents an Ouroboros segment header. -/// -/// # Examples -/// -/// Converting a `Header` to bytes: -/// -/// ``` -/// use byteorder::{BigEndian, ByteOrder}; -/// use pallas_upstream::plexer::Header; -/// -/// let header = Header { -/// protocol: 0x01, -/// timestamp: 1619804871, -/// payload_len: 42, -/// }; -/// -/// let header_bytes: [u8; 8] = header.into(); -/// assert_eq!(header_bytes, [97, 75, 168, 15, 128, 1, 0, 42]); -/// ``` -/// -/// Converting bytes to a `Header`: -/// -/// ``` -/// use byteorder::{BigEndian, ByteOrder}; -/// use pallas_upstream::plexer::Header; -/// -/// let bytes = [97, 75, 168, 15, 128, 1, 0, 42]; -/// let header: Header = (&bytes[..]).into(); -/// -/// assert_eq!(header.protocol, 0x01); -/// assert_eq!(header.timestamp, 1619804871); -/// assert_eq!(header.payload_len, 42); -/// ``` -#[derive(Debug)] -pub struct Header { - pub protocol: Protocol, - pub timestamp: Timestamp, - pub payload_len: u16, -} - -impl From<&[u8]> for Header { - fn from(value: &[u8]) -> Self { - let timestamp = NetworkEndian::read_u32(&value[0..4]); - let protocol = NetworkEndian::read_u16(&value[4..6]) ^ 0x8000; - let payload_len = NetworkEndian::read_u16(&value[6..8]); - - Self { - timestamp, - protocol, - payload_len, - } - } -} - -impl From
for [u8; 8] { - fn from(value: Header) -> Self { - let mut out = [0u8; 8]; - NetworkEndian::write_u32(&mut out[0..4], value.timestamp); - NetworkEndian::write_u16(&mut out[4..6], value.protocol); - NetworkEndian::write_u16(&mut out[6..8], value.payload_len); - - out - } -} - -pub struct Segment { - pub header: Header, - pub payload: Payload, -} - -use tokio::io::{AsyncReadExt, AsyncWriteExt}; - -struct AsyncBearer(OwnedReadHalf, OwnedWriteHalf, Instant); - -impl AsyncBearer { - async fn connect_tcp(addr: impl ToSocketAddrs) -> Result { - let stream = TcpStream::connect(addr).await?; - let (read, write) = stream.into_split(); - - Ok(Self(read, write, Instant::now())) - } -} - -impl AsyncBearer { - async fn readable(&self) -> tokio::io::Result<()> { - self.0.readable().await - } - - /// Peek the available data in search for a frame header - async fn peek_header(&mut self) -> tokio::io::Result> { - let mut buf = [0u8; HEADER_LEN]; - let len = self.0.peek(&mut buf).await?; - - if len < HEADER_LEN { - return Ok(None); - } - - Ok(Some(Header::from(buf.as_slice()))) - } - - async fn has_payload(&mut self, payload_len: usize) -> tokio::io::Result { - let segment_size = HEADER_LEN + payload_len; - let mut buf = vec![0u8; segment_size]; - - let available = self.0.peek(&mut buf).await?; - - return Ok(available >= segment_size); - } - - /// Peeks the bearer to see if a full segment is available to be read - async fn has_segment(&mut self) -> std::io::Result { - let header = match self.peek_header().await? { - Some(x) => x, - None => return Ok(false), - }; - - self.has_payload(header.payload_len as usize).await - } - - /// Reads a full segment from the bearer while consuming the bytes - /// - /// This function is NOT "cancel safe", meaning that it shouldn't be used - /// inside the context of a select!. Only call this function once you're - /// sure that you can await until all the required bytes are available. - async fn read_segment(&mut self) -> tokio::io::Result<(Protocol, Payload)> { - let mut buf = [0u8; HEADER_LEN]; - self.0.read_exact(&mut buf).await?; - let header = Header::from(buf.as_slice()); - - // TODO: assert any business invariants regarding timestamp from the other party - - let mut payload = vec![0u8; header.payload_len as usize]; - self.0.read_exact(&mut payload).await?; - - Ok((header.protocol, payload)) - } - - async fn write_segment(&mut self, protocol: u16, payload: &[u8]) -> Result<(), std::io::Error> { - let header = Header { - protocol, - timestamp: self.2.elapsed().as_micros() as u32, - payload_len: payload.len() as u16, - }; - - let buf: [u8; 8] = header.into(); - self.1.write_all(&buf).await?; - - self.1.write_all(&payload).await?; - - Ok(()) - } -} - -pub struct AsyncAgentChannel( - Protocol, - tokio::sync::mpsc::Sender<(Protocol, Payload)>, - tokio::sync::broadcast::Receiver<(Protocol, Payload)>, -); - -impl pallas_multiplexer::agents::Channel for AsyncAgentChannel { - async fn enqueue_chunk( - &mut self, - chunk: pallas_multiplexer::Payload, - ) -> Result<(), pallas_multiplexer::agents::ChannelError> { - let res = self.1.send((self.0, chunk)).await; - - res.map_err(|err| pallas_multiplexer::agents::ChannelError::NotConnected(Some(err.0 .1))) - } - - async fn dequeue_chunk( - &mut self, - ) -> Result { - loop { - let (protocol, payload) = self - .2 - .recv() - .await - .map_err(|err| pallas_multiplexer::agents::ChannelError::NotConnected(None))?; - - if protocol == self.0 { - break Ok(payload); - } - } - } -} - -pub type AsyncIngress = ( - tokio::sync::mpsc::Sender<(Protocol, Payload)>, - tokio::sync::mpsc::Receiver<(Protocol, Payload)>, -); -pub type AsyncEgress = ( - tokio::sync::broadcast::Sender<(Protocol, Payload)>, - tokio::sync::broadcast::Receiver<(Protocol, Payload)>, -); - -struct AsyncPlexer { - bearer: AsyncBearer, - ingress: AsyncIngress, - egress: AsyncEgress, -} - -impl AsyncPlexer { - pub fn new(bearer: AsyncBearer) -> Self { - Self { - bearer, - ingress: tokio::sync::mpsc::channel(100), // TODO: define buffer - egress: tokio::sync::broadcast::channel(100), - } - } - - async fn mux(&mut self, msg: (Protocol, Payload)) -> tokio::io::Result<()> { - self.bearer.write_segment(msg.0, &msg.1).await?; - - Ok(()) - } - - async fn demux(&mut self) -> tokio::io::Result<()> { - let (protocol, payload) = self.bearer.read_segment().await?; - - self.egress.0.send((protocol, payload)).unwrap(); - - Ok(()) - } - - pub fn subscribe(&mut self, protocol: Protocol) -> AsyncAgentChannel { - let agent_tx = self.ingress.0.clone(); - let agent_rx = self.egress.0.subscribe(); - - AsyncAgentChannel(protocol, agent_tx, agent_rx) - } - - pub async fn run(&mut self) -> tokio::io::Result<()> { - loop { - select! { - Ok(_) = self.bearer.readable() => { - if let Ok(true) = self.bearer.has_segment().await { - trace!("demux selected"); - self.demux().await? - } - }, - Some(x) = self.ingress.1.recv() => { - trace!("mux selected"); - self.mux(x).await? - }, - } - } - } -} - -impl From for AsyncPlexer { - fn from(value: AsyncBearer) -> Self { - Self::new(value) - } -} - -impl From for AsyncBearer { - fn from(value: AsyncPlexer) -> Self { - value.bearer - } -} - -async fn handshake( - plexer: &mut AsyncPlexer, - network_magic: u64, -) -> Result<(), gasket::error::Error> { - info!("executing handshake"); - - let channel0 = plexer.subscribe(0); - let versions = handshake::n2n::VersionTable::v7_and_above(network_magic); - let mut client = handshake::Client::new(channel0); - - //let p = tokio::spawn(plexer.run()); - //let output = client.handshake(versions).or_restart()?; - - let output = select! { - x = client.handshake(versions) => x.or_restart()?, - x = plexer.run() => { - match x.or_restart() { - Err(x) => return Err(x), - _ => unreachable!(), - }; - }, - }; - - debug!("handshake output: {:?}", output); - //p.abort(); - - match output { - handshake::Confirmation::Accepted(version, _) => { - info!(version, "connected to upstream peer"); - Ok(()) - } - _ => { - error!("couldn't agree on handshake version"); - Err(gasket::error::Error::WorkPanic) - } - } -} - -pub struct Worker { - peer_address: String, - network_magic: u64, - bearer: Option, - mux_input: MuxInputPort, - channel2_out: Option, - channel3_out: Option, - ops_count: gasket::metrics::Counter, -} - -impl Worker { - pub fn new( - peer_address: String, - network_magic: u64, - mux_input: MuxInputPort, - channel2_out: Option, - channel3_out: Option, - ) -> Self { - Self { - peer_address, - network_magic, - channel2_out, - channel3_out, - mux_input, - bearer: None, - ops_count: Default::default(), - } - } -} - -pub enum WorkUnit { - Connect, - Mux((u16, Vec)), - Demux, -} - -impl gasket::runtime::Worker for Worker { - type WorkUnit = WorkUnit; - - fn metrics(&self) -> gasket::metrics::Registry { - // TODO: define networking metrics (bytes in / out, etc) - gasket::metrics::Builder::new() - .with_counter("ops_count", &self.ops_count) - .build() - } - - async fn bootstrap(&mut self) -> gasket::runtime::ScheduleResult { - Ok(gasket::runtime::WorkSchedule::Unit(WorkUnit::Connect)) - } - - async fn schedule(&mut self) -> gasket::runtime::ScheduleResult { - let bearer = self.bearer.as_mut().unwrap(); - trace!("selecting"); - select! { - Ok(msg) = self.mux_input.recv() => { Ok(gasket::runtime::WorkSchedule::Unit(WorkUnit::Mux(msg.payload))) } - Ok(true) = bearer.has_segment() => Ok(gasket::runtime::WorkSchedule::Unit(WorkUnit::Demux)), - _ = tokio::time::sleep(tokio::time::Duration::from_secs(5)) => Ok(gasket::runtime::WorkSchedule::Idle), - } - } - - async fn execute(&mut self, unit: &Self::WorkUnit) -> Result<(), gasket::error::Error> { - match unit { - WorkUnit::Connect => { - debug!("connecting"); - let bearer = AsyncBearer::connect_tcp(&self.peer_address) - .await - .or_retry()?; - - let mut plexer = bearer.into(); - - handshake(&mut plexer, self.network_magic).await?; - - self.bearer = Some(plexer.into()); - } - WorkUnit::Mux(x) => { - trace!("muxing"); - self.bearer - .as_mut() - .unwrap() - .write_segment(x.0, &x.1) - .await - .or_restart()?; - } - WorkUnit::Demux => { - trace!("demuxing"); - - let (protocol, payload) = self - .bearer - .as_mut() - .unwrap() - .read_segment() - .await - .or_restart()?; - - match protocol { - 2 => { - if let Some(channel) = &mut self.channel2_out { - channel.send(payload.into()).await?; - trace!("sent protocol 2 msg"); - } - } - 3 => { - if let Some(channel) = &mut self.channel3_out { - channel.send(payload.into()).await?; - trace!("sent protocol 3 msg"); - } - } - x => warn!("trying to demux unexpected protocol {x}"), - } - } - }; - - Ok(()) - } -} diff --git a/pallas-upstream/src/worker.rs b/pallas-upstream/src/worker.rs new file mode 100644 index 0000000..a050cdf --- /dev/null +++ b/pallas-upstream/src/worker.rs @@ -0,0 +1,197 @@ +use gasket::error::AsWorkError; +use tracing::{debug, info}; + +use pallas_network::facades::PeerClient; +use pallas_network::miniprotocols::chainsync::{self, HeaderContent, NextResponse, Tip}; +use pallas_network::miniprotocols::Point; +use pallas_traverse::MultiEraHeader; + +use crate::framework::*; + +fn to_traverse(header: &HeaderContent) -> Result, gasket::error::Error> { + let out = match header.byron_prefix { + Some((subtag, _)) => MultiEraHeader::decode(header.variant, Some(subtag), &header.cbor), + None => MultiEraHeader::decode(header.variant, None, &header.cbor), + }; + + out.or_panic() +} + +pub type DownstreamPort = gasket::messaging::tokio::OutputPort; + +pub struct Worker +where + C: Cursor, +{ + peer_address: String, + network_magic: u64, + chain_cursor: C, + peer_session: Option, + downstream: DownstreamPort, + block_count: gasket::metrics::Counter, + chain_tip: gasket::metrics::Gauge, +} + +impl Worker +where + C: Cursor, +{ + pub fn new( + peer_address: String, + network_magic: u64, + chain_cursor: C, + downstream: DownstreamPort, + ) -> Self { + Self { + peer_address, + network_magic, + chain_cursor, + downstream, + peer_session: None, + block_count: Default::default(), + chain_tip: Default::default(), + } + } + + fn notify_tip(&self, tip: &Tip) { + self.chain_tip.set(tip.0.slot_or_default() as i64); + } + + async fn intersect(&mut self) -> Result<(), gasket::error::Error> { + let value = self.chain_cursor.intersection(); + + let chainsync = self.peer_session.as_mut().unwrap().chainsync(); + + let intersect = match value { + Intersection::Origin => { + info!("intersecting origin"); + chainsync.intersect_origin().await.or_restart()?.into() + } + Intersection::Tip => { + info!("intersecting tip"); + chainsync.intersect_tip().await.or_restart()?.into() + } + Intersection::Breadcrumbs(points) => { + info!("intersecting breadcrumbs"); + let (point, tip) = chainsync.find_intersect(points).await.or_restart()?; + + self.notify_tip(&tip); + + point + } + }; + + info!(?intersect, "intersected"); + + Ok(()) + } + + async fn process_next( + &mut self, + next: &NextResponse, + ) -> Result<(), gasket::error::Error> { + match next { + NextResponse::RollForward(header, tip) => { + let header = to_traverse(header).or_panic()?; + let slot = header.slot(); + let hash = header.hash(); + + debug!(slot, %hash, "chain sync roll forward"); + + let block = self + .peer_session + .as_mut() + .unwrap() + .blockfetch() + .fetch_single(pallas_network::miniprotocols::Point::Specific( + slot, + hash.to_vec(), + )) + .await + .or_retry()?; + + self.downstream + .send(UpstreamEvent::RollForward(slot, hash, block).into()) + .await?; + + self.notify_tip(tip); + + Ok(()) + } + chainsync::NextResponse::RollBackward(point, tip) => { + match &point { + Point::Origin => debug!("rollback to origin"), + Point::Specific(slot, _) => debug!(slot, "rollback"), + }; + + self.downstream + .send(UpstreamEvent::Rollback(point.clone()).into()) + .await?; + + self.notify_tip(tip); + + Ok(()) + } + chainsync::NextResponse::Await => { + info!("chain-sync reached the tip of the chain"); + Ok(()) + } + } + } +} + +#[async_trait::async_trait] +impl gasket::runtime::Worker for Worker +where + C: Cursor + Sync + Send, +{ + type WorkUnit = NextResponse; + + fn metrics(&self) -> gasket::metrics::Registry { + gasket::metrics::Builder::new() + .with_counter("received_blocks", &self.block_count) + .with_gauge("chain_tip", &self.chain_tip) + .build() + } + + async fn bootstrap(&mut self) -> Result<(), gasket::error::Error> { + debug!("connecting"); + + let peer = PeerClient::connect(&self.peer_address, self.network_magic) + .await + .or_restart()?; + + self.peer_session = Some(peer); + + self.intersect().await?; + + Ok(()) + } + + async fn teardown(&mut self) -> Result<(), gasket::error::Error> { + self.peer_session.as_mut().unwrap().abort(); + + Ok(()) + } + + async fn schedule(&mut self) -> gasket::runtime::ScheduleResult { + let client = self.peer_session.as_mut().unwrap().chainsync(); + + let next = match client.has_agency() { + true => { + info!("requesting next block"); + client.request_next().await.or_restart()? + } + false => { + info!("awaiting next block (blocking)"); + client.recv_while_must_reply().await.or_restart()? + } + }; + + Ok(gasket::runtime::WorkSchedule::Unit(next)) + } + + async fn execute(&mut self, unit: &Self::WorkUnit) -> Result<(), gasket::error::Error> { + self.process_next(unit).await + } +} diff --git a/pallas-upstream/tests/integration.rs b/pallas-upstream/tests/integration.rs index 6999fa5..89734e0 100644 --- a/pallas-upstream/tests/integration.rs +++ b/pallas-upstream/tests/integration.rs @@ -1,24 +1,21 @@ -#![feature(async_fn_in_trait)] - -use std::time::Duration; - use gasket::{ messaging::{ tokio::{InputPort, OutputPort}, RecvPort, SendPort, }, - runtime::{ScheduleResult, WorkSchedule, Worker}, + runtime::{WorkSchedule, Worker}, }; -use pallas_miniprotocols::Point; -use pallas_upstream::{BlockFetchEvent, Cursor}; -use tracing::{error, info}; + +use pallas_upstream::{Cursor, UpstreamEvent}; +use tracing::error; struct Witness { - input: InputPort, + input: InputPort, } +#[async_trait::async_trait] impl Worker for Witness { - type WorkUnit = BlockFetchEvent; + type WorkUnit = UpstreamEvent; fn metrics(&self) -> gasket::metrics::Registry { gasket::metrics::Registry::new() @@ -30,7 +27,7 @@ impl Worker for Witness { Ok(WorkSchedule::Unit(msg.payload)) } - async fn execute(&mut self, unit: &Self::WorkUnit) -> Result<(), gasket::error::Error> { + async fn execute(&mut self, _: &Self::WorkUnit) -> Result<(), gasket::error::Error> { error!("witnessing block event"); Ok(()) @@ -46,6 +43,7 @@ impl Cursor for StaticCursor { } #[test] +#[ignore] fn test_mainnet_upstream() { tracing::subscriber::set_global_default( tracing_subscriber::FmtSubscriber::builder() @@ -54,34 +52,28 @@ fn test_mainnet_upstream() { ) .unwrap(); - let mut b = pallas_upstream::n2n::Bootstrapper::new( - StaticCursor, - "relays-new.cardano-mainnet.iohk.io:3001".into(), - 764824073, - ); - let (send, receive) = gasket::messaging::tokio::channel(200); - // let mut f = Faker { - // output: Default::default(), - // }; + let mut output_port = OutputPort::default(); + output_port.connect(send); - //f.output.connect(send); + let upstream = pallas_upstream::n2n::Worker::new( + "relays-new.cardano-mainnet.iohk.io:3001".into(), + 764824073, + StaticCursor, + output_port, + ); - b.connect_output(send); - - let b = b.spawn().unwrap(); - - let mut w = Witness { + let mut witness = Witness { input: Default::default(), }; - w.input.connect(receive); + witness.input.connect(receive); - //let f = gasket::runtime::spawn_stage(f, Default::default(), Some("faker")); - let w = gasket::runtime::spawn_stage(w, Default::default(), Some("witness")); + let upstream = gasket::runtime::spawn_stage(upstream, Default::default(), Some("upstream")); + let witness = gasket::runtime::spawn_stage(witness, Default::default(), Some("witness")); - let d = gasket::daemon::Daemon(vec![w]); + let daemon = gasket::daemon::Daemon(vec![upstream, witness]); - d.block(); + daemon.block(); } diff --git a/pallas/Cargo.toml b/pallas/Cargo.toml index 002351d..2378841 100644 --- a/pallas/Cargo.toml +++ b/pallas/Cargo.toml @@ -11,8 +11,7 @@ readme = "../README.md" authors = ["Santiago Carmuega "] [dependencies] -pallas-multiplexer = { version = "0.18.0", path = "../pallas-multiplexer/" } -pallas-miniprotocols = { version = "0.18.0", path = "../pallas-miniprotocols/" } +pallas-network = { version = "0.18.0", path = "../pallas-network/" } pallas-primitives = { version = "0.18.0", path = "../pallas-primitives/" } pallas-traverse = { version = "0.18.0", path = "../pallas-traverse/" } pallas-addresses = { version = "0.18.0", path = "../pallas-addresses/" } diff --git a/pallas/src/ledger.rs b/pallas/src/ledger.rs deleted file mode 100644 index c18df38..0000000 --- a/pallas/src/ledger.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Ledger primitives and cbor codecs for different Cardano eras - -#[doc(inline)] -pub use pallas_primitives as primitives; - -#[doc(inline)] -pub use pallas_traverse as traverse; - -#[doc(inline)] -pub use pallas_addresses as addresses; diff --git a/pallas/src/lib.rs b/pallas/src/lib.rs index 067f723..84bfa16 100644 --- a/pallas/src/lib.rs +++ b/pallas/src/lib.rs @@ -9,9 +9,21 @@ #![warn(missing_docs)] #![warn(missing_doc_code_examples)] -pub mod network; +#[doc(inline)] +pub use pallas_network as network; -pub mod ledger; +pub mod ledger { + //! Ledger primitives and cbor codecs for different Cardano eras + + #[doc(inline)] + pub use pallas_primitives as primitives; + + #[doc(inline)] + pub use pallas_traverse as traverse; + + #[doc(inline)] + pub use pallas_addresses as addresses; +} #[doc(inline)] pub use pallas_crypto as crypto; diff --git a/pallas/src/network.rs b/pallas/src/network.rs deleted file mode 100644 index 696884d..0000000 --- a/pallas/src/network.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Network components of the Ouroboros protocol - -#[doc(inline)] -pub use pallas_multiplexer as multiplexer; - -#[doc(inline)] -pub use pallas_miniprotocols as miniprotocols; - -#[doc(inline)] -pub use pallas_upstream as upstream;