diff --git a/Cargo.toml b/Cargo.toml index e5d298467..dc978366e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,4 +21,5 @@ members = [ "examples/n2n-miniprotocols", "examples/n2c-miniprotocols", "examples/p2p", + "examples/leios-fetch-demo", ] diff --git a/examples/leios-fetch-demo/Cargo.toml b/examples/leios-fetch-demo/Cargo.toml new file mode 100644 index 000000000..57d3f9f02 --- /dev/null +++ b/examples/leios-fetch-demo/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "leios-fetch-demo" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +pallas = { path = "../../pallas", features = ["leios"]} +hex = "0.4.3" +tracing = "0.1.37" +tracing-subscriber = "0.3.16" +tokio = { version = "1.27.0", features = ["rt-multi-thread"] } +clap = { version = "4.5.50", features = ["derive"] } diff --git a/examples/leios-fetch-demo/src/main.rs b/examples/leios-fetch-demo/src/main.rs new file mode 100644 index 000000000..bf014f796 --- /dev/null +++ b/examples/leios-fetch-demo/src/main.rs @@ -0,0 +1,240 @@ +use pallas::network::{ + facades::{PeerClient, PeerServer}, + miniprotocols::leiosfetch::{self, bitmap_selection, minicbor, AnyCbor, ClientRequest}, +}; +use std::{ + net::{Ipv4Addr, SocketAddrV4}, + time::Duration, +}; + +use tokio::net::TcpListener; +use tracing::{debug, info}; + +use clap::Parser; + +/// Demonstration of `LeiosFetch` miniprotocol +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// Which part we take in the miniprotocol ("server" or "client"). + #[arg(short, long)] + role: String, + + /// Connect through this port + #[arg(short, long, default_value_t = 30003)] + port: u16, +} + +async fn server(port: u16) { + let block_hash: leiosfetch::Hash = + hex::decode("c579268ab0275662d47a3fe2dfcb41981426ddfc217ed3091364ae8f58198809").unwrap(); + + // CBOR bytes obtained from `leiosdemo202510` binary @ ccbe69384bd3d352dc5d31 + let endorser_block = hex::decode( + "bf5820521cacab5d8886db5c111290f8901276a44bc3f3b11b781bef5233\ + ddab1b2db618375820daa5ecee19aa3f240024a59103b37ceb3f4dc7d7ea\ + d8b0c675ff5939d7faa143183758200b1457b31bd0d0293cde0ca2b9f4d4\ + 8707e63d2959914c78a798536f9d310850183758205723adfca7765e74f4\ + a0659abeaffadc09be35325aa306e3ff1f6f4f74bb47491903e8ff", + ) + .unwrap(); + + let endorser_block: leiosfetch::EndorserBlock = minicbor::decode(&endorser_block).unwrap(); + + let block_txs_hash: leiosfetch::Hash = + hex::decode("bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0").unwrap(); + + // Selects first 3 transactions + let tx_map = leiosfetch::TxMap::from([(0, 0xe000000000000000)]); + + let block_slot: leiosfetch::Slot = 5; + let _block_txs_slot: leiosfetch::Slot = 222222222; + + // server setup + + info!("Server waiting for client..."); + let listener = TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port)) + .await + .unwrap(); + + let mut peer_server = PeerServer::accept(&listener, 0).await.unwrap(); + + let server_lf = peer_server.leiosfetch(); + + debug!("server waiting for block request"); + assert_eq!( + server_lf.recv_while_idle().await.unwrap().unwrap(), + ClientRequest::BlockRequest(block_slot, block_hash), + ); + info!("Server received `BlockRequest` from client"); + + assert_eq!(*server_lf.state(), leiosfetch::State::Block); + + info!("Server sends EB"); + server_lf.send_block(endorser_block).await.unwrap(); + assert_eq!(*server_lf.state(), leiosfetch::State::Idle); + + debug!("server waiting for txs request"); + assert_eq!( + server_lf.recv_while_idle().await.unwrap().unwrap(), + ClientRequest::BlockTxsRequest(block_slot, block_txs_hash, tx_map.clone()), + ); + info!("Server received `BlockTxsRequest` from client"); + + assert_eq!(*server_lf.state(), leiosfetch::State::BlockTxs); + + info!("Server selects Txs according to map and sends"); + server_lf + .send_block_txs(bitmap_selection(tx_map, &eb_tx())) + .await + .unwrap(); + assert_eq!(*server_lf.state(), leiosfetch::State::Idle); + + assert!(server_lf.recv_while_idle().await.unwrap().is_none()); + info!("Server received Done message from client"); + assert_eq!(*server_lf.state(), leiosfetch::State::Done); +} + +async fn client(listen_on: u16) { + let block_hash: leiosfetch::Hash = + hex::decode("c579268ab0275662d47a3fe2dfcb41981426ddfc217ed3091364ae8f58198809").unwrap(); + + let block_txs_hash: leiosfetch::Hash = + hex::decode("bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0").unwrap(); + + // Selects first 3 transactions + let tx_map = leiosfetch::TxMap::from([(0, 0xe000000000000000)]); + + let block_slot: leiosfetch::Slot = 5; + + // let vote_issuer_id: leiosfetch::Hash = + // hex::decode("beedbeedbeedbeedbeedbeedbeedbeedbeedbeedbeedbeedbeedbeedbeedbeed").unwrap(); + + tokio::time::sleep(Duration::from_secs(1)).await; + + // client setup + let mut client_to_server_conn = PeerClient::connect(format!("localhost:{}", listen_on), 0) + .await + .unwrap(); + + info!("Connecting to server"); + let client_lf = client_to_server_conn.leiosfetch(); + + info!("Client sends `BlockRequest`"); + client_lf + .send_block_request(block_slot, block_hash) + .await + .unwrap(); + let endorser_block = client_lf.recv_block().await.unwrap(); + info!("Client received endorser block:"); + info!("{:02x?}", endorser_block); + + assert_eq!(*client_lf.state(), leiosfetch::State::Idle); + + tokio::time::sleep(Duration::from_secs(1)).await; + + info!("Client sends `BlockTxsRequest`"); + client_lf + .send_block_txs_request(block_slot, block_txs_hash, tx_map) + .await + .unwrap(); + + tokio::time::sleep(Duration::from_secs(1)).await; + let txs = client_lf.recv_block_txs().await.unwrap(); + info!("Client received vec of txs:"); + info!("{:02x?}", txs); + + info!("Client sends Done"); + client_lf.send_done().await.unwrap(); + assert!(client_lf.is_done()); + + // Delay needed for proper disconnect + tokio::time::sleep(Duration::from_secs(1)).await; +} + +#[cfg(unix)] +#[tokio::main] +async fn main() { + tracing::subscriber::set_global_default( + tracing_subscriber::FmtSubscriber::builder() + .with_max_level(tracing::Level::TRACE) + .finish(), + ) + .unwrap(); + + let args = Args::parse(); + + match &args.role[..] { + "server" => { + server(args.port).await; + } + "client" => { + client(args.port).await; + } + _ => { + tracing::error!( + "Please choose a valid role: `server` or `client`.\n\\---> Role provided: {:?}", + args.role, + ); + return; + } + } +} + +fn eb_tx() -> Vec { + vec![ + hex::decode( + "58359719B92F47E7ABC8436813A42C1A5780C4ADDBF008E58E6CB8A4A3142067\ + E2BD47E713EBDB3672446C8DD5697D6F29477DA5ABD6F9", + ) + .unwrap(), + hex::decode( + "583551C27E9FD7D03351C243B98F6E33E9D29AD62CE9061580358B9CD4754505\ + 7B54A726322F849C5D73C01AE9881AA458F3A5F9DEA664", + ) + .unwrap(), + hex::decode( + "58356764A66870461BD63041BF1028FF898BDC58E95DA9EA6E684EBCC225F97A\ + ECF647BC7EA72BAC069D1FF9E3E9CB59C72181585FD4F0", + ) + .unwrap(), + hex::decode( + "5903E584035557626AE726D5BCE067C798B43B3DE035C3618F86CA1CF31969EB\ + B6711D354C445650D52E34F9E9A2057ECB363FE04FD3D5CE76B05E7C0CE7C563\ + C8F89AF65F3B57D6E34481A13889FACCE87AF020F0044B5EEA3C1BD48387506D\ + BD3C75ED4B9EFD7605DC3571A95B6E97F349C61C5D444A93DDE14F27C7B6EF74\ + F802EA1AB809ECBBEFD9229A85B42BC959B70BD207C06F30675B177096931759\ + 462E64B9F9F90EA5E5C5AA975A454F12AC6E4D21BC641A00B994B15E54BE2D79\ + 382A5ECF65BAA76496433D191CD0BEEB1AD979CD070CDC94FFFECD01CB3BF1E9\ + 86FEA8FE343C419AE71FC9CE7053697BCB75A45552006EFB1D4F36A34E9D70FE\ + 663C5B28D497373DB42AE1A6B8B5BD05390FBF580FCD75D857C9047FBB2A3FA8\ + 265702FD21773E124A5338E88D922A892331B9A7EE3F7375F9864E6990901D32\ + 3E37AB088528FC456B9082F40527C9565248D1D0403CEBEAE8BE8DDF290D0C0F\ + C415487747EFA5D256FA3F997E0D0F111C9F22D9F41C384C0FAA22AFE97BCCCB\ + D663268AE89A7BEC8898D5CEED1ECDFABC33205F8B01CEC18079B03BB7D5BBD8\ + EF80D6FB65FDC4F0445C8712CD717E5879663400652C16C8ECA980AFEC745A2C\ + C17D6A3EA1F9D2A4B0D534F784B35BAD97CCBB495E961D010C0A3FCF89FE7EAE\ + 091B00991EFF8BDB6E36C47FCBD1620130CAE67D68E68CFBE8D43BEBBA8B2331\ + F89F931D9FAA722789BFF1A6A0070480D87D59A94C62A8944EF5D327E7200030\ + 5502F26E7F3FF43C7C46097204C449F07C2F3DA9A9962B7AE51E6117FBF2B591\ + AB4273BA88F9C758EE64CF10FB2BF5F25B0B287F5081A79CEFDBBB0CBB70B9D9\ + DACBC1868C37B731C6C73F49F31C4F047D236DF3ED0BD2C41F4F19B9164D2DA3\ + CAC0067168746965C1B77EDE72A35F0BBD478FF21AE128D20FED009FCA1653CC\ + 16B7DE7F4FC1FBA75062B2E41BA0FFCBB8CA7213694C6947678BA2547BEF34FE\ + CD165A8ABB1DF0E52EBC0600361EFDE93031B290FA63F72F7DBA8F94FB34E6E3\ + 331C84367E4E887BBE982A905564993D7432BD2FE60061B39F0411486669FACA\ + F43E2A589EEBCC635F3D1C887C8444BD8994C2AE726F402CC846E6E150688FA9\ + EEAF836AC0EA978C776C4A14B4ECD9A54104A0D4FA8EEABBB5FBD4EEE80A19A0\ + 01547A1893BF3FAFF98994AD3E127CC4E35E13DA8EDF587DE0DB61824B2601C0\ + 46B83088A95B3DAE5CE118516F7E95E90DBD22A7315A1B990FBB81C264D4E903\ + 5935536ED84FF3D9951EED006ADB6C15F09691DC27037F19227004AE54D682F3\ + 6EE41C20A27E07F10CC3BF2CF68C92E4429D9AA75D2AE487C759AD1EF37263F3\ + 0BD4A50B4145C2B41C833C382FE4A5D15456346BF039A1E840BBF32F99AC80B4\ + A1930D5E838254F5", + ) + .unwrap(), + ] + .into_iter() + .map(|x| minicbor::decode(&x).unwrap()) + .collect() +} diff --git a/pallas-network/Cargo.toml b/pallas-network/Cargo.toml index 6598fb97f..3cdc34cea 100644 --- a/pallas-network/Cargo.toml +++ b/pallas-network/Cargo.toml @@ -28,3 +28,4 @@ tokio = { version = "1", features = ["full"] } [features] blueprint = [] +leios = [] diff --git a/pallas-network/src/facades.rs b/pallas-network/src/facades.rs index bc30f398e..74069f18e 100644 --- a/pallas-network/src/facades.rs +++ b/pallas-network/src/facades.rs @@ -21,6 +21,11 @@ use crate::miniprotocols::{ PROTOCOL_N2N_KEEP_ALIVE, PROTOCOL_N2N_PEER_SHARING, PROTOCOL_N2N_TX_SUBMISSION, }; +#[cfg(feature = "leios")] +use crate::miniprotocols::{ + leiosfetch, leiosnotify, PROTOCOL_N2N_LEIOS_FETCH, PROTOCOL_N2N_LEIOS_NOTIFY, +}; + use crate::multiplexer::{self, Bearer, RunningPlexer}; #[derive(Debug, Error)] @@ -108,6 +113,10 @@ pub struct PeerClient { pub blockfetch: blockfetch::Client, pub txsubmission: txsubmission::Client, pub peersharing: peersharing::Client, + #[cfg(feature = "leios")] + pub leiosnotify: leiosnotify::Client, + #[cfg(feature = "leios")] + pub leiosfetch: leiosfetch::Client, } impl PeerClient { @@ -125,6 +134,10 @@ impl PeerClient { let bf_channel = plexer.subscribe_client(PROTOCOL_N2N_BLOCK_FETCH); let txsub_channel = plexer.subscribe_client(PROTOCOL_N2N_TX_SUBMISSION); let peersharing_channel = plexer.subscribe_client(PROTOCOL_N2N_PEER_SHARING); + #[cfg(feature = "leios")] + let leiosnotify_channel = plexer.subscribe_client(PROTOCOL_N2N_LEIOS_NOTIFY); + #[cfg(feature = "leios")] + let leiosfetch_channel = plexer.subscribe_client(PROTOCOL_N2N_LEIOS_FETCH); let channel = plexer.subscribe_client(PROTOCOL_N2N_KEEP_ALIVE); let keepalive = keepalive::Client::new(channel); @@ -156,6 +169,10 @@ impl PeerClient { blockfetch: blockfetch::Client::new(bf_channel), txsubmission: txsubmission::Client::new(txsub_channel), peersharing: peersharing::Client::new(peersharing_channel), + #[cfg(feature = "leios")] + leiosnotify: leiosnotify::Client::new(leiosnotify_channel), + #[cfg(feature = "leios")] + leiosfetch: leiosfetch::Client::new(leiosfetch_channel), }; Ok(client) @@ -226,6 +243,16 @@ impl PeerClient { &mut self.peersharing } + #[cfg(feature = "leios")] + pub fn leiosnotify(&mut self) -> &mut leiosnotify::Client { + &mut self.leiosnotify + } + + #[cfg(feature = "leios")] + pub fn leiosfetch(&mut self) -> &mut leiosfetch::Client { + &mut self.leiosfetch + } + pub async fn abort(self) { self.plexer.abort().await } @@ -240,6 +267,10 @@ pub struct PeerServer { pub txsubmission: txsubmission::Server, pub keepalive: keepalive::Server, pub peersharing: peersharing::Server, + #[cfg(feature = "leios")] + pub leiosnotify: leiosnotify::Server, + #[cfg(feature = "leios")] + pub leiosfetch: leiosfetch::Server, accepted_address: Option, accepted_version: Option<(u64, n2n::VersionData)>, } @@ -254,6 +285,10 @@ impl PeerServer { let txsub_channel = plexer.subscribe_server(PROTOCOL_N2N_TX_SUBMISSION); let keepalive_channel = plexer.subscribe_server(PROTOCOL_N2N_KEEP_ALIVE); let peersharing_channel = plexer.subscribe_server(PROTOCOL_N2N_PEER_SHARING); + #[cfg(feature = "leios")] + let leiosnotify_channel = plexer.subscribe_server(PROTOCOL_N2N_LEIOS_NOTIFY); + #[cfg(feature = "leios")] + let leiosfetch_channel = plexer.subscribe_server(PROTOCOL_N2N_LEIOS_FETCH); let hs = handshake::N2NServer::new(hs_channel); let cs = chainsync::N2NServer::new(cs_channel); @@ -261,6 +296,10 @@ impl PeerServer { let txsub = txsubmission::Server::new(txsub_channel); let keepalive = keepalive::Server::new(keepalive_channel); let peersharing = peersharing::Server::new(peersharing_channel); + #[cfg(feature = "leios")] + let leiosnotify = leiosnotify::Server::new(leiosnotify_channel); + #[cfg(feature = "leios")] + let leiosfetch = leiosfetch::Server::new(leiosfetch_channel); let plexer = plexer.spawn(); @@ -272,6 +311,10 @@ impl PeerServer { txsubmission: txsub, keepalive, peersharing, + #[cfg(feature = "leios")] + leiosnotify, + #[cfg(feature = "leios")] + leiosfetch, accepted_address: None, accepted_version: None, } @@ -324,6 +367,16 @@ impl PeerServer { &mut self.peersharing } + #[cfg(feature = "leios")] + pub fn leiosnotify(&mut self) -> &mut leiosnotify::Server { + &mut self.leiosnotify + } + + #[cfg(feature = "leios")] + pub fn leiosfetch(&mut self) -> &mut leiosfetch::Server { + &mut self.leiosfetch + } + pub fn accepted_address(&self) -> Option<&SocketAddr> { self.accepted_address.as_ref() } diff --git a/pallas-network/src/miniprotocols/common.rs b/pallas-network/src/miniprotocols/common.rs index b97bce8a3..532d33746 100644 --- a/pallas-network/src/miniprotocols/common.rs +++ b/pallas-network/src/miniprotocols/common.rs @@ -54,6 +54,14 @@ pub const PROTOCOL_N2N_KEEP_ALIVE: u16 = 8; /// Protocol channel number for node-to-node Peer-sharing pub const PROTOCOL_N2N_PEER_SHARING: u16 = 10; +#[cfg(feature = "leios")] +/// Protocol channel number for node-to-node Leios Notify +pub const PROTOCOL_N2N_LEIOS_NOTIFY: u16 = 12; + +#[cfg(feature = "leios")] +/// Protocol channel number for node-to-node Leios Notify +pub const PROTOCOL_N2N_LEIOS_FETCH: u16 = 14; + /// Protocol channel number for node-to-client handshakes pub const PROTOCOL_N2C_HANDSHAKE: u16 = 0; diff --git a/pallas-network/src/miniprotocols/leiosfetch/client.rs b/pallas-network/src/miniprotocols/leiosfetch/client.rs new file mode 100644 index 000000000..a1fb205b1 --- /dev/null +++ b/pallas-network/src/miniprotocols/leiosfetch/client.rs @@ -0,0 +1,234 @@ +use std::fmt::Debug; +use thiserror::*; +use tracing::debug; + +use super::protocol::*; +use crate::{ + miniprotocols::leiosnotify::{Hash, Slot, VoteIssuerId}, + multiplexer, +}; + +#[derive(Error, Debug)] +pub enum ClientError { + #[error("attempted to receive message while agency is ours")] + AgencyIsOurs, + + #[error("attempted to send message while agency is theirs")] + AgencyIsTheirs, + + #[error("attempted to send message after protocol is done")] + ProtocolDone, + + #[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(multiplexer::Error), +} + +pub struct Client(State, multiplexer::ChannelBuffer); + +impl Client { + pub fn new(channel: multiplexer::AgentChannel) -> Self { + Self(State::Idle, multiplexer::ChannelBuffer::new(channel)) + } + + /// Returns the current state of the client. + pub fn state(&self) -> &State { + &self.0 + } + + /// Checks if the client is done. + pub fn is_done(&self) -> bool { + self.state() == &State::Done + } + + /// Checks if the client has agency. + fn has_agency(&self) -> bool { + self.state() == &State::Idle + } + + fn assert_agency_is_ours(&self) -> Result<(), ClientError> { + if self.is_done() { + Err(ClientError::ProtocolDone) + } else if !self.has_agency() { + Err(ClientError::AgencyIsTheirs) + } else { + Ok(()) + } + } + + fn assert_agency_is_theirs(&self) -> Result<(), ClientError> { + if self.has_agency() { + Err(ClientError::AgencyIsOurs) + } else if self.is_done() { + Err(ClientError::ProtocolDone) + } else { + Ok(()) + } + } + + fn assert_outbound_state(&self, msg: &Message) -> Result<(), ClientError> { + use Message::*; + + if self.state() == &State::Idle + && matches!( + msg, + BlockRequest(..) + | BlockTxsRequest(..) + | VoteRequest(..) + | RangeRequest { .. } + | Done + ) + { + Ok(()) + } else { + Err(ClientError::InvalidOutbound) + } + } + + fn assert_inbound_state(&self, msg: &Message) -> Result<(), ClientError> { + use Message::*; + + match (self.state(), msg) { + (State::Block, Block(_)) => Ok(()), + (State::BlockTxs, BlockTxs(_)) => Ok(()), + (State::Votes, VoteDelivery(..)) => Ok(()), + (State::BlockRange, NextBlockAndTxs(..)) => Ok(()), + (State::BlockRange, LastBlockAndTxs(..)) => Ok(()), + _ => Err(ClientError::InvalidInbound), + } + } + + pub async fn send_message(&mut self, msg: &Message) -> Result<(), ClientError> { + self.assert_agency_is_ours()?; + self.assert_outbound_state(msg)?; + self.1 + .send_msg_chunks(msg) + .await + .map_err(ClientError::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(ClientError::Plexer)?; + self.assert_inbound_state(&msg)?; + + Ok(msg) + } + + pub async fn send_block_request(&mut self, slot: Slot, hash: Hash) -> Result<(), ClientError> { + let msg = Message::BlockRequest(slot, hash); + self.send_message(&msg).await?; + self.0 = State::Block; + debug!("sent block request"); + + Ok(()) + } + + pub async fn send_block_txs_request( + &mut self, + slot: Slot, + hash: Hash, + tx_map: TxMap, + ) -> Result<(), ClientError> { + let msg = Message::BlockTxsRequest(slot, hash, tx_map); + self.send_message(&msg).await?; + self.0 = State::BlockTxs; + debug!("sent block and txs request"); + + Ok(()) + } + + pub async fn send_vote_request( + &mut self, + req: Vec<(Slot, VoteIssuerId)>, + ) -> Result<(), ClientError> { + let msg = Message::VoteRequest(req); + self.send_message(&msg).await?; + self.0 = State::Votes; + debug!("sent vote request"); + + Ok(()) + } + + pub async fn send_range_request( + &mut self, + first: (Slot, Hash), + last: (Slot, Hash), + ) -> Result<(), ClientError> { + let msg = Message::RangeRequest { first, last }; + self.send_message(&msg).await?; + self.0 = State::BlockRange; + debug!("sent vote request"); + + Ok(()) + } + + pub async fn send_done(&mut self) -> Result<(), ClientError> { + let msg = Message::Done; + self.send_message(&msg).await?; + self.0 = State::Done; + + Ok(()) + } + pub async fn recv_block(&mut self) -> Result { + let msg = self.recv_message().await?; + match msg { + Message::Block(block) => { + self.0 = State::Idle; + Ok(block) + } + _ => Err(ClientError::InvalidInbound), + } + } + + pub async fn recv_block_txs(&mut self) -> Result, ClientError> { + let msg = self.recv_message().await?; + match msg { + Message::BlockTxs(response) => { + tracing::trace!(?response, "received"); + self.0 = State::Idle; + Ok(response) + } + _ => Err(ClientError::InvalidInbound), + } + } + + pub async fn recv_vote_delivery(&mut self) -> Result, ClientError> { + let msg = self.recv_message().await?; + match msg { + Message::VoteDelivery(votes) => { + tracing::trace!(?votes, "received"); + self.0 = State::Idle; + Ok(votes) + } + _ => Err(ClientError::InvalidInbound), + } + } + + pub async fn recv_while_block_range( + &mut self, + ) -> Result<(EndorserBlock, Vec), ClientError> { + match self.recv_message().await? { + Message::NextBlockAndTxs(block, txs) => { + debug!("Receiving next block and txs"); + tracing::trace!(?block, ?txs, "received"); + self.0 = State::Idle; + Ok((block, txs)) + } + Message::LastBlockAndTxs(block, txs) => { + debug!("Receiving last block and txs"); + tracing::trace!(?block, ?txs, "received"); + self.0 = State::Idle; + Ok((block, txs)) + } + _ => Err(ClientError::InvalidInbound), + } + } +} diff --git a/pallas-network/src/miniprotocols/leiosfetch/mod.rs b/pallas-network/src/miniprotocols/leiosfetch/mod.rs new file mode 100644 index 000000000..c3cd06d4c --- /dev/null +++ b/pallas-network/src/miniprotocols/leiosfetch/mod.rs @@ -0,0 +1,8 @@ +mod client; +mod primitives; +mod protocol; +mod server; + +pub use client::*; +pub use protocol::*; +pub use server::*; diff --git a/pallas-network/src/miniprotocols/leiosfetch/primitives.rs b/pallas-network/src/miniprotocols/leiosfetch/primitives.rs new file mode 100644 index 000000000..18b24a69a --- /dev/null +++ b/pallas-network/src/miniprotocols/leiosfetch/primitives.rs @@ -0,0 +1,48 @@ +// Material brought from `pallas-primitives` +// TODO: Refactor in order to avoid repetition. + +use pallas_codec::{ + minicbor::{self, Decode, Encode}, + utils::Bytes, +}; + +use crate::miniprotocols::{leiosnotify::Hash, localtxsubmission::primitives::PoolKeyhash}; + +pub type BlsSignature = Bytes; // 48 bytes + +#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)] +#[cbor(flat)] +pub enum LeiosVote { + #[n(0)] + Persistent { + #[n(0)] + election_id: Bytes, + + #[n(1)] + persistent_voter_id: Bytes, + + #[n(2)] + endorser_block_hash: Hash, + + #[n(3)] + vote_signature: BlsSignature, + }, + + #[n(1)] + NonPersistent { + #[n(0)] + election_id: Bytes, + + #[n(1)] + pool_id: PoolKeyhash, + + #[n(2)] + eligibility_signature: BlsSignature, + + #[n(3)] + endorser_block_hash: Hash, + + #[n(5)] + vote_signature: BlsSignature, + }, +} diff --git a/pallas-network/src/miniprotocols/leiosfetch/protocol.rs b/pallas-network/src/miniprotocols/leiosfetch/protocol.rs new file mode 100644 index 000000000..5344f017e --- /dev/null +++ b/pallas-network/src/miniprotocols/leiosfetch/protocol.rs @@ -0,0 +1,57 @@ +pub use pallas_codec::{ + minicbor::{self, Decode, Encode}, + utils::{AnyCbor, Bytes}, +}; + +pub use crate::miniprotocols::leiosnotify::{ + BlockOffer, BlockTxsOffer, Hash, Header, Slot, VoteIssuerId, +}; +use std::{collections::BTreeMap, fmt::Debug}; + +pub use super::primitives::LeiosVote; + +pub type Tx = AnyCbor; // Mock Txs +pub type TxHash = Hash; +pub type EndorserBlock = BTreeMap; +pub type BitMap = u64; +pub type TxMap = BTreeMap; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum State { + Idle, + Block, + BlockTxs, + Votes, + BlockRange, + Done, +} + +#[derive(Debug, Encode, Decode)] +#[cbor(flat)] +pub enum Message { + #[n(0)] + BlockRequest(#[n(0)] Slot, #[n(1)] Hash), + #[n(1)] + Block(#[n(0)] EndorserBlock), + #[n(2)] + BlockTxsRequest(#[n(0)] Slot, #[n(1)] Hash, #[n(2)] TxMap), + #[n(3)] + BlockTxs(#[n(0)] Vec), + #[n(4)] + VoteRequest(#[n(0)] Vec<(Slot, VoteIssuerId)>), + #[n(5)] + VoteDelivery(#[n(0)] Vec), + #[n(6)] + RangeRequest { + #[n(0)] + first: (Slot, Hash), + #[n(1)] + last: (Slot, Hash), + }, + #[n(7)] + NextBlockAndTxs(#[n(0)] EndorserBlock, #[n(1)] Vec), + #[n(8)] + LastBlockAndTxs(#[n(0)] EndorserBlock, #[n(1)] Vec), + #[n(9)] + Done, +} diff --git a/pallas-network/src/miniprotocols/leiosfetch/server.rs b/pallas-network/src/miniprotocols/leiosfetch/server.rs new file mode 100644 index 000000000..dd67d3e75 --- /dev/null +++ b/pallas-network/src/miniprotocols/leiosfetch/server.rs @@ -0,0 +1,284 @@ +use std::fmt::Debug; +use thiserror::*; + +use super::protocol::*; +use crate::multiplexer; + +#[derive(Error, Debug)] +pub enum ServerError { + #[error("attempted to receive message while agency is ours")] + AgencyIsOurs, + + #[error("attempted to send message while agency is theirs")] + AgencyIsTheirs, + + #[error("attempted to send message after protocol is done")] + ProtocolDone, + + #[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(multiplexer::Error), +} + +#[derive(Debug, PartialEq, Eq)] +pub enum ClientRequest { + BlockRequest(Slot, Hash), + BlockTxsRequest(Slot, Hash, TxMap), + VoteRequest(Vec<(Slot, VoteIssuerId)>), + RangeRequest { + first: (Slot, Hash), + last: (Slot, Hash), + }, + Done, +} + +pub struct Server(State, multiplexer::ChannelBuffer); + +impl Server { + pub fn new(channel: multiplexer::AgentChannel) -> Self { + Self(State::Idle, multiplexer::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 { + use State::*; + + matches!(&self.0, Block | BlockTxs | Votes | BlockRange) + } + + fn assert_agency_is_ours(&self) -> Result<(), ServerError> { + if self.is_done() { + Err(ServerError::ProtocolDone) + } else if !self.has_agency() { + Err(ServerError::AgencyIsTheirs) + } else { + Ok(()) + } + } + + fn assert_agency_is_theirs(&self) -> Result<(), ServerError> { + if self.has_agency() { + Err(ServerError::AgencyIsOurs) + } else if self.is_done() { + Err(ServerError::ProtocolDone) + } else { + Ok(()) + } + } + + fn assert_outbound_state(&self, msg: &Message) -> Result<(), ServerError> { + use Message::*; + match (self.state(), msg) { + (State::Block, Block(_)) => Ok(()), + (State::BlockTxs, BlockTxs(_)) => Ok(()), + (State::Votes, VoteDelivery(..)) => Ok(()), + (State::BlockRange, NextBlockAndTxs(..)) => Ok(()), + (State::BlockRange, LastBlockAndTxs(..)) => Ok(()), + _ => Err(ServerError::InvalidOutbound), + } + } + + fn assert_inbound_state(&self, msg: &Message) -> Result<(), ServerError> { + use Message::*; + + if self.state() == &State::Idle + && matches!( + msg, + BlockRequest(..) + | BlockTxsRequest(..) + | VoteRequest(..) + | RangeRequest { .. } + | Done + ) + { + Ok(()) + } else { + Err(ServerError::InvalidInbound) + } + } + + pub async fn send_message(&mut self, msg: &Message) -> Result<(), ServerError> { + self.assert_agency_is_ours()?; + self.assert_outbound_state(msg)?; + self.1 + .send_msg_chunks(msg) + .await + .map_err(ServerError::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(ServerError::Plexer)?; + self.assert_inbound_state(&msg)?; + + Ok(msg) + } + + pub async fn recv_while_idle(&mut self) -> Result, ServerError> { + use ClientRequest::*; + + match self.recv_message().await? { + Message::BlockRequest(slot, hash) => { + self.0 = State::Block; + Ok(Some(BlockRequest(slot, hash))) + } + Message::BlockTxsRequest(slot, hash, tx_map) => { + self.0 = State::BlockTxs; + Ok(Some(BlockTxsRequest(slot, hash, tx_map))) + } + Message::VoteRequest(req) => { + self.0 = State::Votes; + Ok(Some(VoteRequest(req))) + } + Message::RangeRequest { first, last } => { + self.0 = State::BlockRange; + Ok(Some(RangeRequest { first, last })) + } + Message::Done => { + self.0 = State::Done; + + Ok(None) + } + _ => Err(ServerError::InvalidInbound), + } + } + + pub async fn send_block(&mut self, response: EndorserBlock) -> Result<(), ServerError> { + let msg = Message::Block(response); + self.send_message(&msg).await?; + self.0 = State::Idle; + + Ok(()) + } + + pub async fn send_block_txs(&mut self, response: Vec) -> Result<(), ServerError> { + let msg = Message::BlockTxs(response); + self.send_message(&msg).await?; + self.0 = State::Idle; + + Ok(()) + } + + pub async fn send_vote_delivery( + &mut self, + response: Vec, + ) -> Result<(), ServerError> { + let msg = Message::VoteDelivery(response); + self.send_message(&msg).await?; + self.0 = State::Idle; + + Ok(()) + } + + pub async fn send_next_block_and_txs( + &mut self, + block: EndorserBlock, + txs: Vec, + ) -> Result<(), ServerError> { + let msg = Message::NextBlockAndTxs(block, txs); + self.send_message(&msg).await?; + self.0 = State::BlockRange; + + Ok(()) + } + + pub async fn send_last_block_and_txs( + &mut self, + block: EndorserBlock, + txs: Vec, + ) -> Result<(), ServerError> { + let msg = Message::NextBlockAndTxs(block, txs); + self.send_message(&msg).await?; + self.0 = State::Idle; + + Ok(()) + } +} + +pub fn bitmap_to_indices(bitmap: u64) -> Vec { + (0..64) + .rev() + .enumerate() + .filter(|(_, y)| (bitmap >> y) & 1 == 1) + .map(|(x, _)| x) + .collect() +} + +pub fn bitmap_selection, Tx: Clone>( + tx_map: TMap, + data: &[Tx], +) -> Vec { + tx_map + .into_iter() + .map(|(index, bitmap)| { + bitmap_to_indices(bitmap) + .into_iter() + .map(move |i| data[64 * index as usize + i].clone()) + }) + .flatten() + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::BTreeMap; + + #[test] + fn test_bitmap_indices() { + assert_eq!(bitmap_to_indices(0xd000000000000002), vec![0, 1, 3, 62]); + assert_eq!(bitmap_to_indices(0xe000000000000000), vec![0, 1, 2]); + } + + #[test] + fn test_bitmap_selection() { + let map = BTreeMap::::from([(0, 0x5000000000000000), (1, 0x8000000000000000)]); + assert_eq!( + bitmap_selection(map, &eb_tx()), + [eb_tx()[1].clone(), eb_tx()[3].clone(), eb_tx()[64].clone()] + ); + } + + fn eb_tx() -> Vec> { + let mut list = vec![ + hex::decode( + "58359719B92F47E7ABC8436813A42C1A5780C4ADDBF008E58E6CB8A4A3142067\ + E2BD47E713EBDB3672446C8DD5697D6F29477DA5ABD6F9", + ) + .unwrap(), + hex::decode( + "583551C27E9FD7D03351C243B98F6E33E9D29AD62CE9061580358B9CD4754505\ + 7B54A726322F849C5D73C01AE9881AA458F3A5F9DEA664", + ) + .unwrap(), + hex::decode( + "58356764A66870461BD63041BF1028FF898BDC58E95DA9EA6E684EBCC225F97A\ + ECF647BC7EA72BAC069D1FF9E3E9CB59C72181585FD4F0", + ) + .unwrap(), + hex::decode( + "5903E584035557626AE726D5BCE067C798B43B3DE035C3618F86CA1CF31969EB\ + B6711D354C445650D52E34F9E9A2057ECB363FE04FD3D5CE76B05E7C0CE7C563", + ) + .unwrap(), + ]; + list.append(&mut vec![vec![]; 60]); + list.append(&mut vec![vec![10]]); + + list + } +} diff --git a/pallas-network/src/miniprotocols/leiosnotify/client.rs b/pallas-network/src/miniprotocols/leiosnotify/client.rs new file mode 100644 index 000000000..13586cd79 --- /dev/null +++ b/pallas-network/src/miniprotocols/leiosnotify/client.rs @@ -0,0 +1,172 @@ +use std::fmt::Debug; +use thiserror::*; +use tracing::debug; + +use super::protocol::*; +use crate::multiplexer; + +#[derive(Error, Debug)] +pub enum ClientError { + #[error("attempted to receive message while agency is ours")] + AgencyIsOurs, + + #[error("attempted to send message while agency is theirs")] + AgencyIsTheirs, + + #[error("attempted to send message after protocol is done")] + ProtocolDone, + + #[error("inbound message is not valid for current state")] + InvalidInbound, + + #[error("outbound message is not valid for current state")] + InvalidOutbound, + + // #[error("")] + // ProtocolSpecificError, + #[error("error while sending or receiving data through the channel")] + Plexer(multiplexer::Error), +} + +pub struct Client(State, multiplexer::ChannelBuffer); + +impl Client { + pub fn new(channel: multiplexer::AgentChannel) -> Self { + Self(State::Idle, multiplexer::ChannelBuffer::new(channel)) + } + + /// Returns the current state of the client. + pub fn state(&self) -> &State { + &self.0 + } + + /// Checks if the client is done. + pub fn is_done(&self) -> bool { + self.state() == &State::Done + } + + /// Checks if the client has agency. + fn has_agency(&self) -> bool { + self.state() == &State::Idle + } + + fn assert_agency_is_ours(&self) -> Result<(), ClientError> { + if self.is_done() { + Err(ClientError::ProtocolDone) + } else if !self.has_agency() { + Err(ClientError::AgencyIsTheirs) + } else { + Ok(()) + } + } + + fn assert_agency_is_theirs(&self) -> Result<(), ClientError> { + if self.has_agency() { + Err(ClientError::AgencyIsOurs) + } else if self.is_done() { + Err(ClientError::ProtocolDone) + } else { + Ok(()) + } + } + + fn assert_outbound_state(&self, msg: &Message) -> Result<(), ClientError> { + if self.state() == &State::Idle && matches!(msg, Message::RequestNext | Message::Done) { + Ok(()) + } else { + Err(ClientError::InvalidOutbound) + } + } + + fn assert_inbound_state(&self, msg: &Message) -> Result<(), ClientError> { + if self.state() != &State::Busy || matches!(msg, Message::RequestNext | Message::Done) { + Err(ClientError::InvalidInbound) + } else { + Ok(()) + } + } + + pub async fn send_message(&mut self, msg: &Message) -> Result<(), ClientError> { + self.assert_agency_is_ours()?; + self.assert_outbound_state(msg)?; + self.1 + .send_msg_chunks(msg) + .await + .map_err(ClientError::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(ClientError::Plexer)?; + self.assert_inbound_state(&msg)?; + + Ok(msg) + } + + pub async fn send_request_next(&mut self) -> Result<(), ClientError> { + let msg = Message::RequestNext; + self.send_message(&msg).await?; + self.0 = State::Busy; + debug!("sent notification request next message"); + + Ok(()) + } + + pub async fn recv_block_announcement(&mut self) -> Result { + let msg = self.recv_message().await?; + match msg { + Message::BlockAnnouncement(params) => { + debug!(?params, "received "); + self.0 = State::Idle; + Ok(params) + } + _ => Err(ClientError::InvalidInbound), + } + } + + pub async fn recv_block_offer(&mut self) -> Result { + let msg = self.recv_message().await?; + match msg { + Message::BlockOffer(slot, hash) => { + debug!(?slot, ?hash, "received "); + self.0 = State::Idle; + Ok((slot, hash)) + } + _ => Err(ClientError::InvalidInbound), + } + } + + pub async fn recv_block_txs_offer(&mut self) -> Result { + let msg = self.recv_message().await?; + match msg { + Message::BlockTxsOffer(slot, hash) => { + debug!(?slot, ?hash, "received "); + self.0 = State::Idle; + Ok((slot, hash)) + } + _ => Err(ClientError::InvalidInbound), + } + } + + pub async fn recv_vote_offer(&mut self) -> Result, ClientError> { + let msg = self.recv_message().await?; + match msg { + Message::VoteOffer(params) => { + debug!(?params, "received "); + self.0 = State::Idle; + Ok(params) + } + _ => Err(ClientError::InvalidInbound), + } + } + + pub async fn send_done(&mut self) -> Result<(), ClientError> { + let msg = Message::Done; + self.send_message(&msg).await?; + self.0 = State::Done; + + Ok(()) + } +} diff --git a/pallas-network/src/miniprotocols/leiosnotify/mod.rs b/pallas-network/src/miniprotocols/leiosnotify/mod.rs new file mode 100644 index 000000000..d99027931 --- /dev/null +++ b/pallas-network/src/miniprotocols/leiosnotify/mod.rs @@ -0,0 +1,7 @@ +mod client; +mod protocol; +mod server; + +pub use client::*; +pub use protocol::*; +pub use server::*; diff --git a/pallas-network/src/miniprotocols/leiosnotify/protocol.rs b/pallas-network/src/miniprotocols/leiosnotify/protocol.rs new file mode 100644 index 000000000..c1c6c19d5 --- /dev/null +++ b/pallas-network/src/miniprotocols/leiosnotify/protocol.rs @@ -0,0 +1,42 @@ +use pallas_codec::{ + minicbor::{self, Decode, Encode}, + // utils::AnyCbor, +}; +use std::fmt::Debug; + +pub type Slot = u64; + +// TODO: Add `pallas_primitives::babbage::Header` +pub type Header = Vec; + +pub type Hash = Vec; + +pub type VoteIssuerId = Vec; + +pub type BlockOffer = (Slot, Hash); + +pub type BlockTxsOffer = (Slot, Hash); + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum State { + Idle, + Busy, + Done, +} + +#[derive(Debug, Encode, Decode)] +#[cbor(flat)] +pub enum Message { + #[n(0)] + RequestNext, + #[n(1)] + BlockAnnouncement(#[n(0)] Header), + #[n(2)] + BlockOffer(#[n(0)] Slot, #[n(1)] Hash), + #[n(3)] + BlockTxsOffer(#[n(0)] Slot, #[n(1)] Hash), + #[n(4)] + VoteOffer(#[n(0)] Vec<(Slot, VoteIssuerId)>), + #[n(5)] + Done, +} diff --git a/pallas-network/src/miniprotocols/leiosnotify/server.rs b/pallas-network/src/miniprotocols/leiosnotify/server.rs new file mode 100644 index 000000000..c0f56b5b0 --- /dev/null +++ b/pallas-network/src/miniprotocols/leiosnotify/server.rs @@ -0,0 +1,162 @@ +use std::fmt::Debug; +use thiserror::*; +use tracing::debug; + +use super::protocol::*; +use crate::multiplexer; + +#[derive(Error, Debug)] +pub enum ServerError { + #[error("attempted to receive message while agency is ours")] + AgencyIsOurs, + + #[error("attempted to send message while agency is theirs")] + AgencyIsTheirs, + + #[error("attempted to send message after protocol is done")] + ProtocolDone, + + #[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(multiplexer::Error), +} + +pub struct Server(State, multiplexer::ChannelBuffer); + +impl Server { + pub fn new(channel: multiplexer::AgentChannel) -> Self { + Self(State::Idle, multiplexer::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 { + matches!(&self.0, State::Busy) + } + + fn assert_agency_is_ours(&self) -> Result<(), ServerError> { + if self.is_done() { + Err(ServerError::ProtocolDone) + } else if !self.has_agency() { + Err(ServerError::AgencyIsTheirs) + } else { + Ok(()) + } + } + + fn assert_agency_is_theirs(&self) -> Result<(), ServerError> { + if self.has_agency() { + Err(ServerError::AgencyIsOurs) + } else if self.is_done() { + Err(ServerError::ProtocolDone) + } else { + Ok(()) + } + } + + fn assert_outbound_state(&self, msg: &Message) -> Result<(), ServerError> { + if self.state() != &State::Busy || matches!(msg, Message::RequestNext | Message::Done) { + Err(ServerError::InvalidOutbound) + } else { + Ok(()) + } + } + + fn assert_inbound_state(&self, msg: &Message) -> Result<(), ServerError> { + if self.state() == &State::Idle && matches!(msg, Message::RequestNext | Message::Done) { + Ok(()) + } else { + Err(ServerError::InvalidInbound) + } + } + + pub async fn send_message(&mut self, msg: &Message) -> Result<(), ServerError> { + self.assert_agency_is_ours()?; + self.assert_outbound_state(msg)?; + self.1 + .send_msg_chunks(msg) + .await + .map_err(ServerError::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(ServerError::Plexer)?; + self.assert_inbound_state(&msg)?; + + Ok(msg) + } + + pub async fn recv_request_next(&mut self) -> Result<(), ServerError> { + let msg = self.recv_message().await?; + match msg { + Message::RequestNext => { + debug!("received Notification Request Next"); + self.0 = State::Busy; + Ok(()) + } + Message::Done => { + debug!("client sent Done message in LeiosNotify protocol"); + self.0 = State::Done; + Ok(()) + } + _ => Err(ServerError::InvalidInbound), + } + } + + pub async fn send_block_announcement(&mut self, response: Header) -> Result<(), ServerError> { + let msg = Message::BlockAnnouncement(response); + self.send_message(&msg).await?; + self.0 = State::Idle; + + Ok(()) + } + + pub async fn send_block_offer( + &mut self, + slot: Slot, + header: Header, + ) -> Result<(), ServerError> { + let msg = Message::BlockOffer(slot, header); + self.send_message(&msg).await?; + self.0 = State::Idle; + + Ok(()) + } + + pub async fn send_block_txs_offer( + &mut self, + slot: Slot, + header: Header, + ) -> Result<(), ServerError> { + let msg = Message::BlockTxsOffer(slot, header); + self.send_message(&msg).await?; + self.0 = State::Idle; + + Ok(()) + } + + pub async fn send_vote_offer( + &mut self, + response: Vec<(Slot, VoteIssuerId)>, + ) -> Result<(), ServerError> { + let msg = Message::VoteOffer(response); + self.send_message(&msg).await?; + self.0 = State::Idle; + + Ok(()) + } +} diff --git a/pallas-network/src/miniprotocols/mod.rs b/pallas-network/src/miniprotocols/mod.rs index 21281934c..71032d4c8 100644 --- a/pallas-network/src/miniprotocols/mod.rs +++ b/pallas-network/src/miniprotocols/mod.rs @@ -6,6 +6,13 @@ pub mod blockfetch; pub mod chainsync; pub mod handshake; pub mod keepalive; + +#[cfg(feature = "leios")] +pub mod leiosnotify; + +#[cfg(feature = "leios")] +pub mod leiosfetch; + pub mod localmsgnotification; pub mod localmsgsubmission; pub mod localstate; diff --git a/pallas-network/tests/leiosfetch.rs b/pallas-network/tests/leiosfetch.rs new file mode 100644 index 000000000..e807d67d6 --- /dev/null +++ b/pallas-network/tests/leiosfetch.rs @@ -0,0 +1,189 @@ +#![cfg(feature = "leios")] +use pallas_network::{ + facades::{PeerClient, PeerServer}, + miniprotocols::leiosfetch::{self, bitmap_selection, minicbor, AnyCbor, ClientRequest}, +}; +use std::{ + net::{Ipv4Addr, SocketAddrV4}, + time::Duration, +}; + +use tokio::net::TcpListener; + +#[cfg(unix)] +#[tokio::test] +pub async fn leiosfetch_server_and_client_happy_path() { + use tracing::debug; + + tracing_subscriber::fmt::init(); + + let block_hash: leiosfetch::Hash = + hex::decode("c579268ab0275662d47a3fe2dfcb41981426ddfc217ed3091364ae8f58198809").unwrap(); + + // CBOR bytes obtained from `leiosdemo202510` binary @ ccbe69384bd3d352dc5d31 + let endorser_block = hex::decode( + "bf5820521cacab5d8886db5c111290f8901276a44bc3f3b11b781bef5233\ + ddab1b2db618375820daa5ecee19aa3f240024a59103b37ceb3f4dc7d7ea\ + d8b0c675ff5939d7faa143183758200b1457b31bd0d0293cde0ca2b9f4d4\ + 8707e63d2959914c78a798536f9d310850183758205723adfca7765e74f4\ + a0659abeaffadc09be35325aa306e3ff1f6f4f74bb47491903e8ff", + ) + .unwrap(); + + let endorser_block: leiosfetch::EndorserBlock = minicbor::decode(&endorser_block).unwrap(); + + let rb_header: leiosfetch::Header = + hex::decode("eade0000eade0000eade0000eade0000eade0000eade0000eade0000eade0000").unwrap(); + + let block_txs_hash: leiosfetch::Hash = + hex::decode("bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0").unwrap(); + + // Selects first 3 transactions + let tx_map = leiosfetch::TxMap::from([(0, 0xe000000000000000)]); + + let block_slot: leiosfetch::Slot = 5; + let _block_txs_slot: leiosfetch::Slot = 222222222; + + let vote_issuer_id: leiosfetch::Hash = + hex::decode("beedbeedbeedbeedbeedbeedbeedbeedbeedbeedbeedbeedbeedbeedbeedbeed").unwrap(); + + let listener = TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 30003)) + .await + .unwrap(); + + let server = tokio::spawn({ + let _header = rb_header.clone(); + let block_hash = block_hash.clone(); + let block = endorser_block.clone(); + let tx_map = tx_map.clone(); + let block_txs_hash = block_txs_hash.clone(); + let _vote_issuer_id = vote_issuer_id.clone(); + + async move { + // server setup + + let mut peer_server = PeerServer::accept(&listener, 0).await.unwrap(); + + let server_lf = peer_server.leiosfetch(); + + // server receives `BlockRequest` from client + debug!("server waiting for block request"); + assert_eq!( + server_lf.recv_while_idle().await.unwrap().unwrap(), + ClientRequest::BlockRequest(block_slot, block_hash), + ); + assert_eq!(*server_lf.state(), leiosfetch::State::Block); + + // Server sends EB + server_lf.send_block(block).await.unwrap(); + assert_eq!(*server_lf.state(), leiosfetch::State::Idle); + + // server receives `BlockTxsRequest` from client + debug!("server waiting for txs request"); + assert_eq!( + server_lf.recv_while_idle().await.unwrap().unwrap(), + ClientRequest::BlockTxsRequest(block_slot, block_txs_hash, tx_map.clone()), + ); + assert_eq!(*server_lf.state(), leiosfetch::State::BlockTxs); + + // Server selects Txs according to map and sends + server_lf + .send_block_txs(bitmap_selection(tx_map, &eb_tx())) + .await + .unwrap(); + assert_eq!(*server_lf.state(), leiosfetch::State::Idle); + + // Server receives Done message from client + assert!(server_lf.recv_while_idle().await.unwrap().is_none()); + assert_eq!(*server_lf.state(), leiosfetch::State::Done); + } + }); + + let client = tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(1)).await; + + // client setup + let mut client_to_server_conn = PeerClient::connect("localhost:30003", 0).await.unwrap(); + + let client_lf = client_to_server_conn.leiosfetch(); + + // client sends `BlockRequest`, receives endorser block + client_lf + .send_block_request(block_slot, block_hash) + .await + .unwrap(); + assert_eq!(client_lf.recv_block().await.unwrap(), endorser_block); + assert_eq!(*client_lf.state(), leiosfetch::State::Idle); + + // client sends `BlockTxsRequest`, receives vec of txs + client_lf + .send_block_txs_request(block_slot, block_txs_hash, tx_map) + .await + .unwrap(); + assert_eq!(client_lf.recv_block_txs().await.unwrap(), eb_tx()[0..3]); + + // client sends Done + client_lf.send_done().await.unwrap(); + assert!(client_lf.is_done()) + }); + + tokio::try_join!(client, server).unwrap(); +} + +fn eb_tx() -> Vec { + vec![ + hex::decode( + "58359719B92F47E7ABC8436813A42C1A5780C4ADDBF008E58E6CB8A4A3142067\ + E2BD47E713EBDB3672446C8DD5697D6F29477DA5ABD6F9", + ) + .unwrap(), + hex::decode( + "583551C27E9FD7D03351C243B98F6E33E9D29AD62CE9061580358B9CD4754505\ + 7B54A726322F849C5D73C01AE9881AA458F3A5F9DEA664", + ) + .unwrap(), + hex::decode( + "58356764A66870461BD63041BF1028FF898BDC58E95DA9EA6E684EBCC225F97A\ + ECF647BC7EA72BAC069D1FF9E3E9CB59C72181585FD4F0", + ) + .unwrap(), + hex::decode( + "5903E584035557626AE726D5BCE067C798B43B3DE035C3618F86CA1CF31969EB\ + B6711D354C445650D52E34F9E9A2057ECB363FE04FD3D5CE76B05E7C0CE7C563\ + C8F89AF65F3B57D6E34481A13889FACCE87AF020F0044B5EEA3C1BD48387506D\ + BD3C75ED4B9EFD7605DC3571A95B6E97F349C61C5D444A93DDE14F27C7B6EF74\ + F802EA1AB809ECBBEFD9229A85B42BC959B70BD207C06F30675B177096931759\ + 462E64B9F9F90EA5E5C5AA975A454F12AC6E4D21BC641A00B994B15E54BE2D79\ + 382A5ECF65BAA76496433D191CD0BEEB1AD979CD070CDC94FFFECD01CB3BF1E9\ + 86FEA8FE343C419AE71FC9CE7053697BCB75A45552006EFB1D4F36A34E9D70FE\ + 663C5B28D497373DB42AE1A6B8B5BD05390FBF580FCD75D857C9047FBB2A3FA8\ + 265702FD21773E124A5338E88D922A892331B9A7EE3F7375F9864E6990901D32\ + 3E37AB088528FC456B9082F40527C9565248D1D0403CEBEAE8BE8DDF290D0C0F\ + C415487747EFA5D256FA3F997E0D0F111C9F22D9F41C384C0FAA22AFE97BCCCB\ + D663268AE89A7BEC8898D5CEED1ECDFABC33205F8B01CEC18079B03BB7D5BBD8\ + EF80D6FB65FDC4F0445C8712CD717E5879663400652C16C8ECA980AFEC745A2C\ + C17D6A3EA1F9D2A4B0D534F784B35BAD97CCBB495E961D010C0A3FCF89FE7EAE\ + 091B00991EFF8BDB6E36C47FCBD1620130CAE67D68E68CFBE8D43BEBBA8B2331\ + F89F931D9FAA722789BFF1A6A0070480D87D59A94C62A8944EF5D327E7200030\ + 5502F26E7F3FF43C7C46097204C449F07C2F3DA9A9962B7AE51E6117FBF2B591\ + AB4273BA88F9C758EE64CF10FB2BF5F25B0B287F5081A79CEFDBBB0CBB70B9D9\ + DACBC1868C37B731C6C73F49F31C4F047D236DF3ED0BD2C41F4F19B9164D2DA3\ + CAC0067168746965C1B77EDE72A35F0BBD478FF21AE128D20FED009FCA1653CC\ + 16B7DE7F4FC1FBA75062B2E41BA0FFCBB8CA7213694C6947678BA2547BEF34FE\ + CD165A8ABB1DF0E52EBC0600361EFDE93031B290FA63F72F7DBA8F94FB34E6E3\ + 331C84367E4E887BBE982A905564993D7432BD2FE60061B39F0411486669FACA\ + F43E2A589EEBCC635F3D1C887C8444BD8994C2AE726F402CC846E6E150688FA9\ + EEAF836AC0EA978C776C4A14B4ECD9A54104A0D4FA8EEABBB5FBD4EEE80A19A0\ + 01547A1893BF3FAFF98994AD3E127CC4E35E13DA8EDF587DE0DB61824B2601C0\ + 46B83088A95B3DAE5CE118516F7E95E90DBD22A7315A1B990FBB81C264D4E903\ + 5935536ED84FF3D9951EED006ADB6C15F09691DC27037F19227004AE54D682F3\ + 6EE41C20A27E07F10CC3BF2CF68C92E4429D9AA75D2AE487C759AD1EF37263F3\ + 0BD4A50B4145C2B41C833C382FE4A5D15456346BF039A1E840BBF32F99AC80B4\ + A1930D5E838254F5", + ) + .unwrap(), + ] + .into_iter() + .map(|x| minicbor::decode(&x).unwrap()) + .collect() +} diff --git a/pallas-network/tests/leiosnotify.rs b/pallas-network/tests/leiosnotify.rs new file mode 100644 index 000000000..b82719dc7 --- /dev/null +++ b/pallas-network/tests/leiosnotify.rs @@ -0,0 +1,217 @@ +#![cfg(feature = "leios")] +use pallas_network::{ + facades::{PeerClient, PeerServer}, + miniprotocols::leiosnotify, +}; +use std::{ + net::{Ipv4Addr, SocketAddrV4}, + time::Duration, +}; + +use tokio::net::TcpListener; + +#[cfg(unix)] +#[tokio::test] +pub async fn leiosnotify_server_and_client_happy_path() { + use tracing::debug; + + tracing_subscriber::fmt::init(); + + let block_hash: leiosnotify::Hash = + hex::decode("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef").unwrap(); + + let rb_header: leiosnotify::Header = + hex::decode("eade0000eade0000eade0000eade0000eade0000eade0000eade0000eade0000").unwrap(); + + let block_txs_hash: leiosnotify::Hash = + hex::decode("bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0bee0").unwrap(); + + let block_slot: leiosnotify::Slot = 123456789; + let block_txs_slot: leiosnotify::Slot = 222222222; + + let vote_issuer_id: leiosnotify::Hash = + hex::decode("beedbeedbeedbeedbeedbeedbeedbeedbeedbeedbeedbeedbeedbeedbeedbeed").unwrap(); + + let listener = TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 30003)) + .await + .unwrap(); + + let server = tokio::spawn({ + let sent_header = rb_header.clone(); + let sent_block_hash = block_hash.clone(); + let sent_block_txs_hash = block_txs_hash.clone(); + let sent_vote_issuer_id = vote_issuer_id.clone(); + + async move { + // server setup + + let mut peer_server = PeerServer::accept(&listener, 0).await.unwrap(); + + let server_ln = peer_server.leiosnotify(); + + // server receives `RequestNext` from client + debug!("server waiting for request next"); + server_ln.recv_request_next().await.unwrap(); + assert_eq!(*server_ln.state(), leiosnotify::State::Busy); + + // Server sends header + server_ln + .send_block_announcement(sent_header) + .await + .unwrap(); + assert_eq!(*server_ln.state(), leiosnotify::State::Idle); + + // server receives `RequestNext` from client + debug!("server waiting for request next"); + server_ln.recv_request_next().await.unwrap(); + assert_eq!(*server_ln.state(), leiosnotify::State::Busy); + + // Server sends block offer + server_ln + .send_block_offer(block_slot, sent_block_hash) + .await + .unwrap(); + assert_eq!(*server_ln.state(), leiosnotify::State::Idle); + + // server receives `RequestNext` from client + debug!("server waiting for request next"); + server_ln.recv_request_next().await.unwrap(); + assert_eq!(*server_ln.state(), leiosnotify::State::Busy); + + // Server sends txs offer + server_ln + .send_block_txs_offer(block_txs_slot, sent_block_txs_hash) + .await + .unwrap(); + assert_eq!(*server_ln.state(), leiosnotify::State::Idle); + + // server receives `RequestNext` from client + debug!("server waiting for request next"); + server_ln.recv_request_next().await.unwrap(); + assert_eq!(*server_ln.state(), leiosnotify::State::Busy); + + // Server sends votes offer + server_ln + .send_vote_offer(vec![(block_slot, sent_vote_issuer_id)]) + .await + .unwrap(); + assert_eq!(*server_ln.state(), leiosnotify::State::Idle); + + // Server receives Done message from client + server_ln.recv_request_next().await.unwrap(); + assert_eq!(*server_ln.state(), leiosnotify::State::Done); + } + }); + + let client = tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(1)).await; + + // client setup + + let mut client_to_server_conn = PeerClient::connect("localhost:30003", 0).await.unwrap(); + + let client_ln = client_to_server_conn.leiosnotify(); + + // client sends `RequestNext`, receives block announcement + client_ln.send_request_next().await.unwrap(); + assert_eq!( + client_ln.recv_block_announcement().await.unwrap(), + rb_header, + ); + + // client sends `RequestNext`, receives block offer + client_ln.send_request_next().await.unwrap(); + assert_eq!( + client_ln.recv_block_offer().await.unwrap(), + (block_slot, block_hash), + ); + + // client sends `RequestNext`, receives tx offer + client_ln.send_request_next().await.unwrap(); + assert_eq!( + client_ln.recv_block_txs_offer().await.unwrap(), + (block_txs_slot, block_txs_hash), + ); + + // client sends `RequestNext`, receives votes offer + client_ln.send_request_next().await.unwrap(); + assert_eq!( + client_ln.recv_vote_offer().await.unwrap(), + vec![(block_slot, vote_issuer_id)], + ); + + // client sends Done + client_ln.send_done().await.unwrap(); + assert!(client_ln.is_done()) + }); + + tokio::try_join!(client, server).unwrap(); +} + +#[cfg(unix)] +#[tokio::test] +pub async fn leiosnotify_outbound_no_agency() { + let rb_header: leiosnotify::Header = + hex::decode("eade0000eade0000eade0000eade0000eade0000eade0000eade0000eade0000").unwrap(); + + let listener = TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 30004)) + .await + .unwrap(); + + let server = tokio::spawn({ + async move { + // server setup + + let mut peer_server = PeerServer::accept(&listener, 0).await.unwrap(); + + let server_ln = peer_server.leiosnotify(); + + // server is Idle + assert_eq!(*server_ln.state(), leiosnotify::State::Idle); + + // Server incorrectly tries to send message + let res = server_ln.send_block_announcement(rb_header).await; + + match res { + Err(leiosnotify::ServerError::AgencyIsTheirs) => {} + Err(leiosnotify::ServerError::InvalidOutbound) => { + tracing::warn!("Expected ServerError `AgencyIsTheirs`, got `InvalidOutbound`") + } + Err(e) => panic!("Unexpected error: {:?}", e), + Ok(_) => panic!("Server has no agency"), + } + + // Server receives Done message from client + server_ln.recv_request_next().await.unwrap(); + assert_eq!(*server_ln.state(), leiosnotify::State::Done); + } + }); + + let client = tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(1)).await; + + // client setup + + let mut client_to_server_conn = PeerClient::connect("localhost:30004", 0).await.unwrap(); + + let client_ln = client_to_server_conn.leiosnotify(); + + // client sends Done + client_ln.send_done().await.unwrap(); + assert!(client_ln.is_done()); + + // client sends `RequestNext` while not having agency + let res = client_ln.send_request_next().await; + + match res { + Err(leiosnotify::ClientError::ProtocolDone) => {} + Err(leiosnotify::ClientError::InvalidOutbound) => { + tracing::warn!("Expected ClientError `ProtocolDone`, got `InvalidOutbound`") + } + Err(e) => panic!("Unexpected error: {:?}", e), + Ok(_) => panic!("Client has no agency"), + } + }); + + tokio::try_join!(client, server).unwrap(); +} diff --git a/pallas-primitives/Cargo.toml b/pallas-primitives/Cargo.toml index b3fb4506c..8fdeda1c6 100644 --- a/pallas-primitives/Cargo.toml +++ b/pallas-primitives/Cargo.toml @@ -24,3 +24,4 @@ test-case = "3.3.1" [features] json = ["serde", "serde_json"] default = ["json"] +leios = [] diff --git a/pallas-primitives/src/leios/defs.cddl b/pallas-primitives/src/leios/defs.cddl new file mode 100644 index 000000000..6d63342e4 --- /dev/null +++ b/pallas-primitives/src/leios/defs.cddl @@ -0,0 +1,84 @@ +; conway.cddl fragments integrated to CIP draft fetched 17 Oct 2025 from cardano-scaling + +block = + [ header + , transaction_bodies : [* transaction_body] + , transaction_witness_sets : [* transaction_witness_set] + , auxiliary_data_set : {* transaction_index => auxiliary_data} + , invalid_transactions : [* transaction_index] + , ? eb_certificate : leios_certificate + ] + +header = [header_body, body_signature : kes_signature] + +header_body = + [ block_number : block_no + , slot : slot_no + , prev_hash : hash32/ nil + , issuer_vkey : vkey + , vrf_vkey : vrf_vkey + , vrf_result : vrf_cert + , block_body_size : uint .size 4 + , block_body_hash : hash32 + , ? ( announced_eb : hash32 + , announced_eb_size : uint .size 4 + ) + , ? certified_eb : bool + , operational_cert + , protocol_version + ] + + +endorser_block = + [ transaction_references : [* tx_reference] + ] + +; Reference structures +tx_reference = + [ tx_hash : hash32 ; Hash of complete transaction bytes + , tx_size : uint .size 2 ; Transaction size in bytes + ] + +leios_certificate = + [ election_id : election_id + , endorser_block_hash : hash32 + , persistent_voters : [* persistent_voter_id] + , nonpersistent_voters : {* pool_id => bls_signature} + , ? aggregate_elig_sig : bls_signature + , aggregate_vote_sig : bls_signature + ] + +leios_vote = persistent_vote / non_persistent_vote + +persistent_vote = + [ 0 + , election_id + , persistent_voter_id + , endorser_block_hash + , vote_signature + ] + +non_persistent_vote = + [ 1 + , election_id + , pool_keyhash + , eligibility_signature + , endorser_block_hash + , vote_signature + ] + +; approx size 136 + ⌈m/8⌉ + 76(n - m), where +; - n = committee size +; - m = number of persistent voters +; (see https://github.com/input-output-hk/ouroboros-leios/blob/d5f1a9bc940e69f406c3e25c0d7d9aa58cf701f8/crypto-benchmarks.rs/Specification.md?plain=1#L106) +bls_signature = bytes .size 48 + +election_id = bytes .size 8 + +persistent_voter_id = bytes .size 2 + +endorser_block_hash = hash32 + +vote_signature = bytes .size 48 + +eligibility_signature = bytes .size 48 diff --git a/pallas-primitives/src/leios/mod.rs b/pallas-primitives/src/leios/mod.rs new file mode 100644 index 000000000..4a7ebf60c --- /dev/null +++ b/pallas-primitives/src/leios/mod.rs @@ -0,0 +1,3 @@ +mod model; + +pub use model::*; diff --git a/pallas-primitives/src/leios/model.rs b/pallas-primitives/src/leios/model.rs new file mode 100644 index 000000000..5fc7a76fb --- /dev/null +++ b/pallas-primitives/src/leios/model.rs @@ -0,0 +1,214 @@ +//! Ledger primitives and cbor codec for the Conway era +//! +//! Handcrafted, idiomatic rust artifacts based on based on the [CIP proposal](https://github.com/cardano-scaling/CIPs/blob/leios/CIP-0164/README.md) at the cardano-scaling repo. + +use serde::{Deserialize, Serialize}; + +use pallas_codec::minicbor::{self, Decode, Encode}; + +pub use pallas_codec::codec_by_datatype; + +pub use crate::{ + plutus_data::*, AddrKeyhash, AssetName, Bytes, Coin, CostModel, DnsName, Epoch, ExUnits, + GenesisDelegateHash, Genesishash, Hash, IPv4, IPv6, KeepRaw, Metadata, Metadatum, + MetadatumLabel, NetworkId, NonEmptySet, NonZeroInt, Nonce, NonceVariant, Nullable, + PlutusScript, PolicyId, PoolKeyhash, PoolMetadata, PoolMetadataHash, Port, PositiveCoin, + PositiveInterval, ProtocolVersion, RationalNumber, Relay, RewardAccount, ScriptHash, Set, + StakeCredential, TransactionIndex, TransactionInput, UnitInterval, VrfCert, VrfKeyhash, +}; + +use crate::{babbage, conway, BTreeMap}; + +pub use babbage::{ + derive_tagged_vrf_output, DatumHash, DatumOption, OperationalCert, VrfDerivation, +}; + +pub use crate::alonzo::{AuxiliaryData, BootstrapWitness, NativeScript, VKeyWitness}; + +pub use conway::{ + Anchor, Certificate, CommitteeColdCredential, CommitteeHotCredential, Constitution, CostModels, + DRep, DRepCredential, DRepVotingThresholds, ExUnitPrices, GovActionId, Language, + LegacyTransactionOutput, Mint, Multiasset, PoolVotingThresholds, PostAlonzoAuxiliaryData, + PostAlonzoTransactionOutput, ProposalProcedure, ProtocolParamUpdate, Redeemer, RedeemerTag, + Redeemers, RedeemersKey, RedeemersValue, RequiredSigners, ScriptRef, TransactionBody, + TransactionOutput, Tx, Update, Value, Vote, Voter, VotingProcedure, VotingProcedures, + Withdrawals, WitnessSet, +}; + +/// Leios ranking block. +/// +/// This is the original Praos (Conway) block, with additional fields for announcing and +/// and certifying previously announced endorser blocks. +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)] +pub struct Block<'b> { + #[n(0)] + pub header: KeepRaw<'b, Header>, + + #[b(1)] + pub transaction_bodies: Vec>>, + + #[n(2)] + pub transaction_witness_sets: Vec>>, + + #[n(3)] + pub auxiliary_data_set: BTreeMap>, + + #[n(4)] + pub invalid_transactions: Option>, + + #[n(5)] + pub eb_certificate: Option>, +} + +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +pub struct EbAnnouncement { + #[n(0)] + pub announced_eb: Option>, + + #[n(1)] + pub announced_eb_size: Option, +} + +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +pub struct HeaderBody { + #[n(0)] + pub block_number: u64, + + #[n(1)] + pub slot: u64, + + #[n(2)] + pub prev_hash: Option>, + + #[n(3)] + pub issuer_vkey: Bytes, + + #[n(4)] + pub vrf_vkey: Bytes, + + #[n(5)] + pub vrf_result: VrfCert, + + #[n(6)] + pub block_body_size: u64, + + #[n(7)] + pub block_body_hash: Hash<32>, + + #[n(8)] + pub eb_announcement: Option, + + #[n(9)] + pub certified_eb: Option, + + #[n(10)] + pub operational_cert: OperationalCert, + + #[n(11)] + pub protocol_version: ProtocolVersion, +} + +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +pub struct Header { + #[n(0)] + pub header_body: HeaderBody, + + #[n(1)] + pub body_signature: Bytes, +} + +impl HeaderBody { + pub fn leader_vrf_output(&self) -> Vec { + derive_tagged_vrf_output(&self.vrf_result.0, VrfDerivation::Leader) + } + + pub fn nonce_vrf_output(&self) -> Vec { + derive_tagged_vrf_output(&self.vrf_result.0, VrfDerivation::Nonce) + } +} + +/// Leios transaction type +/// +/// It saves CBOR data for hashing purposes. +pub type LeiosTx<'a> = KeepRaw<'a, Tx<'a>>; + +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +#[cbor(transparent)] +#[serde(transparent)] +pub struct EndorserBlock { + #[n(0)] + pub transaction_references: Vec, +} + +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +pub struct TxReference { + #[n(0)] + pub tx_hash: Hash<32>, + + #[n(1)] + pub tx_size: u16, +} + +pub type BlsSignature = Bytes; // 48 bytes + +// pub type ElectionId = Bytes; // 8 bytes +// pub type PersistentVoterId = Bytes; // 2 bytes +// pub type EndorserBlockHash = Hash<32>; + +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +pub struct LeiosCertificate { + #[n(0)] + pub election_id: Bytes, + + #[n(1)] + pub endorser_block_hash: Hash<32>, + + #[n(2)] + pub persistent_voters: Vec, + + #[n(3)] + pub nonpersistent_voters: BTreeMap, + + #[n(4)] + pub aggregate_elig_sig: Option, + + #[n(5)] + pub aggregate_vote_sig: BlsSignature, +} + +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +#[cbor(flat)] +pub enum LeiosVote { + #[n(0)] + Persistent { + #[n(0)] + election_id: Bytes, + + #[n(1)] + persistent_voter_id: Bytes, + + #[n(2)] + endorser_block_hash: Hash<32>, + + #[n(3)] + vote_signature: BlsSignature, + }, + + #[n(1)] + NonPersistent { + #[n(0)] + election_id: Bytes, + + #[n(1)] + pool_id: PoolKeyhash, + + #[n(2)] + eligibility_signature: BlsSignature, + + #[n(3)] + endorser_block_hash: Hash<32>, + + #[n(5)] + vote_signature: BlsSignature, + }, +} diff --git a/pallas-primitives/src/lib.rs b/pallas-primitives/src/lib.rs index 03269ff5c..cc67bb5e9 100644 --- a/pallas-primitives/src/lib.rs +++ b/pallas-primitives/src/lib.rs @@ -7,6 +7,9 @@ pub mod alonzo; pub mod babbage; pub mod byron; pub mod conway; + +#[cfg(feature = "leios")] +pub mod leios; pub use plutus_data::*; pub use framework::*; diff --git a/pallas-traverse/Cargo.toml b/pallas-traverse/Cargo.toml index c6e62f1a4..217076f7b 100644 --- a/pallas-traverse/Cargo.toml +++ b/pallas-traverse/Cargo.toml @@ -25,3 +25,4 @@ serde = "1.0.155" [features] unstable = [] +leios = ["pallas-primitives/leios"] diff --git a/pallas-traverse/src/hashes.rs b/pallas-traverse/src/hashes.rs index 5ba9c7131..b45084566 100644 --- a/pallas-traverse/src/hashes.rs +++ b/pallas-traverse/src/hashes.rs @@ -6,6 +6,9 @@ use pallas_crypto::{ }; use pallas_primitives::{alonzo, babbage, byron, conway}; +#[cfg(feature = "leios")] +use pallas_primitives::leios; + impl ComputeHash<32> for byron::EbbHead { fn compute_hash(&self) -> Hash<32> { // hash expects to have a prefix for the type of block @@ -159,6 +162,22 @@ impl ComputeHash<28> for PublicKey { } } +// leios + +#[cfg(feature = "leios")] +impl ComputeHash<32> for leios::Tx<'_> { + fn compute_hash(&self) -> Hash<32> { + Hasher::<256>::hash_cbor(self) + } +} + +#[cfg(feature = "leios")] +impl OriginalHash<32> for leios::LeiosTx<'_> { + fn original_hash(&self) -> pallas_crypto::hash::Hash<32> { + Hasher::<256>::hash(self.raw_cbor()) + } +} + #[cfg(test)] mod tests { use crate::{Era, MultiEraTx}; diff --git a/pallas/Cargo.toml b/pallas/Cargo.toml index 37c327ce2..ea5d02f6e 100644 --- a/pallas/Cargo.toml +++ b/pallas/Cargo.toml @@ -26,6 +26,7 @@ pallas-hardano = { version = "=1.0.0-alpha.2", path = "../pallas-hardano/", opti [features] hardano = ["pallas-hardano"] unstable = ["hardano", "pallas-traverse/unstable"] +leios = ["pallas-network/leios", "pallas-primitives/leios", "pallas-traverse/leios"] # pallas-validate feature flags phase2 = ["pallas-validate/phase2"]