From 2ffb3febd85329e0abfe0c0db243bf3a632350fb Mon Sep 17 00:00:00 2001 From: Romain Ruetschi <106849+romac@users.noreply.github.com> Date: Tue, 6 Feb 2024 13:51:33 +0100 Subject: [PATCH 01/50] Implement TCP-based broadcast network --- code/Cargo.toml | 3 + code/common/src/context.rs | 2 +- code/common/src/proposal.rs | 2 +- code/common/src/vote.rs | 2 +- code/node/Cargo.toml | 22 +++ code/node/src/lib.rs | 2 + code/node/src/network.rs | 19 +++ code/node/src/network/broadcast.rs | 251 +++++++++++++++++++++++++++++ code/node/src/network/msg.rs | 72 +++++++++ code/node/src/node.rs | 0 10 files changed, 372 insertions(+), 3 deletions(-) create mode 100644 code/node/Cargo.toml create mode 100644 code/node/src/lib.rs create mode 100644 code/node/src/network.rs create mode 100644 code/node/src/network/broadcast.rs create mode 100644 code/node/src/network/msg.rs create mode 100644 code/node/src/node.rs diff --git a/code/Cargo.toml b/code/Cargo.toml index f1dff359e..280b9c5af 100644 --- a/code/Cargo.toml +++ b/code/Cargo.toml @@ -5,6 +5,7 @@ members = [ "common", "driver", "itf", + "node", "round", "test", "vote", @@ -32,3 +33,5 @@ serde_json = "1.0" serde_with = "3.4" sha2 = "0.10.8" signature = "2.1.0" +tokio = "1.35.1" +tokio-stream = "0.1" diff --git a/code/common/src/context.rs b/code/common/src/context.rs index a6d6521d0..24caddfce 100644 --- a/code/common/src/context.rs +++ b/code/common/src/context.rs @@ -7,7 +7,7 @@ use crate::{ /// that are used in the consensus engine. pub trait Context where - Self: Sized, + Self: Sized + Send + Sync + 'static, { /// The type of address of a validator. type Address: Address; diff --git a/code/common/src/proposal.rs b/code/common/src/proposal.rs index 4adcc3b38..d63168493 100644 --- a/code/common/src/proposal.rs +++ b/code/common/src/proposal.rs @@ -5,7 +5,7 @@ use crate::{Context, Round}; /// Defines the requirements for a proposal type. pub trait Proposal where - Self: Clone + Debug + PartialEq + Eq, + Self: Clone + Debug + Eq + Send + Sync + 'static, Ctx: Context, { /// The height for which the proposal is for. diff --git a/code/common/src/vote.rs b/code/common/src/vote.rs index 572beea45..82f9c77a9 100644 --- a/code/common/src/vote.rs +++ b/code/common/src/vote.rs @@ -18,7 +18,7 @@ pub enum VoteType { /// include information about the validator signing it. pub trait Vote where - Self: Clone + Debug + Eq, + Self: Clone + Debug + Eq + Send + Sync + 'static, Ctx: Context, { /// The height for which the vote is for. diff --git a/code/node/Cargo.toml b/code/node/Cargo.toml new file mode 100644 index 000000000..98933b090 --- /dev/null +++ b/code/node/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "malachite-node" +description = "Node for running the Malachite consensus engine" + +version.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +malachite-common = { version = "0.1.0", path = "../common" } +malachite-driver = { version = "0.1.0", path = "../driver" } +malachite-round = { version = "0.1.0", path = "../round" } +malachite-vote = { version = "0.1.0", path = "../vote" } + +futures = { workspace = true } +tokio = { workspace = true, features = ["full"] } +tokio-stream = { workspace = true } + +[dev-dependencies] +malachite-test = { version = "0.1.0", path = "../test" } diff --git a/code/node/src/lib.rs b/code/node/src/lib.rs new file mode 100644 index 000000000..4c4746032 --- /dev/null +++ b/code/node/src/lib.rs @@ -0,0 +1,2 @@ +pub mod network; +pub mod node; diff --git a/code/node/src/network.rs b/code/node/src/network.rs new file mode 100644 index 000000000..b979b351f --- /dev/null +++ b/code/node/src/network.rs @@ -0,0 +1,19 @@ +use malachite_common::Context; + +pub mod broadcast; +mod msg; + +pub use self::msg::Msg; + +#[allow(async_fn_in_trait)] +pub trait Network { + async fn recv(&mut self) -> Option>; + async fn broadcast(&mut self, msg: Msg); + + async fn broadcast_vote(&mut self, vote: Ctx::Vote) { + self.broadcast(Msg::Vote(vote)).await + } + async fn broadcast_proposal(&mut self, proposal: Ctx::Proposal) { + self.broadcast(Msg::Proposal(proposal)).await + } +} diff --git a/code/node/src/network/broadcast.rs b/code/node/src/network/broadcast.rs new file mode 100644 index 000000000..648779e42 --- /dev/null +++ b/code/node/src/network/broadcast.rs @@ -0,0 +1,251 @@ +use core::fmt; +use std::fmt::Debug; +use std::net::SocketAddr; + +use futures::channel::oneshot; +use malachite_common::Context; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::sync::{broadcast, mpsc}; + +use super::Msg; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct PeerId(String); + +impl fmt::Display for PeerId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +pub enum PeerEvent { + ConnectToPeer(PeerInfo, oneshot::Sender<()>), + Broadcast(Msg, oneshot::Sender<()>), +} + +impl Debug for PeerEvent { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PeerEvent::ConnectToPeer(peer_info, _) => { + write!(f, "ConnectToPeer({peer_info:?})") + } + PeerEvent::Broadcast(msg, _) => { + write!(f, "Broadcast({msg:?})") + } + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct PeerInfo { + pub id: PeerId, + pub addr: SocketAddr, +} + +pub struct Peer { + id: PeerId, + addr: SocketAddr, + _marker: std::marker::PhantomData, +} + +impl Peer { + pub fn new(info: PeerInfo) -> Self { + Self { + id: info.id, + addr: info.addr, + _marker: std::marker::PhantomData, + } + } + + pub async fn run(self) -> Handle { + let (tx_peer_event, mut rx_peer_event) = mpsc::channel::>(16); + let (tx_msg, rx_msg) = mpsc::channel::<(PeerId, Msg)>(16); + let (tx_broadcast_to_peers, _) = broadcast::channel::<(PeerId, Msg)>(16); + let (tx_spawned, rx_spawned) = oneshot::channel(); + + tokio::spawn(listen(self.id.clone(), self.addr, tx_spawned, tx_msg)); + + let id = self.id.clone(); + tokio::spawn(async move { + while let Some(event) = rx_peer_event.recv().await { + match event { + PeerEvent::ConnectToPeer(peer_info, done) => { + connect_to_peer(id.clone(), peer_info, done, &tx_broadcast_to_peers).await; + } + + PeerEvent::Broadcast(msg, done) => { + println!("[{id}] Broadcasting message: {msg:?}"); + tx_broadcast_to_peers.send((id.clone(), msg)).unwrap(); + done.send(()).unwrap(); + } + } + } + }); + + rx_spawned.await.unwrap(); + + Handle { + peer_id: self.id, + rx_msg, + tx_peer_event, + } + } +} + +async fn connect_to_peer( + id: PeerId, + peer_info: PeerInfo, + done: oneshot::Sender<()>, + per_peer_tx: &broadcast::Sender<(PeerId, Msg)>, +) { + println!("[{id}] Connecting to {peer_info:?}..."); + + let mut stream = TcpStream::connect(peer_info.addr).await.unwrap(); + done.send(()).unwrap(); + + let mut per_peer_rx = per_peer_tx.subscribe(); + + tokio::spawn(async move { + loop { + let (from, msg) = per_peer_rx.recv().await.unwrap(); + if from == peer_info.id { + continue; + } + + println!("[{id}] Sending message to {peer_info:?}: {msg:?}"); + + let bytes = msg.as_bytes(); + stream.write_u32(bytes.len() as u32).await.unwrap(); + stream.write_all(&bytes).await.unwrap(); + stream.flush().await.unwrap(); + } + }); +} + +async fn listen( + id: PeerId, + addr: SocketAddr, + tx_spawned: oneshot::Sender<()>, + tx_msg: mpsc::Sender<(PeerId, Msg)>, +) -> ! { + let listener = TcpListener::bind(addr).await.unwrap(); + println!("[{id}] Listening on {addr}..."); + + tx_spawned.send(()).unwrap(); + + loop { + let (mut socket, _) = listener.accept().await.unwrap(); + + println!( + "[{id}] Accepted connection from {peer}...", + peer = socket.peer_addr().unwrap() + ); + + let id = id.clone(); + let tx_msg = tx_msg.clone(); + + tokio::spawn(async move { + let len = socket.read_u32().await.unwrap(); + let mut buf = vec![0; len as usize]; + socket.read_exact(&mut buf).await.unwrap(); + let msg: Msg = Msg::from_bytes(&buf); + + println!( + "[{id}] Received message from {peer}: {msg:?}", + peer = socket.peer_addr().unwrap(), + ); + + tx_msg.send((id.clone(), msg)).await.unwrap(); // FIXME + }); + } +} + +pub struct Handle { + peer_id: PeerId, + rx_msg: mpsc::Receiver<(PeerId, Msg)>, + tx_peer_event: mpsc::Sender>, +} + +impl Handle { + pub fn peer_id(&self) -> &PeerId { + &self.peer_id + } + + pub async fn recv(&mut self) -> Option<(PeerId, Msg)> { + self.rx_msg.recv().await + } + + pub async fn broadcast(&self, msg: Msg) { + let (tx_done, rx_done) = oneshot::channel(); + + self.tx_peer_event + .send(PeerEvent::Broadcast(msg, tx_done)) + .await + .unwrap(); + + rx_done.await.unwrap(); + } + + pub async fn connect_to_peer(&self, peer_info: PeerInfo) { + let (tx_done, rx_done) = oneshot::channel(); + + self.tx_peer_event + .send(PeerEvent::ConnectToPeer(peer_info, tx_done)) + .await + .unwrap(); + + rx_done.await.unwrap(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use malachite_test::TestContext; + + #[tokio::test] + async fn test_peer() { + let peer1_id = PeerId("peer-1".to_string()); + let peer1_info = PeerInfo { + id: peer1_id.clone(), + addr: "127.0.0.1:12001".parse().unwrap(), + }; + + let peer2_id = PeerId("peer-2".to_string()); + let peer2_info = PeerInfo { + id: peer2_id.clone(), + addr: "127.0.0.1:12002".parse().unwrap(), + }; + + let peer3_id = PeerId("peer-3".to_string()); + let peer3_info = PeerInfo { + id: peer3_id.clone(), + addr: "127.0.0.1:12003".parse().unwrap(), + }; + + let peer1: Peer = Peer::new(peer1_info.clone()); + let peer2: Peer = Peer::new(peer2_info.clone()); + let peer3: Peer = Peer::new(peer3_info.clone()); + + let handle1 = peer1.run().await; + let mut handle2 = peer2.run().await; + let mut handle3 = peer3.run().await; + + handle1.connect_to_peer(peer2_info.clone()).await; + handle1.connect_to_peer(peer3_info.clone()).await; + + handle2.connect_to_peer(peer1_info.clone()).await; + handle2.connect_to_peer(peer3_info.clone()).await; + + handle3.connect_to_peer(peer1_info.clone()).await; + handle3.connect_to_peer(peer2_info.clone()).await; + + handle1.broadcast(Msg::Dummy(1)).await; + + let msg2 = handle2.recv().await.unwrap(); + dbg!(&msg2); + let msg3 = handle3.recv().await.unwrap(); + dbg!(&msg3); + } +} diff --git a/code/node/src/network/msg.rs b/code/node/src/network/msg.rs new file mode 100644 index 000000000..1ddad0a14 --- /dev/null +++ b/code/node/src/network/msg.rs @@ -0,0 +1,72 @@ +use core::fmt; + +use malachite_common::Context; + +pub enum Msg { + Vote(Ctx::Vote), + Proposal(Ctx::Proposal), + + #[cfg(test)] + Dummy(u32), +} + +impl Msg { + pub fn as_bytes(&self) -> Vec { + match self { + Msg::Vote(_vote) => todo!(), + Msg::Proposal(_proposal) => todo!(), + + #[cfg(test)] + Msg::Dummy(n) => [&[0x42], n.to_be_bytes().as_slice()].concat(), + } + } + + pub fn from_bytes(bytes: &[u8]) -> Self { + match bytes { + #[cfg(test)] + [0x42, a, b, c, d] => Msg::Dummy(u32::from_be_bytes([*a, *b, *c, *d])), + + _ => todo!(), + } + } +} + +impl Clone for Msg { + fn clone(&self) -> Self { + match self { + Msg::Vote(vote) => Msg::Vote(vote.clone()), + Msg::Proposal(proposal) => Msg::Proposal(proposal.clone()), + + #[cfg(test)] + Msg::Dummy(n) => Msg::Dummy(*n), + } + } +} + +impl fmt::Debug for Msg { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Msg::Vote(vote) => write!(f, "Vote({vote:?})"), + Msg::Proposal(proposal) => write!(f, "Proposal({proposal:?})"), + + #[cfg(test)] + Msg::Dummy(n) => write!(f, "Dummy({n:?})"), + } + } +} + +impl PartialEq for Msg { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Msg::Vote(vote), Msg::Vote(other_vote)) => vote == other_vote, + (Msg::Proposal(proposal), Msg::Proposal(other_proposal)) => proposal == other_proposal, + + #[cfg(test)] + (Msg::Dummy(n1), Msg::Dummy(n2)) => n1 == n2, + + _ => false, + } + } +} + +impl Eq for Msg {} diff --git a/code/node/src/node.rs b/code/node/src/node.rs new file mode 100644 index 000000000..e69de29bb From b1287cc8283da8bc3d3b0d1d0f14e3d4307e2b31 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Mon, 19 Feb 2024 20:56:52 +0100 Subject: [PATCH 02/50] Identify which peer sent a message --- code/node/src/network/broadcast.rs | 52 ++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/code/node/src/network/broadcast.rs b/code/node/src/network/broadcast.rs index 648779e42..34d4537d5 100644 --- a/code/node/src/network/broadcast.rs +++ b/code/node/src/network/broadcast.rs @@ -43,6 +43,12 @@ pub struct PeerInfo { pub addr: SocketAddr, } +impl fmt::Display for PeerInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{id} ({addr})", id = self.id, addr = self.addr) + } +} + pub struct Peer { id: PeerId, addr: SocketAddr, @@ -67,6 +73,7 @@ impl Peer { tokio::spawn(listen(self.id.clone(), self.addr, tx_spawned, tx_msg)); let id = self.id.clone(); + tokio::spawn(async move { while let Some(event) = rx_peer_event.recv().await { match event { @@ -99,13 +106,15 @@ async fn connect_to_peer( done: oneshot::Sender<()>, per_peer_tx: &broadcast::Sender<(PeerId, Msg)>, ) { - println!("[{id}] Connecting to {peer_info:?}..."); + println!("[{id}] Connecting to {peer_info}..."); let mut stream = TcpStream::connect(peer_info.addr).await.unwrap(); done.send(()).unwrap(); let mut per_peer_rx = per_peer_tx.subscribe(); + send_peer_id(&mut stream, id.clone()).await; + tokio::spawn(async move { loop { let (from, msg) = per_peer_rx.recv().await.unwrap(); @@ -113,7 +122,7 @@ async fn connect_to_peer( continue; } - println!("[{id}] Sending message to {peer_info:?}: {msg:?}"); + println!("[{id}] Sending message to {peer_info}: {msg:?}"); let bytes = msg.as_bytes(); stream.write_u32(bytes.len() as u32).await.unwrap(); @@ -127,7 +136,7 @@ async fn listen( id: PeerId, addr: SocketAddr, tx_spawned: oneshot::Sender<()>, - tx_msg: mpsc::Sender<(PeerId, Msg)>, + tx_received: mpsc::Sender<(PeerId, Msg)>, ) -> ! { let listener = TcpListener::bind(addr).await.unwrap(); println!("[{id}] Listening on {addr}..."); @@ -142,8 +151,10 @@ async fn listen( peer = socket.peer_addr().unwrap() ); + let peer_id = read_peer_id(&mut socket).await; + let id = id.clone(); - let tx_msg = tx_msg.clone(); + let tx_received = tx_received.clone(); tokio::spawn(async move { let len = socket.read_u32().await.unwrap(); @@ -152,15 +163,30 @@ async fn listen( let msg: Msg = Msg::from_bytes(&buf); println!( - "[{id}] Received message from {peer}: {msg:?}", - peer = socket.peer_addr().unwrap(), + "[{id}] Received message from {peer_id} ({addr}): {msg:?}", + addr = socket.peer_addr().unwrap(), ); - tx_msg.send((id.clone(), msg)).await.unwrap(); // FIXME + tx_received.send((peer_id.clone(), msg)).await.unwrap(); // FIXME }); } } +async fn send_peer_id(socket: &mut TcpStream, id: PeerId) { + let bytes = id.0.as_bytes(); + socket.write_u32(bytes.len() as u32).await.unwrap(); + socket.write_all(&bytes).await.unwrap(); + socket.flush().await.unwrap(); +} + +async fn read_peer_id(socket: &mut TcpStream) -> PeerId { + let len = socket.read_u32().await.unwrap(); + let mut buf = vec![0; len as usize]; + socket.read_exact(&mut buf).await.unwrap(); + let id = String::from_utf8(buf).unwrap(); + PeerId(id) +} + pub struct Handle { peer_id: PeerId, rx_msg: mpsc::Receiver<(PeerId, Msg)>, @@ -201,8 +227,12 @@ impl Handle { #[cfg(test)] mod tests { - use super::*; + use std::time::Duration; + use malachite_test::TestContext; + use tokio::time::timeout; + + use super::*; #[tokio::test] async fn test_peer() { @@ -243,9 +273,11 @@ mod tests { handle1.broadcast(Msg::Dummy(1)).await; - let msg2 = handle2.recv().await.unwrap(); + let deadline = Duration::from_millis(100); + + let msg2 = timeout(deadline, handle2.recv()).await.unwrap(); dbg!(&msg2); - let msg3 = handle3.recv().await.unwrap(); + let msg3 = timeout(deadline, handle3.recv()).await.unwrap(); dbg!(&msg3); } } From 8d05ecb6a506e4ac7863cce7ecb6ce829a4b47c7 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Mon, 19 Feb 2024 20:57:19 +0100 Subject: [PATCH 03/50] Fix warning --- code/node/src/network/broadcast.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/node/src/network/broadcast.rs b/code/node/src/network/broadcast.rs index 34d4537d5..8e3442b70 100644 --- a/code/node/src/network/broadcast.rs +++ b/code/node/src/network/broadcast.rs @@ -175,7 +175,7 @@ async fn listen( async fn send_peer_id(socket: &mut TcpStream, id: PeerId) { let bytes = id.0.as_bytes(); socket.write_u32(bytes.len() as u32).await.unwrap(); - socket.write_all(&bytes).await.unwrap(); + socket.write_all(bytes).await.unwrap(); socket.flush().await.unwrap(); } From abb62cc5a9d2831e63b2c04e206716938e581921 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 20 Feb 2024 08:01:12 +0100 Subject: [PATCH 04/50] Implement `Network` for `broadcast::Handle` --- code/node/src/network.rs | 13 ++++++++++++- code/node/src/network/broadcast.rs | 21 +++++++++++---------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/code/node/src/network.rs b/code/node/src/network.rs index b979b351f..826eedb50 100644 --- a/code/node/src/network.rs +++ b/code/node/src/network.rs @@ -1,3 +1,5 @@ +use core::fmt; + use malachite_common::Context; pub mod broadcast; @@ -5,9 +7,18 @@ mod msg; pub use self::msg::Msg; +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct PeerId(String); + +impl fmt::Display for PeerId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + #[allow(async_fn_in_trait)] pub trait Network { - async fn recv(&mut self) -> Option>; + async fn recv(&mut self) -> Option<(PeerId, Msg)>; async fn broadcast(&mut self, msg: Msg); async fn broadcast_vote(&mut self, vote: Ctx::Vote) { diff --git a/code/node/src/network/broadcast.rs b/code/node/src/network/broadcast.rs index 8e3442b70..f09d90e38 100644 --- a/code/node/src/network/broadcast.rs +++ b/code/node/src/network/broadcast.rs @@ -8,16 +8,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::{broadcast, mpsc}; -use super::Msg; - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct PeerId(String); - -impl fmt::Display for PeerId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(&self.0, f) - } -} +use super::{Msg, Network, PeerId}; pub enum PeerEvent { ConnectToPeer(PeerInfo, oneshot::Sender<()>), @@ -225,6 +216,16 @@ impl Handle { } } +impl Network for Handle { + async fn recv(&mut self) -> Option<(PeerId, Msg)> { + Handle::recv(self).await + } + + async fn broadcast(&mut self, msg: Msg) { + Handle::broadcast(self, msg).await; + } +} + #[cfg(test)] mod tests { use std::time::Duration; From 45e2f177a68eeba8a4ee10a7eb1c6d9613ba4af6 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 20 Feb 2024 08:44:32 +0100 Subject: [PATCH 05/50] Remove manual impls of common trait and use `derive-where` instead --- code/node/Cargo.toml | 1 + code/node/src/network/msg.rs | 83 ++++++++++++++++++------------------ 2 files changed, 43 insertions(+), 41 deletions(-) diff --git a/code/node/Cargo.toml b/code/node/Cargo.toml index 98933b090..51dbe53b8 100644 --- a/code/node/Cargo.toml +++ b/code/node/Cargo.toml @@ -14,6 +14,7 @@ malachite-driver = { version = "0.1.0", path = "../driver" } malachite-round = { version = "0.1.0", path = "../round" } malachite-vote = { version = "0.1.0", path = "../vote" } +derive-where = { workspace = true } futures = { workspace = true } tokio = { workspace = true, features = ["full"] } tokio-stream = { workspace = true } diff --git a/code/node/src/network/msg.rs b/code/node/src/network/msg.rs index 1ddad0a14..f14e56024 100644 --- a/code/node/src/network/msg.rs +++ b/code/node/src/network/msg.rs @@ -1,7 +1,8 @@ -use core::fmt; +use derive_where::derive_where; use malachite_common::Context; +#[derive_where(Clone, Debug, PartialEq, Eq)] pub enum Msg { Vote(Ctx::Vote), Proposal(Ctx::Proposal), @@ -30,43 +31,43 @@ impl Msg { } } } - -impl Clone for Msg { - fn clone(&self) -> Self { - match self { - Msg::Vote(vote) => Msg::Vote(vote.clone()), - Msg::Proposal(proposal) => Msg::Proposal(proposal.clone()), - - #[cfg(test)] - Msg::Dummy(n) => Msg::Dummy(*n), - } - } -} - -impl fmt::Debug for Msg { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Msg::Vote(vote) => write!(f, "Vote({vote:?})"), - Msg::Proposal(proposal) => write!(f, "Proposal({proposal:?})"), - - #[cfg(test)] - Msg::Dummy(n) => write!(f, "Dummy({n:?})"), - } - } -} - -impl PartialEq for Msg { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Msg::Vote(vote), Msg::Vote(other_vote)) => vote == other_vote, - (Msg::Proposal(proposal), Msg::Proposal(other_proposal)) => proposal == other_proposal, - - #[cfg(test)] - (Msg::Dummy(n1), Msg::Dummy(n2)) => n1 == n2, - - _ => false, - } - } -} - -impl Eq for Msg {} +// +// impl Clone for Msg { +// fn clone(&self) -> Self { +// match self { +// Msg::Vote(vote) => Msg::Vote(vote.clone()), +// Msg::Proposal(proposal) => Msg::Proposal(proposal.clone()), +// +// #[cfg(test)] +// Msg::Dummy(n) => Msg::Dummy(*n), +// } +// } +// } +// +// impl fmt::Debug for Msg { +// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +// match self { +// Msg::Vote(vote) => write!(f, "Vote({vote:?})"), +// Msg::Proposal(proposal) => write!(f, "Proposal({proposal:?})"), +// +// #[cfg(test)] +// Msg::Dummy(n) => write!(f, "Dummy({n:?})"), +// } +// } +// } +// +// impl PartialEq for Msg { +// fn eq(&self, other: &Self) -> bool { +// match (self, other) { +// (Msg::Vote(vote), Msg::Vote(other_vote)) => vote == other_vote, +// (Msg::Proposal(proposal), Msg::Proposal(other_proposal)) => proposal == other_proposal, +// +// #[cfg(test)] +// (Msg::Dummy(n1), Msg::Dummy(n2)) => n1 == n2, +// +// _ => false, +// } +// } +// } +// +// impl Eq for Msg {} From 31c0ca9f8a401df73c52016c20cdff7df9711c29 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 20 Feb 2024 08:57:13 +0100 Subject: [PATCH 06/50] First draft of Protobuf definitions for Malachite types --- code/Cargo.toml | 16 +++- code/common/Cargo.toml | 5 +- code/common/src/height.rs | 3 + code/common/src/lib.rs | 2 + code/common/src/proposal.rs | 3 + code/common/src/round.rs | 17 ++++ code/common/src/validator_set.rs | 3 + code/common/src/value.rs | 31 ++++++++ code/common/src/vote.rs | 24 ++++++ code/node/Cargo.toml | 17 ++-- code/node/src/network/broadcast.rs | 7 +- code/node/src/network/msg.rs | 124 +++++++++++++++++------------ code/proto/Cargo.toml | 15 ++++ code/proto/build.rs | 9 +++ code/proto/src/lib.rs | 55 +++++++++++++ code/proto/src/malachite.proto | 43 ++++++++++ code/test/Cargo.toml | 12 +-- code/test/src/height.rs | 16 ++++ code/test/src/proposal.rs | 24 ++++++ code/test/src/validator_set.rs | 26 ++++++ code/test/src/value.rs | 45 +++++++++++ code/test/src/vote.rs | 35 ++++++++ 22 files changed, 463 insertions(+), 69 deletions(-) create mode 100644 code/proto/Cargo.toml create mode 100644 code/proto/build.rs create mode 100644 code/proto/src/lib.rs create mode 100644 code/proto/src/malachite.proto diff --git a/code/Cargo.toml b/code/Cargo.toml index 280b9c5af..136214354 100644 --- a/code/Cargo.toml +++ b/code/Cargo.toml @@ -5,7 +5,8 @@ members = [ "common", "driver", "itf", - "node", + "node", + "proto", "round", "test", "vote", @@ -19,6 +20,15 @@ license = "Apache-2.0" publish = false [workspace.dependencies] +malachite-common = { version = "0.1.0", path = "common" } +malachite-driver = { version = "0.1.0", path = "driver" } +malachite-itf = { version = "0.1.0", path = "itf" } +malachite-node = { version = "0.1.0", path = "node" } +malachite-proto = { version = "0.1.0", path = "proto" } +malachite-round = { version = "0.1.0", path = "round" } +malachite-test = { version = "0.1.0", path = "test" } +malachite-vote = { version = "0.1.0", path = "vote" } + derive-where = "1.2.7" ed25519-consensus = "2.1.0" futures = "0.3" @@ -27,11 +37,15 @@ itf = "0.2.2" num-bigint = "0.4.4" num-traits = "0.2.17" pretty_assertions = "1.4" +prost = "0.12.3" +prost-types = "0.12.3" +prost-build = "0.12.3" rand = { version = "0.8.5", features = ["std_rng"] } serde = "1.0" serde_json = "1.0" serde_with = "3.4" sha2 = "0.10.8" signature = "2.1.0" +thiserror = "1.0" tokio = "1.35.1" tokio-stream = "0.1" diff --git a/code/common/Cargo.toml b/code/common/Cargo.toml index 90510329e..dd356d70c 100644 --- a/code/common/Cargo.toml +++ b/code/common/Cargo.toml @@ -9,5 +9,6 @@ license.workspace = true publish.workspace = true [dependencies] -derive-where.workspace = true -signature.workspace = true +malachite-proto.workspace = true +derive-where.workspace = true +signature.workspace = true diff --git a/code/common/src/height.rs b/code/common/src/height.rs index 5f458c123..2293bf2c2 100644 --- a/code/common/src/height.rs +++ b/code/common/src/height.rs @@ -1,5 +1,7 @@ use core::fmt::Debug; +use malachite_proto::Protobuf; + /// Defines the requirements for a height type. /// /// A height denotes the number of blocks (values) created since the chain began. @@ -8,5 +10,6 @@ use core::fmt::Debug; pub trait Height where Self: Default + Copy + Clone + Debug + PartialEq + Eq + PartialOrd + Ord, + Self: Protobuf, { } diff --git a/code/common/src/lib.rs b/code/common/src/lib.rs index 3abfccdcd..3c7f702a6 100644 --- a/code/common/src/lib.rs +++ b/code/common/src/lib.rs @@ -12,6 +12,8 @@ #![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::panic))] #![cfg_attr(coverage_nightly, feature(coverage_attribute))] +extern crate alloc; + mod context; mod height; mod proposal; diff --git a/code/common/src/proposal.rs b/code/common/src/proposal.rs index d63168493..c8c2a21db 100644 --- a/code/common/src/proposal.rs +++ b/code/common/src/proposal.rs @@ -1,11 +1,14 @@ use core::fmt::Debug; +use malachite_proto::Protobuf; + use crate::{Context, Round}; /// Defines the requirements for a proposal type. pub trait Proposal where Self: Clone + Debug + Eq + Send + Sync + 'static, + Self: Protobuf, Ctx: Context, { /// The height for which the proposal is for. diff --git a/code/common/src/round.rs b/code/common/src/round.rs index 713c8954d..57a5c2f34 100644 --- a/code/common/src/round.rs +++ b/code/common/src/round.rs @@ -1,4 +1,5 @@ use core::cmp; +use core::convert::Infallible; /// A round number. /// @@ -72,6 +73,22 @@ impl Ord for Round { } } +impl TryFrom for Round { + type Error = Infallible; + + fn try_from(proto: malachite_proto::Round) -> Result { + Ok(Self::new(proto.round)) + } +} + +impl From for malachite_proto::Round { + fn from(round: Round) -> malachite_proto::Round { + malachite_proto::Round { + round: round.as_i64(), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/code/common/src/validator_set.rs b/code/common/src/validator_set.rs index ccc4dd1f6..1b9758948 100644 --- a/code/common/src/validator_set.rs +++ b/code/common/src/validator_set.rs @@ -1,5 +1,7 @@ use core::fmt::{Debug, Display}; +use malachite_proto::Protobuf; + use crate::{Context, PublicKey}; /// Voting power held by a validator. @@ -11,6 +13,7 @@ pub type VotingPower = u64; pub trait Address where Self: Clone + Debug + Display + Eq + Ord, + Self: Protobuf, { } diff --git a/code/common/src/value.rs b/code/common/src/value.rs index a16ac246e..60b47c69b 100644 --- a/code/common/src/value.rs +++ b/code/common/src/value.rs @@ -1,5 +1,7 @@ use core::fmt::Debug; +use malachite_proto::Protobuf; + /// Represents either `Nil` or a value of type `Value`. /// /// This type is isomorphic to `Option` but is more explicit about its intent. @@ -53,10 +55,39 @@ impl NilOrVal { } } +// impl TryFrom for NilOrVal +// where +// Value: From, // FIXME +// { +// type Error = String; +// +// fn try_from(proto: malachite_proto::Value) -> Result { +// match proto.value { +// Some(value) => Ok(NilOrVal::Val(Value::from(value))), // FIXME +// None => Ok(NilOrVal::Nil), +// } +// } +// } +// +// impl TryFrom for NilOrVal +// where +// Value: TryFrom>, // FIXME +// { +// type Error = String; +// +// fn try_from(proto: malachite_proto::ValueId) -> Result { +// match proto.value { +// Some(value) => Ok(NilOrVal::Val(Value::from(value))), // FIXME +// None => Ok(NilOrVal::Nil), +// } +// } +// } + /// Defines the requirements for the type of value to decide on. pub trait Value where Self: Clone + Debug + PartialEq + Eq + PartialOrd + Ord, + Self: Protobuf, { /// The type of the ID of the value. /// Typically a representation of the value with a lower memory footprint. diff --git a/code/common/src/vote.rs b/code/common/src/vote.rs index 82f9c77a9..6f1e43b3e 100644 --- a/code/common/src/vote.rs +++ b/code/common/src/vote.rs @@ -1,5 +1,8 @@ +use core::convert::Infallible; use core::fmt::Debug; +use malachite_proto::Protobuf; + use crate::{Context, NilOrVal, Round, Value}; /// A type of vote. @@ -12,6 +15,26 @@ pub enum VoteType { Precommit, } +impl TryFrom for VoteType { + type Error = Infallible; + + fn try_from(vote_type: malachite_proto::VoteType) -> Result { + match vote_type { + malachite_proto::VoteType::Prevote => Ok(VoteType::Prevote), + malachite_proto::VoteType::Precommit => Ok(VoteType::Precommit), + } + } +} + +impl From for malachite_proto::VoteType { + fn from(vote_type: VoteType) -> malachite_proto::VoteType { + match vote_type { + VoteType::Prevote => malachite_proto::VoteType::Prevote, + VoteType::Precommit => malachite_proto::VoteType::Precommit, + } + } +} + /// Defines the requirements for a vote. /// /// Votes are signed messages from validators for a particular value which @@ -19,6 +42,7 @@ pub enum VoteType { pub trait Vote where Self: Clone + Debug + Eq + Send + Sync + 'static, + Self: Protobuf, Ctx: Context, { /// The height for which the vote is for. diff --git a/code/node/Cargo.toml b/code/node/Cargo.toml index 51dbe53b8..a6ccbd83b 100644 --- a/code/node/Cargo.toml +++ b/code/node/Cargo.toml @@ -9,15 +9,18 @@ license.workspace = true publish.workspace = true [dependencies] -malachite-common = { version = "0.1.0", path = "../common" } -malachite-driver = { version = "0.1.0", path = "../driver" } -malachite-round = { version = "0.1.0", path = "../round" } -malachite-vote = { version = "0.1.0", path = "../vote" } +malachite-common.workspace = true +malachite-driver.workspace = true +malachite-round.workspace = true +malachite-vote.workspace = true +malachite-proto.workspace = true derive-where = { workspace = true } -futures = { workspace = true } -tokio = { workspace = true, features = ["full"] } +futures = { workspace = true } +tokio = { workspace = true, features = ["full"] } tokio-stream = { workspace = true } +prost = { workspace = true } +prost-types = { workspace = true } [dev-dependencies] -malachite-test = { version = "0.1.0", path = "../test" } +malachite-test.workspace = true diff --git a/code/node/src/network/broadcast.rs b/code/node/src/network/broadcast.rs index f09d90e38..c551b4541 100644 --- a/code/node/src/network/broadcast.rs +++ b/code/node/src/network/broadcast.rs @@ -3,11 +3,12 @@ use std::fmt::Debug; use std::net::SocketAddr; use futures::channel::oneshot; -use malachite_common::Context; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::{broadcast, mpsc}; +use malachite_common::Context; + use super::{Msg, Network, PeerId}; pub enum PeerEvent { @@ -115,7 +116,7 @@ async fn connect_to_peer( println!("[{id}] Sending message to {peer_info}: {msg:?}"); - let bytes = msg.as_bytes(); + let bytes = msg.to_network_bytes().unwrap(); stream.write_u32(bytes.len() as u32).await.unwrap(); stream.write_all(&bytes).await.unwrap(); stream.flush().await.unwrap(); @@ -151,7 +152,7 @@ async fn listen( let len = socket.read_u32().await.unwrap(); let mut buf = vec![0; len as usize]; socket.read_exact(&mut buf).await.unwrap(); - let msg: Msg = Msg::from_bytes(&buf); + let msg: Msg = Msg::from_network_bytes(&buf).unwrap(); println!( "[{id}] Received message from {peer_id} ({addr}): {msg:?}", diff --git a/code/node/src/network/msg.rs b/code/node/src/network/msg.rs index f14e56024..841a8668a 100644 --- a/code/node/src/network/msg.rs +++ b/code/node/src/network/msg.rs @@ -1,6 +1,11 @@ use derive_where::derive_where; +use prost::Message; +use prost_types::Any; + use malachite_common::Context; +use malachite_proto::Error as ProtoError; +use malachite_proto::Protobuf; #[derive_where(Clone, Debug, PartialEq, Eq)] pub enum Msg { @@ -8,66 +13,85 @@ pub enum Msg { Proposal(Ctx::Proposal), #[cfg(test)] - Dummy(u32), + Dummy(u64), } impl Msg { - pub fn as_bytes(&self) -> Vec { - match self { - Msg::Vote(_vote) => todo!(), - Msg::Proposal(_proposal) => todo!(), + pub fn from_network_bytes(bytes: &[u8]) -> Result { + Protobuf::::from_bytes(bytes) + } + + pub fn to_network_bytes(&self) -> Result, ProtoError> { + Protobuf::::to_bytes(self) + } +} + +impl Protobuf for Msg { + fn from_bytes(bytes: &[u8]) -> Result + where + Self: Sized, + { + use prost::Name; + let any = Any::decode(bytes)?; + + if any.type_url == malachite_proto::Vote::type_url() { + let vote = Ctx::Vote::from_bytes(&any.value)?; + Ok(Msg::Vote(vote)) + } else if any.type_url == malachite_proto::Proposal::type_url() { + let proposal = Ctx::Proposal::from_bytes(&any.value)?; + Ok(Msg::Proposal(proposal)) + } else if any.type_url == "malachite.proto.Dummy" { #[cfg(test)] - Msg::Dummy(n) => [&[0x42], n.to_be_bytes().as_slice()].concat(), + { + let value = u64::from_be_bytes(any.value.try_into().unwrap()); + Ok(Msg::Dummy(value)) + } + + #[cfg(not(test))] + { + Err(malachite_proto::Error::Other( + "unknown message type: malachite.proto.Dummy".to_string(), + )) + } + } else { + Err(malachite_proto::Error::Other(format!( + "unknown message type: {}", + any.type_url + ))) } } - pub fn from_bytes(bytes: &[u8]) -> Self { - match bytes { + fn into_bytes(self) -> Result, malachite_proto::Error> { + use prost::Name; + + match self { + Msg::Vote(vote) => { + let any = Any { + type_url: malachite_proto::Vote::type_url(), + value: vote.into_bytes()?, + }; + + Ok(any.encode_to_vec()) + } + Msg::Proposal(proposal) => { + let any = Any { + type_url: malachite_proto::Proposal::type_url(), + value: proposal.into_bytes()?, + }; + + Ok(any.encode_to_vec()) + } + #[cfg(test)] - [0x42, a, b, c, d] => Msg::Dummy(u32::from_be_bytes([*a, *b, *c, *d])), + Msg::Dummy(value) => { + let any = Any { + type_url: "malachite.proto.Dummy".to_string(), + value: value.to_be_bytes().to_vec(), + }; - _ => todo!(), + Ok(any.encode_to_vec()) + } } } } -// -// impl Clone for Msg { -// fn clone(&self) -> Self { -// match self { -// Msg::Vote(vote) => Msg::Vote(vote.clone()), -// Msg::Proposal(proposal) => Msg::Proposal(proposal.clone()), -// -// #[cfg(test)] -// Msg::Dummy(n) => Msg::Dummy(*n), -// } -// } -// } -// -// impl fmt::Debug for Msg { -// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { -// match self { -// Msg::Vote(vote) => write!(f, "Vote({vote:?})"), -// Msg::Proposal(proposal) => write!(f, "Proposal({proposal:?})"), -// -// #[cfg(test)] -// Msg::Dummy(n) => write!(f, "Dummy({n:?})"), -// } -// } -// } -// -// impl PartialEq for Msg { -// fn eq(&self, other: &Self) -> bool { -// match (self, other) { -// (Msg::Vote(vote), Msg::Vote(other_vote)) => vote == other_vote, -// (Msg::Proposal(proposal), Msg::Proposal(other_proposal)) => proposal == other_proposal, -// -// #[cfg(test)] -// (Msg::Dummy(n1), Msg::Dummy(n2)) => n1 == n2, -// -// _ => false, -// } -// } -// } -// -// impl Eq for Msg {} diff --git a/code/proto/Cargo.toml b/code/proto/Cargo.toml new file mode 100644 index 000000000..7d09ef8d1 --- /dev/null +++ b/code/proto/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "malachite-proto" +version.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +prost.workspace = true +prost-types.workspace = true +thiserror.workspace = true + +[build-dependencies] +prost-build.workspace = true diff --git a/code/proto/build.rs b/code/proto/build.rs new file mode 100644 index 000000000..c02bf2f34 --- /dev/null +++ b/code/proto/build.rs @@ -0,0 +1,9 @@ +use std::io::Result; + +fn main() -> Result<()> { + let mut config = prost_build::Config::new(); + config.enable_type_names(); + config.compile_protos(&["src/malachite.proto"], &["src/"])?; + + Ok(()) +} diff --git a/code/proto/src/lib.rs b/code/proto/src/lib.rs new file mode 100644 index 000000000..e29435191 --- /dev/null +++ b/code/proto/src/lib.rs @@ -0,0 +1,55 @@ +use std::fmt::Display; + +use thiserror::Error; + +use prost::{DecodeError, EncodeError, Message}; + +include!(concat!(env!("OUT_DIR"), "/malachite.rs")); + +#[derive(Debug, Error)] +pub enum Error { + #[error("Failed to decode Protobuf message")] + Decode(#[from] DecodeError), + + #[error("Failed to encode Protobuf message")] + Encode(#[from] EncodeError), + + #[error("{0}")] + Other(String), +} + +pub trait Protobuf { + fn from_bytes(bytes: &[u8]) -> Result + where + Self: Sized; + fn into_bytes(self) -> Result, Error>; + + fn to_bytes(&self) -> Result, Error> + where + Self: Clone, + { + self.clone().into_bytes() + } +} + +impl Protobuf for T +where + T: TryFrom, + T::Error: Display, + Proto: Message + From + Default, +{ + fn from_bytes(bytes: &[u8]) -> Result + where + Self: Sized, + { + let proto = Proto::decode(bytes)?; + Self::try_from(proto).map_err(|e| Error::Other(e.to_string())) + } + + fn into_bytes(self) -> Result, Error> { + let proto = Proto::from(self); + let mut bytes = Vec::with_capacity(proto.encoded_len()); + proto.encode(&mut bytes)?; + Ok(bytes) + } +} diff --git a/code/proto/src/malachite.proto b/code/proto/src/malachite.proto new file mode 100644 index 000000000..7d145e58b --- /dev/null +++ b/code/proto/src/malachite.proto @@ -0,0 +1,43 @@ +syntax = "proto3"; + +package malachite; + +message Height { + uint64 value = 1; +} + +message Address { + bytes value = 1; +} + +message Value { + optional bytes value = 2; +} + +message ValueId { + optional bytes value = 1; +} + +message Round { + int64 round = 1; +} + +message Vote { + VoteType vote_type = 1; + Height height = 2; + Round round = 3; + ValueId value = 4; + Address validator_address = 5; +} + +message Proposal { + Height height = 1; + Round round = 2; + Value value = 3; + Round pol_round = 4; +} + +enum VoteType { + PREVOTE = 0; + PRECOMMIT = 1; +} diff --git a/code/test/Cargo.toml b/code/test/Cargo.toml index e2463c636..77d01986f 100644 --- a/code/test/Cargo.toml +++ b/code/test/Cargo.toml @@ -9,13 +9,13 @@ repository.workspace = true license.workspace = true [dependencies] -malachite-common = { version = "0.1.0", path = "../common" } -malachite-driver = { version = "0.1.0", path = "../driver" } -malachite-round = { version = "0.1.0", path = "../round" } -malachite-vote = { version = "0.1.0", path = "../vote" } - -futures = { workspace = true, features = ["executor"] } +malachite-common.workspace = true +malachite-driver.workspace = true +malachite-round.workspace = true +malachite-vote.workspace = true +malachite-proto.workspace = true +futures = { workspace = true, features = ["executor"] } ed25519-consensus.workspace = true signature.workspace = true rand.workspace = true diff --git a/code/test/src/height.rs b/code/test/src/height.rs index 3f4a32c3c..e2737ca76 100644 --- a/code/test/src/height.rs +++ b/code/test/src/height.rs @@ -1,3 +1,5 @@ +use std::convert::Infallible; + /// A blockchain height #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] pub struct Height(u64); @@ -13,3 +15,17 @@ impl Height { } impl malachite_common::Height for Height {} + +impl TryFrom for Height { + type Error = Infallible; + + fn try_from(height: malachite_proto::Height) -> Result { + Ok(Self(height.value)) + } +} + +impl From for malachite_proto::Height { + fn from(height: Height) -> malachite_proto::Height { + malachite_proto::Height { value: height.0 } + } +} diff --git a/code/test/src/proposal.rs b/code/test/src/proposal.rs index 407b4be02..012084ee8 100644 --- a/code/test/src/proposal.rs +++ b/code/test/src/proposal.rs @@ -39,3 +39,27 @@ impl malachite_common::Proposal for Proposal { self.pol_round } } + +impl TryFrom for Proposal { + type Error = String; + + fn try_from(proposal: malachite_proto::Proposal) -> Result { + Ok(Self { + height: proposal.height.unwrap().try_into().unwrap(), // infallible + round: proposal.round.unwrap().try_into().unwrap(), // infallible + value: proposal.value.unwrap().try_into().unwrap(), // FIXME + pol_round: proposal.pol_round.unwrap().try_into().unwrap(), // infallible + }) + } +} + +impl From for malachite_proto::Proposal { + fn from(proposal: Proposal) -> malachite_proto::Proposal { + malachite_proto::Proposal { + height: Some(proposal.height.into()), + round: Some(proposal.round.into()), + value: Some(proposal.value.into()), + pol_round: Some(proposal.pol_round.into()), + } + } +} diff --git a/code/test/src/validator_set.rs b/code/test/src/validator_set.rs index 91b903496..fd08eea27 100644 --- a/code/test/src/validator_set.rs +++ b/code/test/src/validator_set.rs @@ -36,6 +36,32 @@ impl fmt::Display for Address { impl malachite_common::Address for Address {} +impl TryFrom for Address { + type Error = String; + + fn try_from(proto: malachite_proto::Address) -> Result { + if proto.value.len() != Self::LENGTH { + return Err(format!( + "Invalid address length: expected {}, got {}", + Self::LENGTH, + proto.value.len() + )); + } + + let mut address = [0; Self::LENGTH]; + address.copy_from_slice(&proto.value); + Ok(Self(address)) + } +} + +impl From
for malachite_proto::Address { + fn from(address: Address) -> Self { + Self { + value: address.0.to_vec(), + } + } +} + /// A validator is a public key and voting power #[derive(Clone, Debug, PartialEq, Eq)] pub struct Validator { diff --git a/code/test/src/value.rs b/code/test/src/value.rs index bed49dbf6..185c9952a 100644 --- a/code/test/src/value.rs +++ b/code/test/src/value.rs @@ -17,6 +17,28 @@ impl From for ValueId { } } +impl TryFrom for ValueId { + type Error = String; + + fn try_from(proto: malachite_proto::ValueId) -> Result { + match proto.value { + Some(bytes) => { + let bytes = <[u8; 8]>::try_from(bytes).unwrap(); // FIXME + Ok(ValueId::new(u64::from_be_bytes(bytes))) + } + None => Err("ValueId not present".to_string()), + } + } +} + +impl From for malachite_proto::ValueId { + fn from(value: ValueId) -> malachite_proto::ValueId { + malachite_proto::ValueId { + value: Some(value.0.to_be_bytes().to_vec()), + } + } +} + /// The value to decide on #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct Value(u64); @@ -42,3 +64,26 @@ impl malachite_common::Value for Value { self.id() } } + +impl TryFrom for Value { + type Error = String; + + fn try_from(proto: malachite_proto::Value) -> Result { + match proto.value { + Some(bytes) => { + let bytes = <[u8; 8]>::try_from(bytes).unwrap(); // FIXME + let value = u64::from_be_bytes(bytes); + Ok(Value::new(value)) + } + None => Err("Value not present".to_string()), + } + } +} + +impl From for malachite_proto::Value { + fn from(value: Value) -> malachite_proto::Value { + malachite_proto::Value { + value: Some(value.0.to_be_bytes().to_vec()), + } + } +} diff --git a/code/test/src/vote.rs b/code/test/src/vote.rs index 31b9a250d..0acebd12e 100644 --- a/code/test/src/vote.rs +++ b/code/test/src/vote.rs @@ -98,3 +98,38 @@ impl malachite_common::Vote for Vote { &self.validator_address } } + +impl TryFrom for Vote { + type Error = String; + + fn try_from(vote: malachite_proto::Vote) -> Result { + Ok(Self { + typ: malachite_proto::VoteType::try_from(vote.vote_type) + .unwrap() + .try_into() + .unwrap(), // infallible + height: vote.height.unwrap().try_into().unwrap(), // infallible + round: vote.round.unwrap().try_into().unwrap(), // infallible + value: match vote.value { + Some(value) => NilOrVal::Val(value.try_into().unwrap()), // FIXME + None => NilOrVal::Nil, + }, + validator_address: vote.validator_address.unwrap().try_into().unwrap(), // FIXME + }) + } +} + +impl From for malachite_proto::Vote { + fn from(vote: Vote) -> malachite_proto::Vote { + malachite_proto::Vote { + vote_type: i32::from(malachite_proto::VoteType::from(vote.typ)), + height: Some(vote.height.into()), + round: Some(vote.round.into()), + value: match vote.value { + NilOrVal::Nil => None, + NilOrVal::Val(v) => Some(v.into()), + }, + validator_address: Some(vote.validator_address.into()), + } + } +} From 198709dd754f36bf97aebb6ba168cbdef4bbef9d Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 20 Feb 2024 09:10:47 +0100 Subject: [PATCH 07/50] Cleanup --- code/node/src/network/broadcast.rs | 110 +++++++++++++++++++++-------- 1 file changed, 81 insertions(+), 29 deletions(-) diff --git a/code/node/src/network/broadcast.rs b/code/node/src/network/broadcast.rs index c551b4541..81f70c622 100644 --- a/code/node/src/network/broadcast.rs +++ b/code/node/src/network/broadcast.rs @@ -105,7 +105,10 @@ async fn connect_to_peer( let mut per_peer_rx = per_peer_tx.subscribe(); - send_peer_id(&mut stream, id.clone()).await; + Frame::::PeerId(id.clone()) + .write(&mut stream) + .await + .unwrap(); tokio::spawn(async move { loop { @@ -115,11 +118,7 @@ async fn connect_to_peer( } println!("[{id}] Sending message to {peer_info}: {msg:?}"); - - let bytes = msg.to_network_bytes().unwrap(); - stream.write_u32(bytes.len() as u32).await.unwrap(); - stream.write_all(&bytes).await.unwrap(); - stream.flush().await.unwrap(); + Frame::Msg(msg).write(&mut stream).await.unwrap(); } }); } @@ -143,40 +142,87 @@ async fn listen( peer = socket.peer_addr().unwrap() ); - let peer_id = read_peer_id(&mut socket).await; + let Frame::PeerId(peer_id) = Frame::::read(&mut socket).await.unwrap() else { + eprintln!("[{id}] Peer did not send its ID"); + continue; + }; let id = id.clone(); let tx_received = tx_received.clone(); tokio::spawn(async move { - let len = socket.read_u32().await.unwrap(); - let mut buf = vec![0; len as usize]; - socket.read_exact(&mut buf).await.unwrap(); - let msg: Msg = Msg::from_network_bytes(&buf).unwrap(); - - println!( - "[{id}] Received message from {peer_id} ({addr}): {msg:?}", - addr = socket.peer_addr().unwrap(), - ); - - tx_received.send((peer_id.clone(), msg)).await.unwrap(); // FIXME + loop { + let Frame::Msg(msg) = Frame::::read(&mut socket).await.unwrap() else { + eprintln!("[{id}] Peer did not send a message"); + return; + }; + + println!( + "[{id}] Received message from {peer_id} ({addr}): {msg:?}", + addr = socket.peer_addr().unwrap(), + ); + + tx_received.send((peer_id.clone(), msg)).await.unwrap(); // FIXME + } }); } } -async fn send_peer_id(socket: &mut TcpStream, id: PeerId) { - let bytes = id.0.as_bytes(); - socket.write_u32(bytes.len() as u32).await.unwrap(); - socket.write_all(bytes).await.unwrap(); - socket.flush().await.unwrap(); +pub enum Frame { + PeerId(PeerId), + Msg(Msg), } -async fn read_peer_id(socket: &mut TcpStream) -> PeerId { - let len = socket.read_u32().await.unwrap(); - let mut buf = vec![0; len as usize]; - socket.read_exact(&mut buf).await.unwrap(); - let id = String::from_utf8(buf).unwrap(); - PeerId(id) +impl Frame { + /// Write a frame to the given writer, prefixing it with its discriminant. + pub async fn write( + &self, + writer: &mut W, + ) -> Result<(), std::io::Error> { + match self { + Frame::PeerId(id) => { + writer.write_u8(0x40).await?; + let bytes = id.0.as_bytes(); + writer.write_u32(bytes.len() as u32).await?; + writer.write_all(bytes).await?; + writer.flush().await?; + } + Frame::Msg(msg) => { + writer.write_u8(0x41).await?; + let bytes = msg + .to_network_bytes() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + writer.write_u32(bytes.len() as u32).await?; + writer.write_all(&bytes).await?; + writer.flush().await?; + } + } + + Ok(()) + } + + pub async fn read(reader: &mut R) -> Result { + let discriminant = reader.read_u8().await?; + + match discriminant { + 0x40 => { + let len = reader.read_u32().await?; + let mut buf = vec![0; len as usize]; + reader.read_exact(&mut buf).await?; + Ok(Frame::PeerId(PeerId(String::from_utf8(buf).unwrap()))) + } + 0x41 => { + let len = reader.read_u32().await?; + let mut buf = vec![0; len as usize]; + reader.read_exact(&mut buf).await?; + Ok(Frame::Msg(Msg::from_network_bytes(&buf).unwrap())) + } + _ => Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("Invalid frame discriminant: {discriminant}"), + )), + } + } } pub struct Handle { @@ -274,6 +320,7 @@ mod tests { handle3.connect_to_peer(peer2_info.clone()).await; handle1.broadcast(Msg::Dummy(1)).await; + handle1.broadcast(Msg::Dummy(2)).await; let deadline = Duration::from_millis(100); @@ -281,5 +328,10 @@ mod tests { dbg!(&msg2); let msg3 = timeout(deadline, handle3.recv()).await.unwrap(); dbg!(&msg3); + + let msg4 = timeout(deadline, handle2.recv()).await.unwrap(); + dbg!(&msg4); + let msg5 = timeout(deadline, handle3.recv()).await.unwrap(); + dbg!(&msg5); } } From 86d9390e55fda8e7468644f6fe43c20ecb45f895 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 20 Feb 2024 16:05:32 +0100 Subject: [PATCH 08/50] Make network messages agnostic of `Context` by using proto definitions instead --- code/node/src/network.rs | 14 +++--- code/node/src/network/broadcast.rs | 70 +++++++++++++----------------- code/node/src/network/msg.rs | 55 ++++++++++------------- code/proto/src/malachite.proto | 5 +++ 4 files changed, 66 insertions(+), 78 deletions(-) diff --git a/code/node/src/network.rs b/code/node/src/network.rs index 826eedb50..4d9e1117b 100644 --- a/code/node/src/network.rs +++ b/code/node/src/network.rs @@ -1,10 +1,10 @@ use core::fmt; -use malachite_common::Context; - pub mod broadcast; mod msg; +use malachite_proto::{Proposal, SignedVote}; + pub use self::msg::Msg; #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -17,14 +17,14 @@ impl fmt::Display for PeerId { } #[allow(async_fn_in_trait)] -pub trait Network { - async fn recv(&mut self) -> Option<(PeerId, Msg)>; - async fn broadcast(&mut self, msg: Msg); +pub trait Network { + async fn recv(&mut self) -> Option<(PeerId, Msg)>; + async fn broadcast(&mut self, msg: Msg); - async fn broadcast_vote(&mut self, vote: Ctx::Vote) { + async fn broadcast_vote(&mut self, vote: SignedVote) { self.broadcast(Msg::Vote(vote)).await } - async fn broadcast_proposal(&mut self, proposal: Ctx::Proposal) { + async fn broadcast_proposal(&mut self, proposal: Proposal) { self.broadcast(Msg::Proposal(proposal)).await } } diff --git a/code/node/src/network/broadcast.rs b/code/node/src/network/broadcast.rs index 81f70c622..2131b6486 100644 --- a/code/node/src/network/broadcast.rs +++ b/code/node/src/network/broadcast.rs @@ -7,16 +7,14 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::{broadcast, mpsc}; -use malachite_common::Context; - use super::{Msg, Network, PeerId}; -pub enum PeerEvent { +pub enum PeerEvent { ConnectToPeer(PeerInfo, oneshot::Sender<()>), - Broadcast(Msg, oneshot::Sender<()>), + Broadcast(Msg, oneshot::Sender<()>), } -impl Debug for PeerEvent { +impl Debug for PeerEvent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { PeerEvent::ConnectToPeer(peer_info, _) => { @@ -41,25 +39,23 @@ impl fmt::Display for PeerInfo { } } -pub struct Peer { +pub struct Peer { id: PeerId, addr: SocketAddr, - _marker: std::marker::PhantomData, } -impl Peer { +impl Peer { pub fn new(info: PeerInfo) -> Self { Self { id: info.id, addr: info.addr, - _marker: std::marker::PhantomData, } } - pub async fn run(self) -> Handle { - let (tx_peer_event, mut rx_peer_event) = mpsc::channel::>(16); - let (tx_msg, rx_msg) = mpsc::channel::<(PeerId, Msg)>(16); - let (tx_broadcast_to_peers, _) = broadcast::channel::<(PeerId, Msg)>(16); + pub async fn run(self) -> Handle { + let (tx_peer_event, mut rx_peer_event) = mpsc::channel::(16); + let (tx_msg, rx_msg) = mpsc::channel::<(PeerId, Msg)>(16); + let (tx_broadcast_to_peers, _) = broadcast::channel::<(PeerId, Msg)>(16); let (tx_spawned, rx_spawned) = oneshot::channel(); tokio::spawn(listen(self.id.clone(), self.addr, tx_spawned, tx_msg)); @@ -92,11 +88,11 @@ impl Peer { } } -async fn connect_to_peer( +async fn connect_to_peer( id: PeerId, peer_info: PeerInfo, done: oneshot::Sender<()>, - per_peer_tx: &broadcast::Sender<(PeerId, Msg)>, + per_peer_tx: &broadcast::Sender<(PeerId, Msg)>, ) { println!("[{id}] Connecting to {peer_info}..."); @@ -105,10 +101,7 @@ async fn connect_to_peer( let mut per_peer_rx = per_peer_tx.subscribe(); - Frame::::PeerId(id.clone()) - .write(&mut stream) - .await - .unwrap(); + Frame::PeerId(id.clone()).write(&mut stream).await.unwrap(); tokio::spawn(async move { loop { @@ -123,11 +116,11 @@ async fn connect_to_peer( }); } -async fn listen( +async fn listen( id: PeerId, addr: SocketAddr, tx_spawned: oneshot::Sender<()>, - tx_received: mpsc::Sender<(PeerId, Msg)>, + tx_received: mpsc::Sender<(PeerId, Msg)>, ) -> ! { let listener = TcpListener::bind(addr).await.unwrap(); println!("[{id}] Listening on {addr}..."); @@ -142,7 +135,7 @@ async fn listen( peer = socket.peer_addr().unwrap() ); - let Frame::PeerId(peer_id) = Frame::::read(&mut socket).await.unwrap() else { + let Frame::PeerId(peer_id) = Frame::read(&mut socket).await.unwrap() else { eprintln!("[{id}] Peer did not send its ID"); continue; }; @@ -152,7 +145,7 @@ async fn listen( tokio::spawn(async move { loop { - let Frame::Msg(msg) = Frame::::read(&mut socket).await.unwrap() else { + let Frame::Msg(msg) = Frame::read(&mut socket).await.unwrap() else { eprintln!("[{id}] Peer did not send a message"); return; }; @@ -168,12 +161,12 @@ async fn listen( } } -pub enum Frame { +pub enum Frame { PeerId(PeerId), - Msg(Msg), + Msg(Msg), } -impl Frame { +impl Frame { /// Write a frame to the given writer, prefixing it with its discriminant. pub async fn write( &self, @@ -225,22 +218,22 @@ impl Frame { } } -pub struct Handle { +pub struct Handle { peer_id: PeerId, - rx_msg: mpsc::Receiver<(PeerId, Msg)>, - tx_peer_event: mpsc::Sender>, + rx_msg: mpsc::Receiver<(PeerId, Msg)>, + tx_peer_event: mpsc::Sender, } -impl Handle { +impl Handle { pub fn peer_id(&self) -> &PeerId { &self.peer_id } - pub async fn recv(&mut self) -> Option<(PeerId, Msg)> { + pub async fn recv(&mut self) -> Option<(PeerId, Msg)> { self.rx_msg.recv().await } - pub async fn broadcast(&self, msg: Msg) { + pub async fn broadcast(&self, msg: Msg) { let (tx_done, rx_done) = oneshot::channel(); self.tx_peer_event @@ -263,12 +256,12 @@ impl Handle { } } -impl Network for Handle { - async fn recv(&mut self) -> Option<(PeerId, Msg)> { +impl Network for Handle { + async fn recv(&mut self) -> Option<(PeerId, Msg)> { Handle::recv(self).await } - async fn broadcast(&mut self, msg: Msg) { + async fn broadcast(&mut self, msg: Msg) { Handle::broadcast(self, msg).await; } } @@ -277,7 +270,6 @@ impl Network for Handle { mod tests { use std::time::Duration; - use malachite_test::TestContext; use tokio::time::timeout; use super::*; @@ -302,9 +294,9 @@ mod tests { addr: "127.0.0.1:12003".parse().unwrap(), }; - let peer1: Peer = Peer::new(peer1_info.clone()); - let peer2: Peer = Peer::new(peer2_info.clone()); - let peer3: Peer = Peer::new(peer3_info.clone()); + let peer1: Peer = Peer::new(peer1_info.clone()); + let peer2: Peer = Peer::new(peer2_info.clone()); + let peer3: Peer = Peer::new(peer3_info.clone()); let handle1 = peer1.run().await; let mut handle2 = peer2.run().await; diff --git a/code/node/src/network/msg.rs b/code/node/src/network/msg.rs index 841a8668a..1c38d5177 100644 --- a/code/node/src/network/msg.rs +++ b/code/node/src/network/msg.rs @@ -1,22 +1,20 @@ -use derive_where::derive_where; - use prost::Message; use prost_types::Any; -use malachite_common::Context; use malachite_proto::Error as ProtoError; use malachite_proto::Protobuf; +use malachite_proto::{Proposal, SignedVote}; -#[derive_where(Clone, Debug, PartialEq, Eq)] -pub enum Msg { - Vote(Ctx::Vote), - Proposal(Ctx::Proposal), +#[derive(Clone, Debug, PartialEq)] +pub enum Msg { + Vote(SignedVote), + Proposal(Proposal), #[cfg(test)] Dummy(u64), } -impl Msg { +impl Msg { pub fn from_network_bytes(bytes: &[u8]) -> Result { Protobuf::::from_bytes(bytes) } @@ -24,10 +22,12 @@ impl Msg { pub fn to_network_bytes(&self) -> Result, ProtoError> { Protobuf::::to_bytes(self) } + + const DUMMY_TYPE_URL: &'static str = "malachite.Dummy"; } -impl Protobuf for Msg { - fn from_bytes(bytes: &[u8]) -> Result +impl Protobuf for Msg { + fn from_bytes(bytes: &[u8]) -> Result where Self: Sized, { @@ -35,13 +35,13 @@ impl Protobuf for Msg { let any = Any::decode(bytes)?; - if any.type_url == malachite_proto::Vote::type_url() { - let vote = Ctx::Vote::from_bytes(&any.value)?; + if any.type_url == SignedVote::type_url() { + let vote = SignedVote::decode(any.value.as_slice())?; Ok(Msg::Vote(vote)) } else if any.type_url == malachite_proto::Proposal::type_url() { - let proposal = Ctx::Proposal::from_bytes(&any.value)?; + let proposal = Proposal::decode(any.value.as_slice())?; Ok(Msg::Proposal(proposal)) - } else if any.type_url == "malachite.proto.Dummy" { + } else if any.type_url == Msg::DUMMY_TYPE_URL { #[cfg(test)] { let value = u64::from_be_bytes(any.value.try_into().unwrap()); @@ -50,43 +50,34 @@ impl Protobuf for Msg { #[cfg(not(test))] { - Err(malachite_proto::Error::Other( - "unknown message type: malachite.proto.Dummy".to_string(), - )) + Err(malachite_proto::Error::Other(format!( + "unknown message type: {}", + Msg::DUMMY_TYPE_URL + ))) } } else { - Err(malachite_proto::Error::Other(format!( + Err(ProtoError::Other(format!( "unknown message type: {}", any.type_url ))) } } - fn into_bytes(self) -> Result, malachite_proto::Error> { - use prost::Name; - + fn into_bytes(self) -> Result, ProtoError> { match self { Msg::Vote(vote) => { - let any = Any { - type_url: malachite_proto::Vote::type_url(), - value: vote.into_bytes()?, - }; - + let any = Any::from_msg(&vote)?; Ok(any.encode_to_vec()) } Msg::Proposal(proposal) => { - let any = Any { - type_url: malachite_proto::Proposal::type_url(), - value: proposal.into_bytes()?, - }; - + let any = Any::from_msg(&proposal)?; Ok(any.encode_to_vec()) } #[cfg(test)] Msg::Dummy(value) => { let any = Any { - type_url: "malachite.proto.Dummy".to_string(), + type_url: Msg::DUMMY_TYPE_URL.to_string(), value: value.to_be_bytes().to_vec(), }; diff --git a/code/proto/src/malachite.proto b/code/proto/src/malachite.proto index 7d145e58b..84ec514e7 100644 --- a/code/proto/src/malachite.proto +++ b/code/proto/src/malachite.proto @@ -30,6 +30,11 @@ message Vote { Address validator_address = 5; } +message SignedVote { + Vote vote = 1; + bytes signature = 2; +} + message Proposal { Height height = 1; Round round = 2; From d6c85aeaa129b9aad57f4bb85c9c32287d6b9e77 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 20 Feb 2024 16:26:09 +0100 Subject: [PATCH 09/50] Refactor `Protobuf` trait --- code/common/src/height.rs | 2 +- code/common/src/proposal.rs | 2 +- code/common/src/validator_set.rs | 2 +- code/common/src/value.rs | 2 +- code/common/src/vote.rs | 2 +- code/node/src/network/msg.rs | 70 ++++++++++++++++++-------------- code/proto/src/lib.rs | 40 ++++++++---------- code/test/src/address.rs | 65 +++++++++++++++++++++++++++++ code/test/src/context.rs | 3 +- code/test/src/height.rs | 4 ++ code/test/src/lib.rs | 2 + code/test/src/proposal.rs | 4 ++ code/test/src/validator_set.rs | 63 +--------------------------- code/test/src/value.rs | 4 ++ code/test/src/vote.rs | 4 ++ 15 files changed, 150 insertions(+), 119 deletions(-) create mode 100644 code/test/src/address.rs diff --git a/code/common/src/height.rs b/code/common/src/height.rs index 2293bf2c2..fc34b7ade 100644 --- a/code/common/src/height.rs +++ b/code/common/src/height.rs @@ -10,6 +10,6 @@ use malachite_proto::Protobuf; pub trait Height where Self: Default + Copy + Clone + Debug + PartialEq + Eq + PartialOrd + Ord, - Self: Protobuf, + Self: Protobuf, { } diff --git a/code/common/src/proposal.rs b/code/common/src/proposal.rs index c8c2a21db..e273f7924 100644 --- a/code/common/src/proposal.rs +++ b/code/common/src/proposal.rs @@ -8,7 +8,7 @@ use crate::{Context, Round}; pub trait Proposal where Self: Clone + Debug + Eq + Send + Sync + 'static, - Self: Protobuf, + Self: Protobuf, Ctx: Context, { /// The height for which the proposal is for. diff --git a/code/common/src/validator_set.rs b/code/common/src/validator_set.rs index 1b9758948..309b6be9a 100644 --- a/code/common/src/validator_set.rs +++ b/code/common/src/validator_set.rs @@ -13,7 +13,7 @@ pub type VotingPower = u64; pub trait Address where Self: Clone + Debug + Display + Eq + Ord, - Self: Protobuf, + Self: Protobuf, { } diff --git a/code/common/src/value.rs b/code/common/src/value.rs index 60b47c69b..a17765ce4 100644 --- a/code/common/src/value.rs +++ b/code/common/src/value.rs @@ -87,7 +87,7 @@ impl NilOrVal { pub trait Value where Self: Clone + Debug + PartialEq + Eq + PartialOrd + Ord, - Self: Protobuf, + Self: Protobuf, { /// The type of the ID of the value. /// Typically a representation of the value with a lower memory footprint. diff --git a/code/common/src/vote.rs b/code/common/src/vote.rs index 6f1e43b3e..a27d39e78 100644 --- a/code/common/src/vote.rs +++ b/code/common/src/vote.rs @@ -42,7 +42,7 @@ impl From for malachite_proto::VoteType { pub trait Vote where Self: Clone + Debug + Eq + Send + Sync + 'static, - Self: Protobuf, + Self: Protobuf, Ctx: Context, { /// The height for which the vote is for. diff --git a/code/node/src/network/msg.rs b/code/node/src/network/msg.rs index 1c38d5177..fec14ce28 100644 --- a/code/node/src/network/msg.rs +++ b/code/node/src/network/msg.rs @@ -1,4 +1,5 @@ use prost::Message; +use prost::Name; use prost_types::Any; use malachite_proto::Error as ProtoError; @@ -16,32 +17,48 @@ pub enum Msg { impl Msg { pub fn from_network_bytes(bytes: &[u8]) -> Result { - Protobuf::::from_bytes(bytes) + Protobuf::from_bytes(bytes) } pub fn to_network_bytes(&self) -> Result, ProtoError> { - Protobuf::::to_bytes(self) + Protobuf::to_bytes(self) } const DUMMY_TYPE_URL: &'static str = "malachite.Dummy"; } -impl Protobuf for Msg { - fn from_bytes(bytes: &[u8]) -> Result - where - Self: Sized, - { - use prost::Name; +impl From for Any { + fn from(msg: Msg) -> Self { + match msg { + Msg::Vote(vote) => Any { + type_url: SignedVote::type_url(), + value: vote.encode_to_vec(), + }, + Msg::Proposal(proposal) => Any { + type_url: Proposal::type_url(), + value: proposal.encode_to_vec(), + }, - let any = Any::decode(bytes)?; + #[cfg(test)] + Msg::Dummy(value) => Any { + type_url: Msg::DUMMY_TYPE_URL.to_string(), + value: value.to_be_bytes().to_vec(), + }, + } + } +} +impl TryFrom for Msg { + type Error = ProtoError; + + fn try_from(any: Any) -> Result { if any.type_url == SignedVote::type_url() { let vote = SignedVote::decode(any.value.as_slice())?; Ok(Msg::Vote(vote)) - } else if any.type_url == malachite_proto::Proposal::type_url() { + } else if any.type_url == Proposal::type_url() { let proposal = Proposal::decode(any.value.as_slice())?; Ok(Msg::Proposal(proposal)) - } else if any.type_url == Msg::DUMMY_TYPE_URL { + } else if cfg!(test) && any.type_url == Msg::DUMMY_TYPE_URL { #[cfg(test)] { let value = u64::from_be_bytes(any.value.try_into().unwrap()); @@ -62,27 +79,20 @@ impl Protobuf for Msg { ))) } } +} - fn into_bytes(self) -> Result, ProtoError> { - match self { - Msg::Vote(vote) => { - let any = Any::from_msg(&vote)?; - Ok(any.encode_to_vec()) - } - Msg::Proposal(proposal) => { - let any = Any::from_msg(&proposal)?; - Ok(any.encode_to_vec()) - } +impl Protobuf for Msg { + type Proto = Any; - #[cfg(test)] - Msg::Dummy(value) => { - let any = Any { - type_url: Msg::DUMMY_TYPE_URL.to_string(), - value: value.to_be_bytes().to_vec(), - }; + fn from_bytes(bytes: &[u8]) -> Result + where + Self: Sized, + { + let any = Any::decode(bytes)?; + Self::try_from(any) + } - Ok(any.encode_to_vec()) - } - } + fn into_bytes(self) -> Result, ProtoError> { + Ok(Any::from(self).encode_to_vec()) } } diff --git a/code/proto/src/lib.rs b/code/proto/src/lib.rs index e29435191..441603b3e 100644 --- a/code/proto/src/lib.rs +++ b/code/proto/src/lib.rs @@ -18,38 +18,34 @@ pub enum Error { Other(String), } -pub trait Protobuf { - fn from_bytes(bytes: &[u8]) -> Result - where - Self: Sized; - fn into_bytes(self) -> Result, Error>; +pub trait Protobuf { + type Proto: Message + Default; - fn to_bytes(&self) -> Result, Error> + fn from_bytes(bytes: &[u8]) -> Result where - Self: Clone, + Self: TryFrom + Sized, // FIXME: Require `TryFrom<&Self::Proto>` instead + Self::Error: Display, // FIXME: Require `std::error::Error` instead { - self.clone().into_bytes() + let proto = Self::Proto::decode(bytes)?; + Self::try_from(proto).map_err(|e| Error::Other(e.to_string())) } -} -impl Protobuf for T -where - T: TryFrom, - T::Error: Display, - Proto: Message + From + Default, -{ - fn from_bytes(bytes: &[u8]) -> Result + fn into_bytes(self) -> Result, Error> where Self: Sized, + Self::Proto: From, { - let proto = Proto::decode(bytes)?; - Self::try_from(proto).map_err(|e| Error::Other(e.to_string())) - } - - fn into_bytes(self) -> Result, Error> { - let proto = Proto::from(self); + let proto = Self::Proto::from(self); let mut bytes = Vec::with_capacity(proto.encoded_len()); proto.encode(&mut bytes)?; Ok(bytes) } + + fn to_bytes(&self) -> Result, Error> + where + Self: Sized + Clone, + Self::Proto: From, + { + Protobuf::into_bytes(self.clone()) + } } diff --git a/code/test/src/address.rs b/code/test/src/address.rs new file mode 100644 index 000000000..d5d44e710 --- /dev/null +++ b/code/test/src/address.rs @@ -0,0 +1,65 @@ +use core::fmt; + +use crate::signing::PublicKey; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Address([u8; Self::LENGTH]); + +impl Address { + const LENGTH: usize = 20; + + #[cfg_attr(coverage_nightly, coverage(off))] + pub const fn new(value: [u8; Self::LENGTH]) -> Self { + Self(value) + } + + #[cfg_attr(coverage_nightly, coverage(off))] + pub fn from_public_key(public_key: &PublicKey) -> Self { + let hash = public_key.hash(); + let mut address = [0; Self::LENGTH]; + address.copy_from_slice(&hash[..Self::LENGTH]); + Self(address) + } +} + +impl fmt::Display for Address { + #[cfg_attr(coverage_nightly, coverage(off))] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for byte in self.0.iter() { + write!(f, "{:02x}", byte)?; + } + Ok(()) + } +} + +impl malachite_common::Address for Address {} + +impl TryFrom for Address { + type Error = String; + + fn try_from(proto: malachite_proto::Address) -> Result { + if proto.value.len() != Self::LENGTH { + return Err(format!( + "Invalid address length: expected {}, got {}", + Self::LENGTH, + proto.value.len() + )); + } + + let mut address = [0; Self::LENGTH]; + address.copy_from_slice(&proto.value); + Ok(Self(address)) + } +} + +impl From
for malachite_proto::Address { + fn from(address: Address) -> Self { + Self { + value: address.0.to_vec(), + } + } +} + +impl malachite_proto::Protobuf for Address { + type Proto = malachite_proto::Address; +} diff --git a/code/test/src/context.rs b/code/test/src/context.rs index 9bfbdca95..49ab192b1 100644 --- a/code/test/src/context.rs +++ b/code/test/src/context.rs @@ -3,9 +3,10 @@ use malachite_common::NilOrVal; use malachite_common::Round; use malachite_common::SignedVote; +use crate::address::*; use crate::height::*; use crate::proposal::*; -use crate::signing::{Ed25519, PrivateKey, PublicKey}; +use crate::signing::*; use crate::validator_set::*; use crate::value::*; use crate::vote::*; diff --git a/code/test/src/height.rs b/code/test/src/height.rs index e2737ca76..52c6c9daa 100644 --- a/code/test/src/height.rs +++ b/code/test/src/height.rs @@ -29,3 +29,7 @@ impl From for malachite_proto::Height { malachite_proto::Height { value: height.0 } } } + +impl malachite_proto::Protobuf for Height { + type Proto = malachite_proto::Height; +} diff --git a/code/test/src/lib.rs b/code/test/src/lib.rs index 6c24bcd72..869cfe276 100644 --- a/code/test/src/lib.rs +++ b/code/test/src/lib.rs @@ -2,6 +2,7 @@ #![deny(trivial_casts, trivial_numeric_casts)] #![cfg_attr(coverage_nightly, feature(coverage_attribute))] +mod address; mod context; mod height; mod proposal; @@ -12,6 +13,7 @@ mod vote; pub mod utils; +pub use crate::address::*; pub use crate::context::*; pub use crate::height::*; pub use crate::proposal::*; diff --git a/code/test/src/proposal.rs b/code/test/src/proposal.rs index 012084ee8..3a9cfacf1 100644 --- a/code/test/src/proposal.rs +++ b/code/test/src/proposal.rs @@ -63,3 +63,7 @@ impl From for malachite_proto::Proposal { } } } + +impl malachite_proto::Protobuf for Proposal { + type Proto = malachite_proto::Proposal; +} diff --git a/code/test/src/validator_set.rs b/code/test/src/validator_set.rs index fd08eea27..49b9c00f5 100644 --- a/code/test/src/validator_set.rs +++ b/code/test/src/validator_set.rs @@ -1,66 +1,7 @@ -use core::fmt; - use malachite_common::VotingPower; -use crate::{signing::PublicKey, TestContext}; - -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct Address([u8; Self::LENGTH]); - -impl Address { - const LENGTH: usize = 20; - - #[cfg_attr(coverage_nightly, coverage(off))] - pub const fn new(value: [u8; Self::LENGTH]) -> Self { - Self(value) - } - - #[cfg_attr(coverage_nightly, coverage(off))] - pub fn from_public_key(public_key: &PublicKey) -> Self { - let hash = public_key.hash(); - let mut address = [0; Self::LENGTH]; - address.copy_from_slice(&hash[..Self::LENGTH]); - Self(address) - } -} - -impl fmt::Display for Address { - #[cfg_attr(coverage_nightly, coverage(off))] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for byte in self.0.iter() { - write!(f, "{:02x}", byte)?; - } - Ok(()) - } -} - -impl malachite_common::Address for Address {} - -impl TryFrom for Address { - type Error = String; - - fn try_from(proto: malachite_proto::Address) -> Result { - if proto.value.len() != Self::LENGTH { - return Err(format!( - "Invalid address length: expected {}, got {}", - Self::LENGTH, - proto.value.len() - )); - } - - let mut address = [0; Self::LENGTH]; - address.copy_from_slice(&proto.value); - Ok(Self(address)) - } -} - -impl From
for malachite_proto::Address { - fn from(address: Address) -> Self { - Self { - value: address.0.to_vec(), - } - } -} +use crate::signing::PublicKey; +use crate::{Address, TestContext}; /// A validator is a public key and voting power #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/code/test/src/value.rs b/code/test/src/value.rs index 185c9952a..d53881440 100644 --- a/code/test/src/value.rs +++ b/code/test/src/value.rs @@ -87,3 +87,7 @@ impl From for malachite_proto::Value { } } } + +impl malachite_proto::Protobuf for Value { + type Proto = malachite_proto::Value; +} diff --git a/code/test/src/vote.rs b/code/test/src/vote.rs index 0acebd12e..71ca9e2a2 100644 --- a/code/test/src/vote.rs +++ b/code/test/src/vote.rs @@ -133,3 +133,7 @@ impl From for malachite_proto::Vote { } } } + +impl malachite_proto::Protobuf for Vote { + type Proto = malachite_proto::Vote; +} From 67babff16b241de402ba6a21964eefeb57f803cf Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 20 Feb 2024 16:39:53 +0100 Subject: [PATCH 10/50] Add `Protobuf` impl for `SignedVote` --- code/common/src/signed_vote.rs | 29 ++++++++++++++++++++++++++++- code/common/src/signing.rs | 9 +++++++++ code/proto/src/lib.rs | 7 ++----- code/test/src/signing.rs | 9 +++++++++ 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/code/common/src/signed_vote.rs b/code/common/src/signed_vote.rs index 551a401fa..527d2a828 100644 --- a/code/common/src/signed_vote.rs +++ b/code/common/src/signed_vote.rs @@ -1,6 +1,9 @@ +use alloc::string::ToString; + use derive_where::derive_where; +use malachite_proto::Protobuf; -use crate::{Context, Signature, Vote}; +use crate::{Context, Signature, SigningScheme, Vote}; /// A signed vote, ie. a vote emitted by a validator and signed by its private key. #[derive_where(Clone, Debug, PartialEq, Eq)] @@ -29,3 +32,27 @@ where self.vote.validator_address() } } + +use malachite_proto::Error as ProtoError; + +impl TryFrom for SignedVote +where + Ctx::Vote: TryFrom, +{ + type Error = malachite_proto::Error; + + fn try_from(value: malachite_proto::SignedVote) -> Result { + let vote = value + .vote + .ok_or_else(|| ProtoError::Other("Missing field `vote`".to_string()))?; + + Ok(Self { + vote: Ctx::Vote::try_from(vote)?, + signature: Ctx::SigningScheme::decode_signature(&value.signature)?, + }) + } +} + +impl Protobuf for SignedVote { + type Proto = malachite_proto::SignedVote; +} diff --git a/code/common/src/signing.rs b/code/common/src/signing.rs index d91caabef..75291f339 100644 --- a/code/common/src/signing.rs +++ b/code/common/src/signing.rs @@ -1,7 +1,10 @@ use core::fmt::Debug; +use alloc::vec::Vec; use signature::{Keypair, Signer, Verifier}; +use malachite_proto::Error as ProtoError; + /// A signing scheme that can be used to sign votes and verify such signatures. /// /// This trait is used to abstract over the signature scheme used by the consensus engine. @@ -22,4 +25,10 @@ where /// The type of private keys produced by this signing scheme. type PrivateKey: Clone + Signer + Keypair; + + /// Decode a signature from a byte array. + fn decode_signature(bytes: &[u8]) -> Result; + + /// Encode a signature to a byte array. + fn encode_signature(signature: &Self::Signature) -> Vec; } diff --git a/code/proto/src/lib.rs b/code/proto/src/lib.rs index 441603b3e..f1756cd39 100644 --- a/code/proto/src/lib.rs +++ b/code/proto/src/lib.rs @@ -1,5 +1,3 @@ -use std::fmt::Display; - use thiserror::Error; use prost::{DecodeError, EncodeError, Message}; @@ -23,11 +21,10 @@ pub trait Protobuf { fn from_bytes(bytes: &[u8]) -> Result where - Self: TryFrom + Sized, // FIXME: Require `TryFrom<&Self::Proto>` instead - Self::Error: Display, // FIXME: Require `std::error::Error` instead + Self: TryFrom + Sized, // FIXME: Require `TryFrom<&Self::Proto>` instead { let proto = Self::Proto::decode(bytes)?; - Self::try_from(proto).map_err(|e| Error::Other(e.to_string())) + Self::try_from(proto) } fn into_bytes(self) -> Result, Error> diff --git a/code/test/src/signing.rs b/code/test/src/signing.rs index a0564f497..d2c0e3d63 100644 --- a/code/test/src/signing.rs +++ b/code/test/src/signing.rs @@ -21,6 +21,15 @@ impl SigningScheme for Ed25519 { type Signature = Signature; type PublicKey = PublicKey; type PrivateKey = PrivateKey; + + fn encode_signature(signature: &Signature) -> Vec { + signature.to_bytes().to_vec() + } + + fn decode_signature(bytes: &[u8]) -> Result { + Signature::try_from(bytes) + .map_err(|e| malachite_proto::Error::Other(format!("Failed to decode signature: {e}"))) + } } #[derive(Clone, Debug)] From ac5f202242d85c62ea4996455ff1b208b7d0c2d7 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 20 Feb 2024 16:42:04 +0100 Subject: [PATCH 11/50] Cleanup --- code/proto/src/lib.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/code/proto/src/lib.rs b/code/proto/src/lib.rs index f1756cd39..9d26ca71a 100644 --- a/code/proto/src/lib.rs +++ b/code/proto/src/lib.rs @@ -16,12 +16,12 @@ pub enum Error { Other(String), } -pub trait Protobuf { +pub trait Protobuf: Sized { type Proto: Message + Default; fn from_bytes(bytes: &[u8]) -> Result where - Self: TryFrom + Sized, // FIXME: Require `TryFrom<&Self::Proto>` instead + Self: TryFrom, { let proto = Self::Proto::decode(bytes)?; Self::try_from(proto) @@ -29,7 +29,6 @@ pub trait Protobuf { fn into_bytes(self) -> Result, Error> where - Self: Sized, Self::Proto: From, { let proto = Self::Proto::from(self); @@ -40,7 +39,7 @@ pub trait Protobuf { fn to_bytes(&self) -> Result, Error> where - Self: Sized + Clone, + Self: Clone, Self::Proto: From, { Protobuf::into_bytes(self.clone()) From 967587b72b73bb9557e8004280a504f6eec83551 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 20 Feb 2024 17:15:56 +0100 Subject: [PATCH 12/50] Remove Protobuf related code from `malachite-common` and into `malachite-proto` --- code/common/Cargo.toml | 1 - code/common/src/height.rs | 3 -- code/common/src/proposal.rs | 3 -- code/common/src/round.rs | 17 --------- code/common/src/signed_vote.rs | 29 +-------------- code/common/src/signing.rs | 11 +++--- code/common/src/validator_set.rs | 3 -- code/common/src/value.rs | 31 ---------------- code/common/src/vote.rs | 24 ------------ code/proto/Cargo.toml | 2 + code/proto/src/impls.rs | 64 ++++++++++++++++++++++++++++++++ code/proto/src/lib.rs | 2 + code/test/src/signing.rs | 5 ++- 13 files changed, 78 insertions(+), 117 deletions(-) create mode 100644 code/proto/src/impls.rs diff --git a/code/common/Cargo.toml b/code/common/Cargo.toml index dd356d70c..c1c67468b 100644 --- a/code/common/Cargo.toml +++ b/code/common/Cargo.toml @@ -9,6 +9,5 @@ license.workspace = true publish.workspace = true [dependencies] -malachite-proto.workspace = true derive-where.workspace = true signature.workspace = true diff --git a/code/common/src/height.rs b/code/common/src/height.rs index fc34b7ade..5f458c123 100644 --- a/code/common/src/height.rs +++ b/code/common/src/height.rs @@ -1,7 +1,5 @@ use core::fmt::Debug; -use malachite_proto::Protobuf; - /// Defines the requirements for a height type. /// /// A height denotes the number of blocks (values) created since the chain began. @@ -10,6 +8,5 @@ use malachite_proto::Protobuf; pub trait Height where Self: Default + Copy + Clone + Debug + PartialEq + Eq + PartialOrd + Ord, - Self: Protobuf, { } diff --git a/code/common/src/proposal.rs b/code/common/src/proposal.rs index e273f7924..d63168493 100644 --- a/code/common/src/proposal.rs +++ b/code/common/src/proposal.rs @@ -1,14 +1,11 @@ use core::fmt::Debug; -use malachite_proto::Protobuf; - use crate::{Context, Round}; /// Defines the requirements for a proposal type. pub trait Proposal where Self: Clone + Debug + Eq + Send + Sync + 'static, - Self: Protobuf, Ctx: Context, { /// The height for which the proposal is for. diff --git a/code/common/src/round.rs b/code/common/src/round.rs index 57a5c2f34..713c8954d 100644 --- a/code/common/src/round.rs +++ b/code/common/src/round.rs @@ -1,5 +1,4 @@ use core::cmp; -use core::convert::Infallible; /// A round number. /// @@ -73,22 +72,6 @@ impl Ord for Round { } } -impl TryFrom for Round { - type Error = Infallible; - - fn try_from(proto: malachite_proto::Round) -> Result { - Ok(Self::new(proto.round)) - } -} - -impl From for malachite_proto::Round { - fn from(round: Round) -> malachite_proto::Round { - malachite_proto::Round { - round: round.as_i64(), - } - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/code/common/src/signed_vote.rs b/code/common/src/signed_vote.rs index 527d2a828..551a401fa 100644 --- a/code/common/src/signed_vote.rs +++ b/code/common/src/signed_vote.rs @@ -1,9 +1,6 @@ -use alloc::string::ToString; - use derive_where::derive_where; -use malachite_proto::Protobuf; -use crate::{Context, Signature, SigningScheme, Vote}; +use crate::{Context, Signature, Vote}; /// A signed vote, ie. a vote emitted by a validator and signed by its private key. #[derive_where(Clone, Debug, PartialEq, Eq)] @@ -32,27 +29,3 @@ where self.vote.validator_address() } } - -use malachite_proto::Error as ProtoError; - -impl TryFrom for SignedVote -where - Ctx::Vote: TryFrom, -{ - type Error = malachite_proto::Error; - - fn try_from(value: malachite_proto::SignedVote) -> Result { - let vote = value - .vote - .ok_or_else(|| ProtoError::Other("Missing field `vote`".to_string()))?; - - Ok(Self { - vote: Ctx::Vote::try_from(vote)?, - signature: Ctx::SigningScheme::decode_signature(&value.signature)?, - }) - } -} - -impl Protobuf for SignedVote { - type Proto = malachite_proto::SignedVote; -} diff --git a/code/common/src/signing.rs b/code/common/src/signing.rs index 75291f339..3da354c04 100644 --- a/code/common/src/signing.rs +++ b/code/common/src/signing.rs @@ -1,9 +1,7 @@ -use core::fmt::Debug; - use alloc::vec::Vec; -use signature::{Keypair, Signer, Verifier}; +use core::fmt::{Debug, Display}; -use malachite_proto::Error as ProtoError; +use signature::{Keypair, Signer, Verifier}; /// A signing scheme that can be used to sign votes and verify such signatures. /// @@ -17,6 +15,9 @@ pub trait SigningScheme where Self: Clone + Debug + Eq, { + /// Errors that can occur when decoding a signature from a byte array. + type DecodingError: Display; + /// The type of signatures produced by this signing scheme. type Signature: Clone + Debug + Eq; @@ -27,7 +28,7 @@ where type PrivateKey: Clone + Signer + Keypair; /// Decode a signature from a byte array. - fn decode_signature(bytes: &[u8]) -> Result; + fn decode_signature(bytes: &[u8]) -> Result; /// Encode a signature to a byte array. fn encode_signature(signature: &Self::Signature) -> Vec; diff --git a/code/common/src/validator_set.rs b/code/common/src/validator_set.rs index 309b6be9a..ccc4dd1f6 100644 --- a/code/common/src/validator_set.rs +++ b/code/common/src/validator_set.rs @@ -1,7 +1,5 @@ use core::fmt::{Debug, Display}; -use malachite_proto::Protobuf; - use crate::{Context, PublicKey}; /// Voting power held by a validator. @@ -13,7 +11,6 @@ pub type VotingPower = u64; pub trait Address where Self: Clone + Debug + Display + Eq + Ord, - Self: Protobuf, { } diff --git a/code/common/src/value.rs b/code/common/src/value.rs index a17765ce4..a16ac246e 100644 --- a/code/common/src/value.rs +++ b/code/common/src/value.rs @@ -1,7 +1,5 @@ use core::fmt::Debug; -use malachite_proto::Protobuf; - /// Represents either `Nil` or a value of type `Value`. /// /// This type is isomorphic to `Option` but is more explicit about its intent. @@ -55,39 +53,10 @@ impl NilOrVal { } } -// impl TryFrom for NilOrVal -// where -// Value: From, // FIXME -// { -// type Error = String; -// -// fn try_from(proto: malachite_proto::Value) -> Result { -// match proto.value { -// Some(value) => Ok(NilOrVal::Val(Value::from(value))), // FIXME -// None => Ok(NilOrVal::Nil), -// } -// } -// } -// -// impl TryFrom for NilOrVal -// where -// Value: TryFrom>, // FIXME -// { -// type Error = String; -// -// fn try_from(proto: malachite_proto::ValueId) -> Result { -// match proto.value { -// Some(value) => Ok(NilOrVal::Val(Value::from(value))), // FIXME -// None => Ok(NilOrVal::Nil), -// } -// } -// } - /// Defines the requirements for the type of value to decide on. pub trait Value where Self: Clone + Debug + PartialEq + Eq + PartialOrd + Ord, - Self: Protobuf, { /// The type of the ID of the value. /// Typically a representation of the value with a lower memory footprint. diff --git a/code/common/src/vote.rs b/code/common/src/vote.rs index a27d39e78..82f9c77a9 100644 --- a/code/common/src/vote.rs +++ b/code/common/src/vote.rs @@ -1,8 +1,5 @@ -use core::convert::Infallible; use core::fmt::Debug; -use malachite_proto::Protobuf; - use crate::{Context, NilOrVal, Round, Value}; /// A type of vote. @@ -15,26 +12,6 @@ pub enum VoteType { Precommit, } -impl TryFrom for VoteType { - type Error = Infallible; - - fn try_from(vote_type: malachite_proto::VoteType) -> Result { - match vote_type { - malachite_proto::VoteType::Prevote => Ok(VoteType::Prevote), - malachite_proto::VoteType::Precommit => Ok(VoteType::Precommit), - } - } -} - -impl From for malachite_proto::VoteType { - fn from(vote_type: VoteType) -> malachite_proto::VoteType { - match vote_type { - VoteType::Prevote => malachite_proto::VoteType::Prevote, - VoteType::Precommit => malachite_proto::VoteType::Precommit, - } - } -} - /// Defines the requirements for a vote. /// /// Votes are signed messages from validators for a particular value which @@ -42,7 +19,6 @@ impl From for malachite_proto::VoteType { pub trait Vote where Self: Clone + Debug + Eq + Send + Sync + 'static, - Self: Protobuf, Ctx: Context, { /// The height for which the vote is for. diff --git a/code/proto/Cargo.toml b/code/proto/Cargo.toml index 7d09ef8d1..8105ae134 100644 --- a/code/proto/Cargo.toml +++ b/code/proto/Cargo.toml @@ -7,6 +7,8 @@ license.workspace = true publish.workspace = true [dependencies] +malachite-common.workspace = true + prost.workspace = true prost-types.workspace = true thiserror.workspace = true diff --git a/code/proto/src/impls.rs b/code/proto/src/impls.rs new file mode 100644 index 000000000..e8e3add9d --- /dev/null +++ b/code/proto/src/impls.rs @@ -0,0 +1,64 @@ +use core::convert::Infallible; + +use malachite_common::{Context, Round, SignedVote, SigningScheme, VoteType}; + +use crate::{self as proto, Error, Protobuf}; + +impl TryFrom for Round { + type Error = Infallible; + + fn try_from(proto: proto::Round) -> Result { + Ok(Self::new(proto.round)) + } +} + +impl From for proto::Round { + fn from(round: Round) -> proto::Round { + proto::Round { + round: round.as_i64(), + } + } +} + +impl TryFrom for SignedVote +where + Ctx::Vote: TryFrom, +{ + type Error = Error; + + fn try_from(value: proto::SignedVote) -> Result { + let vote = value + .vote + .ok_or_else(|| Error::Other("Missing field `vote`".to_string()))?; + + Ok(Self { + vote: Ctx::Vote::try_from(vote)?, + signature: Ctx::SigningScheme::decode_signature(&value.signature) + .map_err(|e| Error::Other(format!("Failed to decode signature: {e}")))?, + }) + } +} + +impl Protobuf for SignedVote { + type Proto = proto::SignedVote; +} + +impl TryFrom for VoteType { + type Error = Infallible; + + fn try_from(vote_type: proto::VoteType) -> Result { + match vote_type { + proto::VoteType::Prevote => Ok(VoteType::Prevote), + proto::VoteType::Precommit => Ok(VoteType::Precommit), + } + } +} + +impl From for proto::VoteType { + fn from(vote_type: VoteType) -> proto::VoteType { + match vote_type { + VoteType::Prevote => proto::VoteType::Prevote, + VoteType::Precommit => proto::VoteType::Precommit, + } + } +} diff --git a/code/proto/src/lib.rs b/code/proto/src/lib.rs index 9d26ca71a..b2c63e02c 100644 --- a/code/proto/src/lib.rs +++ b/code/proto/src/lib.rs @@ -4,6 +4,8 @@ use prost::{DecodeError, EncodeError, Message}; include!(concat!(env!("OUT_DIR"), "/malachite.rs")); +mod impls; + #[derive(Debug, Error)] pub enum Error { #[error("Failed to decode Protobuf message")] diff --git a/code/test/src/signing.rs b/code/test/src/signing.rs index d2c0e3d63..25b329ae7 100644 --- a/code/test/src/signing.rs +++ b/code/test/src/signing.rs @@ -18,6 +18,8 @@ impl Ed25519 { } impl SigningScheme for Ed25519 { + type DecodingError = ed25519_consensus::Error; + type Signature = Signature; type PublicKey = PublicKey; type PrivateKey = PrivateKey; @@ -26,9 +28,8 @@ impl SigningScheme for Ed25519 { signature.to_bytes().to_vec() } - fn decode_signature(bytes: &[u8]) -> Result { + fn decode_signature(bytes: &[u8]) -> Result { Signature::try_from(bytes) - .map_err(|e| malachite_proto::Error::Other(format!("Failed to decode signature: {e}"))) } } From 5f06576e2e7dda3185806d87a888bb489760fcaa Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 20 Feb 2024 17:22:35 +0100 Subject: [PATCH 13/50] Refactor `Protobuf` trait once more --- code/proto/src/impls.rs | 17 ++++++++++++++++- code/proto/src/lib.rs | 36 ++++++++++++++++++++++++------------ code/test/src/address.rs | 6 +++--- code/test/src/value.rs | 4 ++++ 4 files changed, 47 insertions(+), 16 deletions(-) diff --git a/code/proto/src/impls.rs b/code/proto/src/impls.rs index e8e3add9d..c0579b3b8 100644 --- a/code/proto/src/impls.rs +++ b/code/proto/src/impls.rs @@ -20,6 +20,18 @@ impl From for proto::Round { } } +impl From> for proto::SignedVote +where + Ctx::Vote: Into, +{ + fn from(signed_vote: SignedVote) -> proto::SignedVote { + proto::SignedVote { + vote: Some(signed_vote.vote.into()), + signature: Ctx::SigningScheme::encode_signature(&signed_vote.signature), + } + } +} + impl TryFrom for SignedVote where Ctx::Vote: TryFrom, @@ -39,7 +51,10 @@ where } } -impl Protobuf for SignedVote { +impl Protobuf for SignedVote +where + Ctx::Vote: TryFrom + Into, +{ type Proto = proto::SignedVote; } diff --git a/code/proto/src/lib.rs b/code/proto/src/lib.rs index b2c63e02c..42dcad570 100644 --- a/code/proto/src/lib.rs +++ b/code/proto/src/lib.rs @@ -1,3 +1,5 @@ +use std::convert::Infallible; + use thiserror::Error; use prost::{DecodeError, EncodeError, Message}; @@ -18,21 +20,32 @@ pub enum Error { Other(String), } -pub trait Protobuf: Sized { - type Proto: Message + Default; +impl From for Error { + fn from(s: String) -> Self { + Self::Other(s) + } +} - fn from_bytes(bytes: &[u8]) -> Result - where - Self: TryFrom, - { +impl From for Error { + fn from(_: Infallible) -> Self { + unreachable!() + } +} + +pub trait Protobuf +where + Self: Sized + TryFrom, + Error: From, +{ + type Proto: Message + Default + From; + + fn from_bytes(bytes: &[u8]) -> Result { let proto = Self::Proto::decode(bytes)?; - Self::try_from(proto) + let result = Self::try_from(proto)?; + Ok(result) } - fn into_bytes(self) -> Result, Error> - where - Self::Proto: From, - { + fn into_bytes(self) -> Result, Error> { let proto = Self::Proto::from(self); let mut bytes = Vec::with_capacity(proto.encoded_len()); proto.encode(&mut bytes)?; @@ -42,7 +55,6 @@ pub trait Protobuf: Sized { fn to_bytes(&self) -> Result, Error> where Self: Clone, - Self::Proto: From, { Protobuf::into_bytes(self.clone()) } diff --git a/code/test/src/address.rs b/code/test/src/address.rs index d5d44e710..b58a2eec1 100644 --- a/code/test/src/address.rs +++ b/code/test/src/address.rs @@ -35,15 +35,15 @@ impl fmt::Display for Address { impl malachite_common::Address for Address {} impl TryFrom for Address { - type Error = String; + type Error = malachite_proto::Error; fn try_from(proto: malachite_proto::Address) -> Result { if proto.value.len() != Self::LENGTH { - return Err(format!( + return Err(malachite_proto::Error::Other(format!( "Invalid address length: expected {}, got {}", Self::LENGTH, proto.value.len() - )); + ))); } let mut address = [0; Self::LENGTH]; diff --git a/code/test/src/value.rs b/code/test/src/value.rs index d53881440..679575326 100644 --- a/code/test/src/value.rs +++ b/code/test/src/value.rs @@ -39,6 +39,10 @@ impl From for malachite_proto::ValueId { } } +impl malachite_proto::Protobuf for ValueId { + type Proto = malachite_proto::ValueId; +} + /// The value to decide on #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct Value(u64); From 959e8aaa901fdf0aed6642097e877d3d60af6db6 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 21 Feb 2024 12:49:08 +0100 Subject: [PATCH 14/50] Implement skeleton for basic node --- code/Cargo.toml | 1 + code/common/src/context.rs | 18 +++- code/common/src/lib.rs | 2 + code/common/src/round.rs | 8 +- code/common/src/signed_proposal.rs | 29 +++++ code/common/src/timeout.rs | 4 +- code/driver/src/driver.rs | 38 +++++-- code/node/Cargo.toml | 1 + code/node/src/lib.rs | 1 + code/node/src/network.rs | 4 +- code/node/src/network/msg.rs | 13 ++- code/node/src/node.rs | 165 +++++++++++++++++++++++++++++ code/node/src/timers.rs | 103 ++++++++++++++++++ code/proto/src/impls.rs | 40 ++++++- code/proto/src/malachite.proto | 12 ++- code/test/src/context.rs | 29 ++++- code/test/src/height.rs | 7 ++ code/test/src/proposal.rs | 5 + code/test/src/vote.rs | 17 +-- code/test/tests/driver.rs | 26 ++--- code/test/tests/driver_extra.rs | 22 ++-- 21 files changed, 479 insertions(+), 66 deletions(-) create mode 100644 code/common/src/signed_proposal.rs create mode 100644 code/node/src/timers.rs diff --git a/code/Cargo.toml b/code/Cargo.toml index 136214354..5c82a254b 100644 --- a/code/Cargo.toml +++ b/code/Cargo.toml @@ -49,3 +49,4 @@ signature = "2.1.0" thiserror = "1.0" tokio = "1.35.1" tokio-stream = "0.1" +tracing = "0.1.40" diff --git a/code/common/src/context.rs b/code/common/src/context.rs index 24caddfce..46c93a70c 100644 --- a/code/common/src/context.rs +++ b/code/common/src/context.rs @@ -1,13 +1,13 @@ use crate::{ - Address, Height, NilOrVal, Proposal, PublicKey, Round, SignedVote, SigningScheme, Validator, - ValidatorSet, Value, ValueId, Vote, + Address, Height, NilOrVal, Proposal, PublicKey, Round, SignedProposal, SignedVote, + SigningScheme, Validator, ValidatorSet, Value, ValueId, Vote, }; /// This trait allows to abstract over the various datatypes /// that are used in the consensus engine. pub trait Context where - Self: Sized + Send + Sync + 'static, + Self: Sized + Clone + Send + Sync + 'static, { /// The type of address of a validator. type Address: Address; @@ -33,7 +33,7 @@ where /// The signing scheme used to sign votes. type SigningScheme: SigningScheme; - /// Sign the given vote our private key. + /// Sign the given vote with our private key. fn sign_vote(&self, vote: Self::Vote) -> SignedVote; /// Verify the given vote's signature using the given public key. @@ -43,6 +43,16 @@ where public_key: &PublicKey, ) -> bool; + /// Sign the given proposal with our private key. + fn sign_proposal(&self, proposal: Self::Proposal) -> SignedProposal; + + /// Verify the given proposal's signature using the given public key. + fn verify_signed_proposal( + &self, + signed_proposal: &SignedProposal, + public_key: &PublicKey, + ) -> bool; + /// Build a new proposal for the given value at the given height, round and POL round. fn new_proposal( height: Self::Height, diff --git a/code/common/src/lib.rs b/code/common/src/lib.rs index 3c7f702a6..184f10e5c 100644 --- a/code/common/src/lib.rs +++ b/code/common/src/lib.rs @@ -18,6 +18,7 @@ mod context; mod height; mod proposal; mod round; +mod signed_proposal; mod signed_vote; mod signing; mod timeout; @@ -44,6 +45,7 @@ pub use context::Context; pub use height::Height; pub use proposal::Proposal; pub use round::Round; +pub use signed_proposal::SignedProposal; pub use signed_vote::SignedVote; pub use signing::SigningScheme; pub use timeout::{Timeout, TimeoutStep}; diff --git a/code/common/src/round.rs b/code/common/src/round.rs index 713c8954d..5001c66de 100644 --- a/code/common/src/round.rs +++ b/code/common/src/round.rs @@ -1,4 +1,4 @@ -use core::cmp; +use core::{cmp, fmt}; /// A round number. /// @@ -72,6 +72,12 @@ impl Ord for Round { } } +impl fmt::Display for Round { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.as_i64().fmt(f) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/code/common/src/signed_proposal.rs b/code/common/src/signed_proposal.rs new file mode 100644 index 000000000..f4b07ef2f --- /dev/null +++ b/code/common/src/signed_proposal.rs @@ -0,0 +1,29 @@ +use derive_where::derive_where; + +use crate::{Context, Signature}; + +/// A signed proposal, ie. a proposal emitted by a validator and signed by its private key. +#[derive_where(Clone, Debug, PartialEq, Eq)] +pub struct SignedProposal +where + Ctx: Context, +{ + /// The proposal. + pub proposal: Ctx::Proposal, + + /// The signature of the proposal. + pub signature: Signature, +} + +impl SignedProposal +where + Ctx: Context, +{ + /// Create a new signed proposal from the given proposal and signature. + pub fn new(proposal: Ctx::Proposal, signature: Signature) -> Self { + Self { + proposal, + signature, + } + } +} diff --git a/code/common/src/timeout.rs b/code/common/src/timeout.rs index 30b3b76d6..7f8efb718 100644 --- a/code/common/src/timeout.rs +++ b/code/common/src/timeout.rs @@ -1,7 +1,7 @@ use crate::Round; /// The round step for which the timeout is for. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub enum TimeoutStep { /// The timeout is for the propose step. Propose, @@ -14,7 +14,7 @@ pub enum TimeoutStep { } /// A timeout for a round step. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub struct Timeout { /// The round for which the timeout is for. pub round: Round, diff --git a/code/driver/src/driver.rs b/code/driver/src/driver.rs index 059d22621..e093085b8 100644 --- a/code/driver/src/driver.rs +++ b/code/driver/src/driver.rs @@ -1,4 +1,4 @@ -use alloc::boxed::Box; +use alloc::sync::Arc; use alloc::vec; use alloc::vec::Vec; use core::fmt; @@ -29,11 +29,14 @@ where pub ctx: Ctx, /// The proposer selector. - pub proposer_selector: Box>, + pub proposer_selector: Arc>, /// The address of the node. pub address: Ctx::Address, + /// Quorum thresholds + pub threshold_params: ThresholdParams, + /// The validator set at the current height pub validator_set: Ctx::ValidatorSet, @@ -61,22 +64,43 @@ where pub fn new( ctx: Ctx, height: Ctx::Height, - proposer_selector: impl ProposerSelector + 'static, + proposer_selector: Arc>, validator_set: Ctx::ValidatorSet, address: Ctx::Address, threshold_params: ThresholdParams, ) -> Self { - let votes = VoteKeeper::new(validator_set.total_voting_power(), threshold_params); + let vote_keeper = VoteKeeper::new(validator_set.total_voting_power(), threshold_params); + let round_state = RoundState::new(height, Round::Nil); Self { ctx, - proposer_selector: Box::new(proposer_selector), + proposer_selector, address, + threshold_params, validator_set, - vote_keeper: votes, - round_state: RoundState::new(height, Round::Nil), + vote_keeper, + round_state, + proposal: None, + pending_input: None, + } + } + + /// Reset votes, round state, pending input and move to new height. + /// TODO: Allow validator set to change + pub fn move_to_height(self, height: Ctx::Height) -> Self { + let vote_keeper = VoteKeeper::new( + self.validator_set.total_voting_power(), + self.threshold_params, + ); + + let round_state = RoundState::new(height, Round::Nil); + + Self { + vote_keeper, + round_state, proposal: None, pending_input: None, + ..self } } diff --git a/code/node/Cargo.toml b/code/node/Cargo.toml index a6ccbd83b..47a5ad156 100644 --- a/code/node/Cargo.toml +++ b/code/node/Cargo.toml @@ -21,6 +21,7 @@ tokio = { workspace = true, features = ["full"] } tokio-stream = { workspace = true } prost = { workspace = true } prost-types = { workspace = true } +tracing = { workspace = true } [dev-dependencies] malachite-test.workspace = true diff --git a/code/node/src/lib.rs b/code/node/src/lib.rs index 4c4746032..daa3e115a 100644 --- a/code/node/src/lib.rs +++ b/code/node/src/lib.rs @@ -1,2 +1,3 @@ pub mod network; pub mod node; +pub mod timers; diff --git a/code/node/src/network.rs b/code/node/src/network.rs index 4d9e1117b..9fdc55837 100644 --- a/code/node/src/network.rs +++ b/code/node/src/network.rs @@ -3,7 +3,7 @@ use core::fmt; pub mod broadcast; mod msg; -use malachite_proto::{Proposal, SignedVote}; +use malachite_proto::{SignedProposal, SignedVote}; pub use self::msg::Msg; @@ -24,7 +24,7 @@ pub trait Network { async fn broadcast_vote(&mut self, vote: SignedVote) { self.broadcast(Msg::Vote(vote)).await } - async fn broadcast_proposal(&mut self, proposal: Proposal) { + async fn broadcast_proposal(&mut self, proposal: SignedProposal) { self.broadcast(Msg::Proposal(proposal)).await } } diff --git a/code/node/src/network/msg.rs b/code/node/src/network/msg.rs index fec14ce28..09eae90eb 100644 --- a/code/node/src/network/msg.rs +++ b/code/node/src/network/msg.rs @@ -1,15 +1,14 @@ -use prost::Message; -use prost::Name; +use prost::{Message, Name}; use prost_types::Any; use malachite_proto::Error as ProtoError; use malachite_proto::Protobuf; -use malachite_proto::{Proposal, SignedVote}; +use malachite_proto::{SignedProposal, SignedVote}; #[derive(Clone, Debug, PartialEq)] pub enum Msg { Vote(SignedVote), - Proposal(Proposal), + Proposal(SignedProposal), #[cfg(test)] Dummy(u64), @@ -35,7 +34,7 @@ impl From for Any { value: vote.encode_to_vec(), }, Msg::Proposal(proposal) => Any { - type_url: Proposal::type_url(), + type_url: SignedProposal::type_url(), value: proposal.encode_to_vec(), }, @@ -55,8 +54,8 @@ impl TryFrom for Msg { if any.type_url == SignedVote::type_url() { let vote = SignedVote::decode(any.value.as_slice())?; Ok(Msg::Vote(vote)) - } else if any.type_url == Proposal::type_url() { - let proposal = Proposal::decode(any.value.as_slice())?; + } else if any.type_url == SignedProposal::type_url() { + let proposal = SignedProposal::decode(any.value.as_slice())?; Ok(Msg::Proposal(proposal)) } else if cfg!(test) && any.type_url == Msg::DUMMY_TYPE_URL { #[cfg(test)] diff --git a/code/node/src/node.rs b/code/node/src/node.rs index e69de29bb..a127eeb0d 100644 --- a/code/node/src/node.rs +++ b/code/node/src/node.rs @@ -0,0 +1,165 @@ +#![allow(dead_code)] + +use std::fmt::{Debug, Display}; +use std::sync::Arc; + +use tokio::sync::mpsc; +use tracing::info; + +use malachite_common::{Context, Proposal, Round, SignedProposal, SignedVote, Timeout, Vote}; +use malachite_driver::{Driver, Input, Output, ProposerSelector, Validity}; +use malachite_proto as proto; +use malachite_vote::ThresholdParams; + +use crate::network::Msg as NetworkMsg; +use crate::network::Network; +use crate::timers::{self, Timers}; + +pub struct Params { + pub start_height: Ctx::Height, + pub proposer_selector: Arc>, + pub validator_set: Ctx::ValidatorSet, + pub address: Ctx::Address, + pub threshold_params: ThresholdParams, +} + +pub struct Node +where + Ctx: Context, +{ + ctx: Ctx, + driver: Driver, + params: Params, + network: Net, + timers: Timers, + timeout_elapsed: mpsc::Receiver, +} + +impl Node +where + Ctx: Context, + Net: Network, + Ctx::Height: Display, + // TODO: Avoid having to specify these bounds + Ctx::Vote: TryFrom, + >::Error: Debug, + Ctx::Proposal: TryFrom, + >::Error: Debug, + SignedVote: TryFrom, + as TryFrom>::Error: Debug, + SignedProposal: TryFrom, + as TryFrom>::Error: Debug, + proto::Vote: From, + proto::Proposal: From, +{ + pub fn new(ctx: Ctx, params: Params, network: Net, timers_config: timers::Config) -> Self { + let driver = Driver::new( + ctx.clone(), + params.start_height, + params.proposer_selector.clone(), + params.validator_set.clone(), + params.address.clone(), + params.threshold_params, + ); + + let (timers, timeout_elapsed) = Timers::new(timers_config); + + Self { + ctx, + driver, + params, + network, + timers, + timeout_elapsed, + } + } + + pub async fn run(mut self) { + let height = self.driver.height(); + let mut input = Some(Input::NewRound(height, Round::new(0))); + + loop { + if let Some(input) = input.take() { + let outputs = self.driver.process(input).unwrap(); + + for output in outputs { + self.process_output(output).await; + } + } + + tokio::select! { + Some(timeout) = self.timeout_elapsed.recv() => { + let height = self.driver.height(); + let round = self.driver.round(); + + info!("{timeout:?} elapsed at height {height} and round {round}"); + + input = Some(Input::TimeoutElapsed(timeout)); + } + Some((peer_id, msg)) = self.network.recv() => { + info!("Received message from peer {peer_id}: {msg:?}"); + + match msg { + NetworkMsg::Vote(signed_vote) => { + let signed_vote = SignedVote::::try_from(signed_vote).unwrap(); + // self.ctx.verify_signed_vote(signed_vote); + input = Some(Input::Vote(signed_vote.vote)); + } + NetworkMsg::Proposal(proposal) => { + let signed_proposal = SignedProposal::::try_from(proposal).unwrap(); + let validity = Validity::Valid; // self.ctx.verify_proposal(proposal); + input = Some(Input::Proposal(signed_proposal.proposal, validity)); + } + + #[cfg(test)] + NetworkMsg::Dummy(_) => unreachable!() + } + } + } + } + } + + pub async fn process_output(&mut self, output: Output) { + match output { + Output::NewRound(height, round) => { + info!("New round at height {height}: {round}"); + } + Output::Propose(proposal) => { + info!( + "Proposing value {:?} at round {}", + proposal.value(), + proposal.round() + ); + + let signed_proposal = self.ctx.sign_proposal(proposal); + let proto = proto::SignedProposal::from(signed_proposal); + self.network.broadcast_proposal(proto).await; + } + Output::Vote(vote) => { + info!( + "Voting for value {:?} at round {}", + vote.value(), + vote.round() + ); + + let signed_vote = self.ctx.sign_vote(vote); + let proto = proto::SignedVote::from(signed_vote); + self.network.broadcast_vote(proto).await; + } + Output::Decide(round, value) => { + info!("Decided on value {value:?} at round {round}"); + } + Output::ScheduleTimeout(timeout) => { + info!("Scheduling {:?} at round {}", timeout.step, timeout.round); + + self.timers.schedule_timeout(timeout).await + } + Output::GetValue(height, round, timeout) => { + info!("Requesting value at height {height} and round {round}"); + info!("Scheduling {:?} at round {}", timeout.step, timeout.round); + + self.timers.schedule_timeout(timeout).await; + } + } + } +} diff --git a/code/node/src/timers.rs b/code/node/src/timers.rs new file mode 100644 index 000000000..644b0676e --- /dev/null +++ b/code/node/src/timers.rs @@ -0,0 +1,103 @@ +use std::collections::HashMap; +use std::time::Duration; + +use malachite_common::{Timeout, TimeoutStep}; +use tokio::sync::mpsc; +use tokio::task::JoinHandle; + +pub struct Config { + pub propose_timeout: Duration, + pub prevote_timeout: Duration, + pub precommit_timeout: Duration, +} + +pub struct Timers { + config: Config, + timeouts: HashMap>, + + timeout_elapsed: mpsc::Sender, +} + +impl Timers { + pub fn new(config: Config) -> (Self, mpsc::Receiver) { + let (tx_timeout_elapsed, rx_timeout_elapsed) = mpsc::channel(100); + + let timers = Self { + config, + timeouts: HashMap::new(), + timeout_elapsed: tx_timeout_elapsed, + }; + + (timers, rx_timeout_elapsed) + } + + pub fn reset(&mut self) { + for (_, handle) in self.timeouts.drain() { + handle.abort(); + } + } + + pub async fn schedule_timeout(&mut self, timeout: Timeout) { + let tx = self.timeout_elapsed.clone(); + let duration = self.timeout_duration(&timeout); + + let handle = tokio::spawn(async move { + tokio::time::sleep(duration).await; + tx.send(timeout).await.unwrap(); + }); + + self.timeouts.insert(timeout, handle); + } + + pub async fn cancel_timeout(&mut self, timeout: &Timeout) { + if let Some(handle) = self.timeouts.remove(timeout) { + handle.abort(); + } + } + + fn timeout_duration(&self, timeout: &Timeout) -> Duration { + match timeout.step { + TimeoutStep::Propose => self.config.propose_timeout, + TimeoutStep::Prevote => self.config.prevote_timeout, + TimeoutStep::Precommit => self.config.precommit_timeout, + } + } +} + +#[cfg(test)] +mod tests { + use malachite_common::Round; + + use super::*; + + #[tokio::test] + async fn test_timers() { + let config = Config { + propose_timeout: Duration::from_millis(100), + prevote_timeout: Duration::from_millis(200), + precommit_timeout: Duration::from_millis(300), + }; + + let (r0, r1, r2) = (Round::new(0), Round::new(1), Round::new(2)); + let (t0, t1, t2) = ( + Timeout::new(r0, TimeoutStep::Propose), + Timeout::new(r1, TimeoutStep::Prevote), + Timeout::new(r2, TimeoutStep::Precommit), + ); + + let (mut timers, mut rx_timeout_elapsed) = Timers::new(config); + + timers.schedule_timeout(t2).await; + timers.schedule_timeout(t1).await; + timers.schedule_timeout(t0).await; + + assert_eq!(rx_timeout_elapsed.recv().await.unwrap(), t0); + + timers.cancel_timeout(&t1).await; + + assert_eq!( + rx_timeout_elapsed.recv().await.unwrap(), + Timeout::new(r2, TimeoutStep::Precommit) + ); + } +} diff --git a/code/proto/src/impls.rs b/code/proto/src/impls.rs index c0579b3b8..edf7d2056 100644 --- a/code/proto/src/impls.rs +++ b/code/proto/src/impls.rs @@ -1,6 +1,6 @@ use core::convert::Infallible; -use malachite_common::{Context, Round, SignedVote, SigningScheme, VoteType}; +use malachite_common::{Context, Round, SignedProposal, SignedVote, SigningScheme, VoteType}; use crate::{self as proto, Error, Protobuf}; @@ -77,3 +77,41 @@ impl From for proto::VoteType { } } } + +impl From> for proto::SignedProposal +where + Ctx::Proposal: Into, +{ + fn from(signed_proposal: SignedProposal) -> proto::SignedProposal { + proto::SignedProposal { + proposal: Some(signed_proposal.proposal.into()), + signature: Ctx::SigningScheme::encode_signature(&signed_proposal.signature), + } + } +} + +impl TryFrom for SignedProposal +where + Ctx::Proposal: TryFrom, +{ + type Error = Error; + + fn try_from(value: proto::SignedProposal) -> Result { + let proposal = value + .proposal + .ok_or_else(|| Error::Other("Missing field `proposal`".to_string()))?; + + Ok(Self { + proposal: Ctx::Proposal::try_from(proposal)?, + signature: Ctx::SigningScheme::decode_signature(&value.signature) + .map_err(|e| Error::Other(format!("Failed to decode signature: {e}")))?, + }) + } +} + +impl Protobuf for SignedProposal +where + Ctx::Proposal: TryFrom + Into, +{ + type Proto = proto::SignedProposal; +} diff --git a/code/proto/src/malachite.proto b/code/proto/src/malachite.proto index 84ec514e7..022e499d9 100644 --- a/code/proto/src/malachite.proto +++ b/code/proto/src/malachite.proto @@ -22,6 +22,11 @@ message Round { int64 round = 1; } +enum VoteType { + PREVOTE = 0; + PRECOMMIT = 1; +} + message Vote { VoteType vote_type = 1; Height height = 2; @@ -42,7 +47,8 @@ message Proposal { Round pol_round = 4; } -enum VoteType { - PREVOTE = 0; - PRECOMMIT = 1; +message SignedProposal { + Proposal proposal = 1; + bytes signature = 2; } + diff --git a/code/test/src/context.rs b/code/test/src/context.rs index 49ab192b1..00d21aba4 100644 --- a/code/test/src/context.rs +++ b/code/test/src/context.rs @@ -1,6 +1,9 @@ +use std::sync::Arc; + use malachite_common::Context; use malachite_common::NilOrVal; use malachite_common::Round; +use malachite_common::SignedProposal; use malachite_common::SignedVote; use crate::address::*; @@ -13,12 +16,14 @@ use crate::vote::*; #[derive(Clone, Debug)] pub struct TestContext { - private_key: PrivateKey, + private_key: Arc, } impl TestContext { pub fn new(private_key: PrivateKey) -> Self { - Self { private_key } + Self { + private_key: Arc::new(private_key), + } } } @@ -45,6 +50,26 @@ impl Context for TestContext { .is_ok() } + fn sign_proposal(&self, proposal: Self::Proposal) -> SignedProposal { + use signature::Signer; + let signature = self.private_key.sign(&proposal.to_bytes()); + SignedProposal::new(proposal, signature) + } + + fn verify_signed_proposal( + &self, + signed_proposal: &SignedProposal, + public_key: &PublicKey, + ) -> bool { + use signature::Verifier; + public_key + .verify( + &signed_proposal.proposal.to_bytes(), + &signed_proposal.signature, + ) + .is_ok() + } + fn new_proposal(height: Height, round: Round, value: Value, pol_round: Round) -> Proposal { Proposal::new(height, round, value, pol_round) } diff --git a/code/test/src/height.rs b/code/test/src/height.rs index 52c6c9daa..e72f153d0 100644 --- a/code/test/src/height.rs +++ b/code/test/src/height.rs @@ -1,3 +1,4 @@ +use core::fmt; use std::convert::Infallible; /// A blockchain height @@ -14,6 +15,12 @@ impl Height { } } +impl fmt::Display for Height { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + impl malachite_common::Height for Height {} impl TryFrom for Height { diff --git a/code/test/src/proposal.rs b/code/test/src/proposal.rs index 3a9cfacf1..855942cc6 100644 --- a/code/test/src/proposal.rs +++ b/code/test/src/proposal.rs @@ -1,4 +1,5 @@ use malachite_common::Round; +use malachite_proto::Protobuf; use crate::{Height, TestContext, Value}; @@ -20,6 +21,10 @@ impl Proposal { pol_round, } } + + pub fn to_bytes(&self) -> Vec { + Protobuf::to_bytes(self).unwrap() + } } impl malachite_common::Proposal for Proposal { diff --git a/code/test/src/vote.rs b/code/test/src/vote.rs index 71ca9e2a2..74a81fab5 100644 --- a/code/test/src/vote.rs +++ b/code/test/src/vote.rs @@ -1,3 +1,4 @@ +use malachite_proto::Protobuf; use signature::Signer; use malachite_common::{NilOrVal, Round, SignedVote, VoteType}; @@ -46,21 +47,7 @@ impl Vote { } pub fn to_bytes(&self) -> Vec { - let vtpe = match self.typ { - VoteType::Prevote => 0, - VoteType::Precommit => 1, - }; - - let mut bytes = vec![vtpe]; - bytes.extend_from_slice(&self.round.as_i64().to_be_bytes()); - bytes.extend_from_slice( - self.value - .as_ref() - .map(|v| v.as_u64().to_be_bytes()) - .value_or_default() - .as_slice(), - ); - bytes + Protobuf::to_bytes(self).unwrap() } pub fn signed(self, private_key: &PrivateKey) -> SignedVote { diff --git a/code/test/tests/driver.rs b/code/test/tests/driver.rs index a8ed5d1ac..9b91ed5ea 100644 --- a/code/test/tests/driver.rs +++ b/code/test/tests/driver.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use malachite_test::utils::{make_validators, FixedProposer, RotateProposer}; use malachite_common::{NilOrVal, Round, Timeout, TimeoutStep}; @@ -34,7 +36,7 @@ fn driver_steps_proposer() { let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); - let sel = FixedProposer::new(my_addr); + let sel = Arc::new(FixedProposer::new(my_addr)); let vs = ValidatorSet::new(vec![v1, v2.clone(), v3.clone()]); let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); @@ -241,7 +243,7 @@ fn driver_steps_proposer_timeout_get_value() { let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); - let sel = FixedProposer::new(my_addr); + let sel = Arc::new(FixedProposer::new(my_addr)); let vs = ValidatorSet::new(vec![v1, v2.clone(), v3.clone()]); let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); @@ -303,7 +305,7 @@ fn driver_steps_not_proposer_valid() { let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); - let sel = FixedProposer::new(v1.address); + let sel = Arc::new(FixedProposer::new(v1.address)); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); @@ -493,7 +495,7 @@ fn driver_steps_not_proposer_invalid() { let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); - let sel = FixedProposer::new(v1.address); + let sel = Arc::new(FixedProposer::new(v1.address)); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); @@ -609,7 +611,7 @@ fn driver_steps_not_proposer_other_height() { let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); - let sel = FixedProposer::new(v1.address); + let sel = Arc::new(FixedProposer::new(v1.address)); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone()]); let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); @@ -662,7 +664,7 @@ fn driver_steps_not_proposer_other_round() { let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); - let sel = FixedProposer::new(v1.address); + let sel = Arc::new(FixedProposer::new(v1.address)); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone()]); let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); @@ -715,7 +717,7 @@ fn driver_steps_not_proposer_timeout_multiple_rounds() { let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); - let sel = FixedProposer::new(v1.address); + let sel = Arc::new(FixedProposer::new(v1.address)); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); @@ -915,7 +917,7 @@ fn driver_steps_no_value_to_propose() { let ctx = TestContext::new(my_sk.clone()); // We are the proposer - let sel = FixedProposer::new(v1.address); + let sel = Arc::new(FixedProposer::new(v1.address)); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); @@ -947,7 +949,7 @@ fn driver_steps_proposer_not_found() { let ctx = TestContext::new(my_sk.clone()); // Proposer is v1, which is not in the validator set - let sel = FixedProposer::new(v1.address); + let sel = Arc::new(FixedProposer::new(v1.address)); let vs = ValidatorSet::new(vec![v2.clone(), v3.clone()]); let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); @@ -968,7 +970,7 @@ fn driver_steps_validator_not_found() { let ctx = TestContext::new(my_sk.clone()); // Proposer is v1 - let sel = FixedProposer::new(v1.address); + let sel = Arc::new(FixedProposer::new(v1.address)); // We omit v2 from the validator set let vs = ValidatorSet::new(vec![v1.clone(), v3.clone()]); @@ -994,7 +996,7 @@ fn driver_steps_validator_not_found() { fn driver_steps_skip_round_skip_threshold() { let value = Value::new(9999); - let sel = RotateProposer; + let sel = Arc::new(RotateProposer); let [(v1, _sk1), (v2, _sk2), (v3, sk3)] = make_validators([1, 1, 1]); @@ -1107,7 +1109,7 @@ fn driver_steps_skip_round_skip_threshold() { fn driver_steps_skip_round_quorum_threshold() { let value = Value::new(9999); - let sel = RotateProposer; + let sel = Arc::new(RotateProposer); let [(v1, _sk1), (v2, _sk2), (v3, sk3)] = make_validators([1, 2, 1]); diff --git a/code/test/tests/driver_extra.rs b/code/test/tests/driver_extra.rs index 417d233fa..f0f575397 100644 --- a/code/test/tests/driver_extra.rs +++ b/code/test/tests/driver_extra.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use malachite_common::Round; use malachite_driver::{Driver, Input, Output, Validity}; use malachite_round::state::State; @@ -77,7 +79,7 @@ fn driver_steps_decide_current_with_no_locked_no_valid() { let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); - let sel = RotateProposer; + let sel = Arc::new(RotateProposer); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); @@ -146,7 +148,7 @@ fn driver_steps_decide_previous_with_no_locked_no_valid() { let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); - let sel = RotateProposer; + let sel = Arc::new(RotateProposer); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); @@ -243,7 +245,7 @@ fn driver_steps_decide_previous_with_locked_and_valid() { let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); - let sel = RotateProposer; + let sel = Arc::new(RotateProposer); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); @@ -368,7 +370,7 @@ fn driver_steps_polka_previous_with_locked() { let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); - let sel = RotateProposer; + let sel = Arc::new(RotateProposer); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); @@ -476,7 +478,7 @@ fn driver_steps_polka_previous_invalid_proposal() { let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); - let sel = RotateProposer; + let sel = Arc::new(RotateProposer); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); @@ -567,7 +569,7 @@ fn driver_steps_polka_previous_new_proposal() { let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); - let sel = RotateProposer; + let sel = Arc::new(RotateProposer); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); @@ -678,7 +680,7 @@ fn driver_steps_polka_previous_with_no_locked() { let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); - let sel = RotateProposer; + let sel = Arc::new(RotateProposer); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); @@ -787,7 +789,7 @@ fn driver_steps_polka_nil_and_timout_propose() { let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); - let sel = RotateProposer; + let sel = Arc::new(RotateProposer); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); @@ -851,7 +853,7 @@ fn driver_steps_polka_value_then_proposal() { let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); - let sel = RotateProposer; + let sel = Arc::new(RotateProposer); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); @@ -918,7 +920,7 @@ fn driver_steps_polka_any_then_proposal_other() { let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); - let sel = RotateProposer; + let sel = Arc::new(RotateProposer); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); From 6436ff6c703fed2740d808b79ad074d20db1ce6b Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 21 Feb 2024 14:31:09 +0100 Subject: [PATCH 15/50] Simpler design for Protobuf encoding --- code/node/src/network/msg.rs | 82 ++++++++++---------------- code/node/src/node.rs | 25 +++----- code/proto/src/impls.rs | 108 +++++++++++++---------------------- code/proto/src/lib.rs | 45 +++++++++------ code/test/src/address.rs | 24 ++++---- code/test/src/height.rs | 21 +++---- code/test/src/proposal.rs | 56 ++++++++++-------- code/test/src/value.rs | 77 +++++++++++-------------- code/test/src/vote.rs | 61 ++++++++++---------- 9 files changed, 221 insertions(+), 278 deletions(-) diff --git a/code/node/src/network/msg.rs b/code/node/src/network/msg.rs index 09eae90eb..128f4e932 100644 --- a/code/node/src/network/msg.rs +++ b/code/node/src/network/msg.rs @@ -26,72 +26,52 @@ impl Msg { const DUMMY_TYPE_URL: &'static str = "malachite.Dummy"; } -impl From for Any { - fn from(msg: Msg) -> Self { - match msg { - Msg::Vote(vote) => Any { - type_url: SignedVote::type_url(), - value: vote.encode_to_vec(), - }, - Msg::Proposal(proposal) => Any { - type_url: SignedProposal::type_url(), - value: proposal.encode_to_vec(), - }, - - #[cfg(test)] - Msg::Dummy(value) => Any { - type_url: Msg::DUMMY_TYPE_URL.to_string(), - value: value.to_be_bytes().to_vec(), - }, - } - } -} - -impl TryFrom for Msg { - type Error = ProtoError; +impl Protobuf for Msg { + type Proto = Any; - fn try_from(any: Any) -> Result { - if any.type_url == SignedVote::type_url() { - let vote = SignedVote::decode(any.value.as_slice())?; + fn from_proto(proto: Self::Proto) -> Result { + if proto.type_url == SignedVote::type_url() { + let vote = SignedVote::decode(proto.value.as_slice())?; Ok(Msg::Vote(vote)) - } else if any.type_url == SignedProposal::type_url() { - let proposal = SignedProposal::decode(any.value.as_slice())?; + } else if proto.type_url == SignedProposal::type_url() { + let proposal = SignedProposal::decode(proto.value.as_slice())?; Ok(Msg::Proposal(proposal)) - } else if cfg!(test) && any.type_url == Msg::DUMMY_TYPE_URL { + } else if cfg!(test) && proto.type_url == Msg::DUMMY_TYPE_URL { #[cfg(test)] { - let value = u64::from_be_bytes(any.value.try_into().unwrap()); + let value = u64::from_be_bytes(proto.value.try_into().unwrap()); Ok(Msg::Dummy(value)) } #[cfg(not(test))] { - Err(malachite_proto::Error::Other(format!( - "unknown message type: {}", - Msg::DUMMY_TYPE_URL - ))) + Err(ProtoError::UnknownMessageType { + type_url: Msg::DUMMY_TYPE_URL.to_string(), + }) } } else { - Err(ProtoError::Other(format!( - "unknown message type: {}", - any.type_url - ))) + Err(ProtoError::UnknownMessageType { + type_url: proto.type_url, + }) } } -} - -impl Protobuf for Msg { - type Proto = Any; - fn from_bytes(bytes: &[u8]) -> Result - where - Self: Sized, - { - let any = Any::decode(bytes)?; - Self::try_from(any) - } + fn to_proto(&self) -> Result { + Ok(match self { + Msg::Vote(vote) => Any { + type_url: SignedVote::type_url(), + value: vote.encode_to_vec(), + }, + Msg::Proposal(proposal) => Any { + type_url: SignedProposal::type_url(), + value: proposal.encode_to_vec(), + }, - fn into_bytes(self) -> Result, ProtoError> { - Ok(Any::from(self).encode_to_vec()) + #[cfg(test)] + Msg::Dummy(value) => Any { + type_url: Msg::DUMMY_TYPE_URL.to_string(), + value: value.to_be_bytes().to_vec(), + }, + }) } } diff --git a/code/node/src/node.rs b/code/node/src/node.rs index a127eeb0d..02cb1b3c1 100644 --- a/code/node/src/node.rs +++ b/code/node/src/node.rs @@ -1,6 +1,6 @@ #![allow(dead_code)] -use std::fmt::{Debug, Display}; +use std::fmt::Display; use std::sync::Arc; use tokio::sync::mpsc; @@ -8,7 +8,7 @@ use tracing::info; use malachite_common::{Context, Proposal, Round, SignedProposal, SignedVote, Timeout, Vote}; use malachite_driver::{Driver, Input, Output, ProposerSelector, Validity}; -use malachite_proto as proto; +use malachite_proto::{self as proto, Protobuf}; use malachite_vote::ThresholdParams; use crate::network::Msg as NetworkMsg; @@ -40,17 +40,8 @@ where Ctx: Context, Net: Network, Ctx::Height: Display, - // TODO: Avoid having to specify these bounds - Ctx::Vote: TryFrom, - >::Error: Debug, - Ctx::Proposal: TryFrom, - >::Error: Debug, - SignedVote: TryFrom, - as TryFrom>::Error: Debug, - SignedProposal: TryFrom, - as TryFrom>::Error: Debug, - proto::Vote: From, - proto::Proposal: From, + Ctx::Vote: Protobuf, + Ctx::Proposal: Protobuf, { pub fn new(ctx: Ctx, params: Params, network: Net, timers_config: timers::Config) -> Self { let driver = Driver::new( @@ -101,12 +92,12 @@ where match msg { NetworkMsg::Vote(signed_vote) => { - let signed_vote = SignedVote::::try_from(signed_vote).unwrap(); + let signed_vote = SignedVote::::from_proto(signed_vote).unwrap(); // self.ctx.verify_signed_vote(signed_vote); input = Some(Input::Vote(signed_vote.vote)); } NetworkMsg::Proposal(proposal) => { - let signed_proposal = SignedProposal::::try_from(proposal).unwrap(); + let signed_proposal = SignedProposal::::from_proto(proposal).unwrap(); let validity = Validity::Valid; // self.ctx.verify_proposal(proposal); input = Some(Input::Proposal(signed_proposal.proposal, validity)); } @@ -132,7 +123,7 @@ where ); let signed_proposal = self.ctx.sign_proposal(proposal); - let proto = proto::SignedProposal::from(signed_proposal); + let proto = signed_proposal.to_proto().unwrap(); self.network.broadcast_proposal(proto).await; } Output::Vote(vote) => { @@ -143,7 +134,7 @@ where ); let signed_vote = self.ctx.sign_vote(vote); - let proto = proto::SignedVote::from(signed_vote); + let proto = signed_vote.to_proto().unwrap(); self.network.broadcast_vote(proto).await; } Output::Decide(round, value) => { diff --git a/code/proto/src/impls.rs b/code/proto/src/impls.rs index edf7d2056..b0a9061b5 100644 --- a/code/proto/src/impls.rs +++ b/code/proto/src/impls.rs @@ -1,70 +1,52 @@ -use core::convert::Infallible; - use malachite_common::{Context, Round, SignedProposal, SignedVote, SigningScheme, VoteType}; use crate::{self as proto, Error, Protobuf}; -impl TryFrom for Round { - type Error = Infallible; +impl Protobuf for Round { + type Proto = proto::Round; - fn try_from(proto: proto::Round) -> Result { - Ok(Self::new(proto.round)) + fn from_proto(proto: Self::Proto) -> Result { + Ok(Round::new(proto.round)) } -} -impl From for proto::Round { - fn from(round: Round) -> proto::Round { - proto::Round { - round: round.as_i64(), - } - } -} - -impl From> for proto::SignedVote -where - Ctx::Vote: Into, -{ - fn from(signed_vote: SignedVote) -> proto::SignedVote { - proto::SignedVote { - vote: Some(signed_vote.vote.into()), - signature: Ctx::SigningScheme::encode_signature(&signed_vote.signature), - } + fn to_proto(&self) -> Result { + Ok(proto::Round { + round: self.as_i64(), + }) } } -impl TryFrom for SignedVote +impl Protobuf for SignedVote where - Ctx::Vote: TryFrom, + Ctx::Vote: Protobuf, { - type Error = Error; + type Proto = proto::SignedVote; - fn try_from(value: proto::SignedVote) -> Result { - let vote = value + fn from_proto(proto: Self::Proto) -> Result { + let vote = proto .vote - .ok_or_else(|| Error::Other("Missing field `vote`".to_string()))?; + .ok_or_else(|| Error::missing_field::("vote"))?; Ok(Self { - vote: Ctx::Vote::try_from(vote)?, - signature: Ctx::SigningScheme::decode_signature(&value.signature) + vote: Ctx::Vote::from_proto(vote)?, + signature: Ctx::SigningScheme::decode_signature(&proto.signature) .map_err(|e| Error::Other(format!("Failed to decode signature: {e}")))?, }) } -} -impl Protobuf for SignedVote -where - Ctx::Vote: TryFrom + Into, -{ - type Proto = proto::SignedVote; + fn to_proto(&self) -> Result { + Ok(proto::SignedVote { + vote: Some(self.vote.to_proto()?), + signature: Ctx::SigningScheme::encode_signature(&self.signature), + }) + } } -impl TryFrom for VoteType { - type Error = Infallible; - - fn try_from(vote_type: proto::VoteType) -> Result { +impl From for VoteType { + fn from(vote_type: proto::VoteType) -> Self { match vote_type { - proto::VoteType::Prevote => Ok(VoteType::Prevote), - proto::VoteType::Precommit => Ok(VoteType::Precommit), + proto::VoteType::Prevote => VoteType::Prevote, + proto::VoteType::Precommit => VoteType::Precommit, } } } @@ -78,40 +60,28 @@ impl From for proto::VoteType { } } -impl From> for proto::SignedProposal -where - Ctx::Proposal: Into, -{ - fn from(signed_proposal: SignedProposal) -> proto::SignedProposal { - proto::SignedProposal { - proposal: Some(signed_proposal.proposal.into()), - signature: Ctx::SigningScheme::encode_signature(&signed_proposal.signature), - } - } -} - -impl TryFrom for SignedProposal +impl Protobuf for SignedProposal where - Ctx::Proposal: TryFrom, + Ctx::Proposal: Protobuf, { - type Error = Error; + type Proto = proto::SignedProposal; - fn try_from(value: proto::SignedProposal) -> Result { - let proposal = value + fn from_proto(proto: Self::Proto) -> Result { + let proposal = proto .proposal .ok_or_else(|| Error::Other("Missing field `proposal`".to_string()))?; Ok(Self { - proposal: Ctx::Proposal::try_from(proposal)?, - signature: Ctx::SigningScheme::decode_signature(&value.signature) + proposal: Ctx::Proposal::from_proto(proposal)?, + signature: Ctx::SigningScheme::decode_signature(&proto.signature) .map_err(|e| Error::Other(format!("Failed to decode signature: {e}")))?, }) } -} -impl Protobuf for SignedProposal -where - Ctx::Proposal: TryFrom + Into, -{ - type Proto = proto::SignedProposal; + fn to_proto(&self) -> Result { + Ok(proto::SignedProposal { + proposal: Some(self.proposal.to_proto()?), + signature: Ctx::SigningScheme::encode_signature(&self.signature), + }) + } } diff --git a/code/proto/src/lib.rs b/code/proto/src/lib.rs index 42dcad570..7100a2db8 100644 --- a/code/proto/src/lib.rs +++ b/code/proto/src/lib.rs @@ -16,10 +16,26 @@ pub enum Error { #[error("Failed to encode Protobuf message")] Encode(#[from] EncodeError), + #[error("Unable to decode Protobuf message `{type_url}`: missing field `{field}`")] + MissingField { + type_url: String, + field: &'static str, + }, + + #[error("Unknown message type: `{type_url}`")] + UnknownMessageType { type_url: String }, + #[error("{0}")] Other(String), } +impl Error { + pub fn missing_field(field: &'static str) -> Self { + let type_url = N::full_name(); + Self::MissingField { type_url, field } + } +} + impl From for Error { fn from(s: String) -> Self { Self::Other(s) @@ -32,30 +48,21 @@ impl From for Error { } } -pub trait Protobuf -where - Self: Sized + TryFrom, - Error: From, -{ - type Proto: Message + Default + From; +pub trait Protobuf: Sized { + type Proto: Message + Default; + + fn from_proto(proto: Self::Proto) -> Result; + + fn to_proto(&self) -> Result; fn from_bytes(bytes: &[u8]) -> Result { let proto = Self::Proto::decode(bytes)?; - let result = Self::try_from(proto)?; + let result = Self::from_proto(proto)?; Ok(result) } - fn into_bytes(self) -> Result, Error> { - let proto = Self::Proto::from(self); - let mut bytes = Vec::with_capacity(proto.encoded_len()); - proto.encode(&mut bytes)?; - Ok(bytes) - } - - fn to_bytes(&self) -> Result, Error> - where - Self: Clone, - { - Protobuf::into_bytes(self.clone()) + fn to_bytes(&self) -> Result, Error> { + let proto = self.to_proto()?; + Ok(proto.encode_to_vec()) } } diff --git a/code/test/src/address.rs b/code/test/src/address.rs index b58a2eec1..56f589ec2 100644 --- a/code/test/src/address.rs +++ b/code/test/src/address.rs @@ -1,5 +1,7 @@ use core::fmt; +use malachite_proto as proto; + use crate::signing::PublicKey; #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -34,12 +36,12 @@ impl fmt::Display for Address { impl malachite_common::Address for Address {} -impl TryFrom for Address { - type Error = malachite_proto::Error; +impl malachite_proto::Protobuf for Address { + type Proto = proto::Address; - fn try_from(proto: malachite_proto::Address) -> Result { + fn from_proto(proto: Self::Proto) -> Result { if proto.value.len() != Self::LENGTH { - return Err(malachite_proto::Error::Other(format!( + return Err(proto::Error::Other(format!( "Invalid address length: expected {}, got {}", Self::LENGTH, proto.value.len() @@ -50,16 +52,10 @@ impl TryFrom for Address { address.copy_from_slice(&proto.value); Ok(Self(address)) } -} -impl From
for malachite_proto::Address { - fn from(address: Address) -> Self { - Self { - value: address.0.to_vec(), - } + fn to_proto(&self) -> Result { + Ok(proto::Address { + value: self.0.to_vec(), + }) } } - -impl malachite_proto::Protobuf for Address { - type Proto = malachite_proto::Address; -} diff --git a/code/test/src/height.rs b/code/test/src/height.rs index e72f153d0..644028b48 100644 --- a/code/test/src/height.rs +++ b/code/test/src/height.rs @@ -1,5 +1,6 @@ use core::fmt; -use std::convert::Infallible; + +use malachite_proto as proto; /// A blockchain height #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] @@ -23,20 +24,14 @@ impl fmt::Display for Height { impl malachite_common::Height for Height {} -impl TryFrom for Height { - type Error = Infallible; +impl proto::Protobuf for Height { + type Proto = proto::Height; - fn try_from(height: malachite_proto::Height) -> Result { - Ok(Self(height.value)) + fn from_proto(proto: Self::Proto) -> Result { + Ok(Self(proto.value)) } -} -impl From for malachite_proto::Height { - fn from(height: Height) -> malachite_proto::Height { - malachite_proto::Height { value: height.0 } + fn to_proto(&self) -> Result { + Ok(proto::Height { value: self.0 }) } } - -impl malachite_proto::Protobuf for Height { - type Proto = malachite_proto::Height; -} diff --git a/code/test/src/proposal.rs b/code/test/src/proposal.rs index 855942cc6..222470eb0 100644 --- a/code/test/src/proposal.rs +++ b/code/test/src/proposal.rs @@ -1,5 +1,5 @@ use malachite_common::Round; -use malachite_proto::Protobuf; +use malachite_proto::{self as proto}; use crate::{Height, TestContext, Value}; @@ -23,7 +23,7 @@ impl Proposal { } pub fn to_bytes(&self) -> Vec { - Protobuf::to_bytes(self).unwrap() + proto::Protobuf::to_bytes(self).unwrap() } } @@ -45,30 +45,40 @@ impl malachite_common::Proposal for Proposal { } } -impl TryFrom for Proposal { - type Error = String; +impl proto::Protobuf for Proposal { + type Proto = malachite_proto::Proposal; - fn try_from(proposal: malachite_proto::Proposal) -> Result { - Ok(Self { - height: proposal.height.unwrap().try_into().unwrap(), // infallible - round: proposal.round.unwrap().try_into().unwrap(), // infallible - value: proposal.value.unwrap().try_into().unwrap(), // FIXME - pol_round: proposal.pol_round.unwrap().try_into().unwrap(), // infallible + fn to_proto(&self) -> Result { + Ok(proto::Proposal { + height: Some(self.height.to_proto()?), + round: Some(self.round.to_proto()?), + value: Some(self.value.to_proto()?), + pol_round: Some(self.pol_round.to_proto()?), }) } -} -impl From for malachite_proto::Proposal { - fn from(proposal: Proposal) -> malachite_proto::Proposal { - malachite_proto::Proposal { - height: Some(proposal.height.into()), - round: Some(proposal.round.into()), - value: Some(proposal.value.into()), - pol_round: Some(proposal.pol_round.into()), - } + fn from_proto(proto: Self::Proto) -> Result { + Ok(Self { + height: Height::from_proto( + proto + .height + .ok_or_else(|| proto::Error::missing_field::("height"))?, + )?, + round: Round::from_proto( + proto + .round + .ok_or_else(|| proto::Error::missing_field::("round"))?, + )?, + value: Value::from_proto( + proto + .value + .ok_or_else(|| proto::Error::missing_field::("value"))?, + )?, + pol_round: Round::from_proto( + proto + .pol_round + .ok_or_else(|| proto::Error::missing_field::("pol_round"))?, + )?, + }) } } - -impl malachite_proto::Protobuf for Proposal { - type Proto = malachite_proto::Proposal; -} diff --git a/code/test/src/value.rs b/code/test/src/value.rs index 679575326..fac05ef74 100644 --- a/code/test/src/value.rs +++ b/code/test/src/value.rs @@ -1,3 +1,5 @@ +use malachite_proto::{self as proto}; + #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Copy)] pub struct ValueId(u64); @@ -17,30 +19,25 @@ impl From for ValueId { } } -impl TryFrom for ValueId { - type Error = String; - - fn try_from(proto: malachite_proto::ValueId) -> Result { - match proto.value { - Some(bytes) => { - let bytes = <[u8; 8]>::try_from(bytes).unwrap(); // FIXME - Ok(ValueId::new(u64::from_be_bytes(bytes))) - } - None => Err("ValueId not present".to_string()), - } - } -} +impl proto::Protobuf for ValueId { + type Proto = proto::ValueId; + + fn from_proto(proto: Self::Proto) -> Result { + let bytes = proto + .value + .ok_or_else(|| proto::Error::missing_field::("value"))?; -impl From for malachite_proto::ValueId { - fn from(value: ValueId) -> malachite_proto::ValueId { - malachite_proto::ValueId { - value: Some(value.0.to_be_bytes().to_vec()), - } + let bytes = <[u8; 8]>::try_from(bytes) + .map_err(|_| proto::Error::Other("Invalid value length".to_string()))?; + + Ok(ValueId::new(u64::from_be_bytes(bytes))) } -} -impl malachite_proto::Protobuf for ValueId { - type Proto = malachite_proto::ValueId; + fn to_proto(&self) -> Result { + Ok(proto::ValueId { + value: Some(self.0.to_be_bytes().to_vec()), + }) + } } /// The value to decide on @@ -69,29 +66,23 @@ impl malachite_common::Value for Value { } } -impl TryFrom for Value { - type Error = String; - - fn try_from(proto: malachite_proto::Value) -> Result { - match proto.value { - Some(bytes) => { - let bytes = <[u8; 8]>::try_from(bytes).unwrap(); // FIXME - let value = u64::from_be_bytes(bytes); - Ok(Value::new(value)) - } - None => Err("Value not present".to_string()), - } - } -} +impl proto::Protobuf for Value { + type Proto = proto::Value; + + fn from_proto(proto: Self::Proto) -> Result { + let bytes = proto + .value + .ok_or_else(|| proto::Error::missing_field::("value"))?; -impl From for malachite_proto::Value { - fn from(value: Value) -> malachite_proto::Value { - malachite_proto::Value { - value: Some(value.0.to_be_bytes().to_vec()), - } + let bytes = <[u8; 8]>::try_from(bytes) + .map_err(|_| proto::Error::Other("Invalid value length".to_string()))?; + + Ok(Value::new(u64::from_be_bytes(bytes))) } -} -impl malachite_proto::Protobuf for Value { - type Proto = malachite_proto::Value; + fn to_proto(&self) -> Result { + Ok(proto::Value { + value: Some(self.0.to_be_bytes().to_vec()), + }) + } } diff --git a/code/test/src/vote.rs b/code/test/src/vote.rs index 74a81fab5..32f1c233c 100644 --- a/code/test/src/vote.rs +++ b/code/test/src/vote.rs @@ -1,7 +1,7 @@ -use malachite_proto::Protobuf; use signature::Signer; use malachite_common::{NilOrVal, Round, SignedVote, VoteType}; +use malachite_proto::{self as proto}; use crate::{Address, Height, PrivateKey, TestContext, ValueId}; @@ -47,7 +47,7 @@ impl Vote { } pub fn to_bytes(&self) -> Vec { - Protobuf::to_bytes(self).unwrap() + proto::Protobuf::to_bytes(self).unwrap() } pub fn signed(self, private_key: &PrivateKey) -> SignedVote { @@ -86,41 +86,44 @@ impl malachite_common::Vote for Vote { } } -impl TryFrom for Vote { - type Error = String; +impl proto::Protobuf for Vote { + type Proto = proto::Vote; - fn try_from(vote: malachite_proto::Vote) -> Result { + fn from_proto(proto: Self::Proto) -> Result { Ok(Self { - typ: malachite_proto::VoteType::try_from(vote.vote_type) - .unwrap() - .try_into() - .unwrap(), // infallible - height: vote.height.unwrap().try_into().unwrap(), // infallible - round: vote.round.unwrap().try_into().unwrap(), // infallible - value: match vote.value { - Some(value) => NilOrVal::Val(value.try_into().unwrap()), // FIXME + typ: VoteType::from(proto.vote_type()), + height: Height::from_proto( + proto + .height + .ok_or_else(|| proto::Error::missing_field::("height"))?, + )?, + round: Round::from_proto( + proto + .round + .ok_or_else(|| proto::Error::missing_field::("round"))?, + )?, + value: match proto.value { + Some(value) => NilOrVal::Val(ValueId::from_proto(value)?), None => NilOrVal::Nil, }, - validator_address: vote.validator_address.unwrap().try_into().unwrap(), // FIXME + validator_address: Address::from_proto( + proto.validator_address.ok_or_else(|| { + proto::Error::missing_field::("validator_address") + })?, + )?, }) } -} -impl From for malachite_proto::Vote { - fn from(vote: Vote) -> malachite_proto::Vote { - malachite_proto::Vote { - vote_type: i32::from(malachite_proto::VoteType::from(vote.typ)), - height: Some(vote.height.into()), - round: Some(vote.round.into()), - value: match vote.value { + fn to_proto(&self) -> Result { + Ok(proto::Vote { + vote_type: proto::VoteType::from(self.typ).into(), + height: Some(self.height.to_proto()?), + round: Some(self.round.to_proto()?), + value: match &self.value { NilOrVal::Nil => None, - NilOrVal::Val(v) => Some(v.into()), + NilOrVal::Val(v) => Some(v.to_proto()?), }, - validator_address: Some(vote.validator_address.into()), - } + validator_address: Some(self.validator_address.to_proto()?), + }) } } - -impl malachite_proto::Protobuf for Vote { - type Proto = malachite_proto::Vote; -} From 9cacb53cda77cedd296e6b94653483855075a0a9 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 21 Feb 2024 15:02:50 +0100 Subject: [PATCH 16/50] Automatically clean elapsed timeouts --- code/common/src/timeout.rs | 8 ++-- code/node/src/timers.rs | 95 ++++++++++++++++++++++++++++++-------- 2 files changed, 79 insertions(+), 24 deletions(-) diff --git a/code/common/src/timeout.rs b/code/common/src/timeout.rs index 7f8efb718..387069010 100644 --- a/code/common/src/timeout.rs +++ b/code/common/src/timeout.rs @@ -25,22 +25,22 @@ pub struct Timeout { impl Timeout { /// Create a new timeout for the given round and step. - pub fn new(round: Round, step: TimeoutStep) -> Self { + pub const fn new(round: Round, step: TimeoutStep) -> Self { Self { round, step } } /// Create a new timeout for the propose step of the given round. - pub fn propose(round: Round) -> Self { + pub const fn propose(round: Round) -> Self { Self::new(round, TimeoutStep::Propose) } /// Create a new timeout for the prevote step of the given round. - pub fn prevote(round: Round) -> Self { + pub const fn prevote(round: Round) -> Self { Self::new(round, TimeoutStep::Prevote) } /// Create a new timeout for the precommit step of the given round. - pub fn precommit(round: Round) -> Self { + pub const fn precommit(round: Round) -> Self { Self::new(round, TimeoutStep::Precommit) } } diff --git a/code/node/src/timers.rs b/code/node/src/timers.rs index 644b0676e..534b8e543 100644 --- a/code/node/src/timers.rs +++ b/code/node/src/timers.rs @@ -1,10 +1,13 @@ use std::collections::HashMap; +use std::sync::Arc; use std::time::Duration; use malachite_common::{Timeout, TimeoutStep}; use tokio::sync::mpsc; +use tokio::sync::Mutex; // TODO: Use parking_lot instead? use tokio::task::JoinHandle; +#[derive(Copy, Clone, Debug)] pub struct Config { pub propose_timeout: Duration, pub prevote_timeout: Duration, @@ -13,8 +16,7 @@ pub struct Config { pub struct Timers { config: Config, - timeouts: HashMap>, - + timeouts: Arc>>>, timeout_elapsed: mpsc::Sender, } @@ -24,33 +26,39 @@ impl Timers { let timers = Self { config, - timeouts: HashMap::new(), + timeouts: Arc::new(Mutex::new(HashMap::new())), timeout_elapsed: tx_timeout_elapsed, }; (timers, rx_timeout_elapsed) } - pub fn reset(&mut self) { - for (_, handle) in self.timeouts.drain() { + pub async fn reset(&mut self) { + for (_, handle) in self.timeouts.lock().await.drain() { handle.abort(); } } + pub async fn scheduled(&self) -> usize { + self.timeouts.lock().await.len() + } + pub async fn schedule_timeout(&mut self, timeout: Timeout) { let tx = self.timeout_elapsed.clone(); let duration = self.timeout_duration(&timeout); + let timeouts = self.timeouts.clone(); let handle = tokio::spawn(async move { tokio::time::sleep(duration).await; + timeouts.lock().await.remove(&timeout); tx.send(timeout).await.unwrap(); }); - self.timeouts.insert(timeout, handle); + self.timeouts.lock().await.insert(timeout, handle); } pub async fn cancel_timeout(&mut self, timeout: &Timeout) { - if let Some(handle) = self.timeouts.remove(timeout) { + if let Some(handle) = self.timeouts.lock().await.remove(timeout) { handle.abort(); } } @@ -65,39 +73,86 @@ impl Timers { } #[cfg(test)] +#[allow(non_upper_case_globals)] mod tests { use malachite_common::Round; use super::*; - #[tokio::test] - async fn test_timers() { - let config = Config { - propose_timeout: Duration::from_millis(100), - prevote_timeout: Duration::from_millis(200), - precommit_timeout: Duration::from_millis(300), - }; + const config: Config = Config { + propose_timeout: Duration::from_millis(50), + prevote_timeout: Duration::from_millis(100), + precommit_timeout: Duration::from_millis(150), + }; + const fn timeouts() -> (Timeout, Timeout, Timeout) { let (r0, r1, r2) = (Round::new(0), Round::new(1), Round::new(2)); - let (t0, t1, t2) = ( + + ( Timeout::new(r0, TimeoutStep::Propose), Timeout::new(r1, TimeoutStep::Prevote), Timeout::new(r2, TimeoutStep::Precommit), - ); + ) + } + + #[tokio::test] + async fn timers_no_cancel() { + let (t0, t1, t2) = timeouts(); + + let (mut timers, mut rx_timeout_elapsed) = Timers::new(config); + + timers.schedule_timeout(t1).await; + timers.schedule_timeout(t0).await; + timers.schedule_timeout(t2).await; + assert_eq!(timers.scheduled().await, 3); + + assert_eq!(rx_timeout_elapsed.recv().await.unwrap(), t0); + assert_eq!(timers.scheduled().await, 2); + assert_eq!(rx_timeout_elapsed.recv().await.unwrap(), t1); + assert_eq!(timers.scheduled().await, 1); + assert_eq!(rx_timeout_elapsed.recv().await.unwrap(), t2); + assert_eq!(timers.scheduled().await, 0); + } + + #[tokio::test] + async fn timers_cancel_first() { + let (t0, t1, t2) = timeouts(); + + let (mut timers, mut rx_timeout_elapsed) = Timers::new(config); + + timers.schedule_timeout(t0).await; + timers.schedule_timeout(t1).await; + timers.schedule_timeout(t2).await; + assert_eq!(timers.scheduled().await, 3); + + timers.cancel_timeout(&t0).await; + assert_eq!(timers.scheduled().await, 2); + + assert_eq!(rx_timeout_elapsed.recv().await.unwrap(), t1); + assert_eq!(timers.scheduled().await, 1); + + assert_eq!(rx_timeout_elapsed.recv().await.unwrap(), t2); + assert_eq!(timers.scheduled().await, 0); + } + + #[tokio::test] + async fn timers_cancel_middle() { + let (t0, t1, t2) = timeouts(); let (mut timers, mut rx_timeout_elapsed) = Timers::new(config); timers.schedule_timeout(t2).await; timers.schedule_timeout(t1).await; timers.schedule_timeout(t0).await; + assert_eq!(timers.scheduled().await, 3); assert_eq!(rx_timeout_elapsed.recv().await.unwrap(), t0); + assert_eq!(timers.scheduled().await, 2); timers.cancel_timeout(&t1).await; + assert_eq!(timers.scheduled().await, 1); - assert_eq!( - rx_timeout_elapsed.recv().await.unwrap(), - Timeout::new(r2, TimeoutStep::Precommit) - ); + assert_eq!(rx_timeout_elapsed.recv().await.unwrap(), t2); + assert_eq!(timers.scheduled().await, 0); } } From 47581baf3949a7f4334479ad03dd9ff4f7bdff18 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 21 Feb 2024 16:47:07 +0100 Subject: [PATCH 17/50] Add basic node CLI with support for 3 peers --- code/Cargo.toml | 44 +++++++------- code/common/src/timeout.rs | 8 +++ code/node/Cargo.toml | 23 +++++--- code/node/bin/cli.rs | 17 ++++++ code/node/bin/config.rs | 74 ++++++++++++++++++++++++ code/node/bin/main.rs | 93 ++++++++++++++++++++++++++++++ code/node/peers.toml | 14 +++++ code/node/src/network.rs | 16 +++++ code/node/src/network/broadcast.rs | 78 ++++++++++++++++++------- code/node/src/node.rs | 7 ++- code/test/src/signing.rs | 4 ++ 11 files changed, 326 insertions(+), 52 deletions(-) create mode 100644 code/node/bin/cli.rs create mode 100644 code/node/bin/config.rs create mode 100644 code/node/bin/main.rs create mode 100644 code/node/peers.toml diff --git a/code/Cargo.toml b/code/Cargo.toml index 5c82a254b..02f37f745 100644 --- a/code/Cargo.toml +++ b/code/Cargo.toml @@ -29,24 +29,26 @@ malachite-round = { version = "0.1.0", path = "round" } malachite-test = { version = "0.1.0", path = "test" } malachite-vote = { version = "0.1.0", path = "vote" } -derive-where = "1.2.7" -ed25519-consensus = "2.1.0" -futures = "0.3" -glob = "0.3.0" -itf = "0.2.2" -num-bigint = "0.4.4" -num-traits = "0.2.17" -pretty_assertions = "1.4" -prost = "0.12.3" -prost-types = "0.12.3" -prost-build = "0.12.3" -rand = { version = "0.8.5", features = ["std_rng"] } -serde = "1.0" -serde_json = "1.0" -serde_with = "3.4" -sha2 = "0.10.8" -signature = "2.1.0" -thiserror = "1.0" -tokio = "1.35.1" -tokio-stream = "0.1" -tracing = "0.1.40" +derive-where = "1.2.7" +ed25519-consensus = "2.1.0" +futures = "0.3" +glob = "0.3.0" +itf = "0.2.2" +num-bigint = "0.4.4" +num-traits = "0.2.17" +pretty_assertions = "1.4" +prost = "0.12.3" +prost-types = "0.12.3" +prost-build = "0.12.3" +rand = "0.8.5" +serde = "1.0" +serde_json = "1.0" +serde_with = "3.4" +sha2 = "0.10.8" +signature = "2.1.0" +thiserror = "1.0" +tokio = "1.35.1" +tokio-stream = "0.1" +toml = "0.8.10" +tracing = "0.1.40" +tracing-subscriber = "0.3.18" diff --git a/code/common/src/timeout.rs b/code/common/src/timeout.rs index 387069010..05f80985d 100644 --- a/code/common/src/timeout.rs +++ b/code/common/src/timeout.rs @@ -1,3 +1,5 @@ +use core::fmt; + use crate::Round; /// The round step for which the timeout is for. @@ -44,3 +46,9 @@ impl Timeout { Self::new(round, TimeoutStep::Precommit) } } + +impl fmt::Display for Timeout { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}Timeout({})", self.step, self.round) + } +} diff --git a/code/node/Cargo.toml b/code/node/Cargo.toml index 47a5ad156..f77cb3c05 100644 --- a/code/node/Cargo.toml +++ b/code/node/Cargo.toml @@ -8,20 +8,29 @@ repository.workspace = true license.workspace = true publish.workspace = true +[[bin]] +name = "malachite-node" +path = "bin/main.rs" + [dependencies] malachite-common.workspace = true malachite-driver.workspace = true malachite-round.workspace = true malachite-vote.workspace = true malachite-proto.workspace = true +malachite-test.workspace = true -derive-where = { workspace = true } -futures = { workspace = true } -tokio = { workspace = true, features = ["full"] } -tokio-stream = { workspace = true } -prost = { workspace = true } -prost-types = { workspace = true } -tracing = { workspace = true } +ed25519-consensus = { workspace = true, features = ["serde"] } +derive-where = { workspace = true } +futures = { workspace = true } +tokio = { workspace = true, features = ["full"] } +tokio-stream = { workspace = true } +prost = { workspace = true } +prost-types = { workspace = true } +serde = { workspace = true, features = ["derive"] } +toml = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["fmt"] } [dev-dependencies] malachite-test.workspace = true diff --git a/code/node/bin/cli.rs b/code/node/bin/cli.rs new file mode 100644 index 000000000..919dab5c2 --- /dev/null +++ b/code/node/bin/cli.rs @@ -0,0 +1,17 @@ +use malachite_node::network::PeerId; + +pub struct Cli { + pub peer_id: PeerId, +} + +impl Cli { + pub fn from_env() -> Self { + let peer_id = std::env::args() + .nth(1) + .expect("Usage: node ") + .parse() + .expect("Error: Invalid PEER_ID"); + + Self { peer_id } + } +} diff --git a/code/node/bin/config.rs b/code/node/bin/config.rs new file mode 100644 index 000000000..c5600fc0f --- /dev/null +++ b/code/node/bin/config.rs @@ -0,0 +1,74 @@ +use std::net::SocketAddr; + +use serde::{Deserialize, Serialize}; + +use malachite_node::network::{broadcast::PeerInfo, PeerId}; +use malachite_test::PublicKey; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + pub peers: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PeerConfig { + #[serde(with = "de::peer_id")] + pub id: PeerId, + pub addr: SocketAddr, + #[serde(with = "de::public_key")] + pub public_key: PublicKey, +} + +impl PeerConfig { + pub fn peer_info(&self) -> PeerInfo { + PeerInfo { + id: self.id.clone(), + addr: self.addr, + } + } +} + +pub mod de { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub mod peer_id { + use super::*; + + use malachite_node::network::PeerId; + + pub fn serialize(id: &PeerId, s: S) -> Result + where + S: Serializer, + { + s.serialize_str(&id.to_string()) + } + + pub fn deserialize<'de, D>(d: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(d)?; + Ok(PeerId::new(s)) + } + } + + pub mod public_key { + use super::*; + + use malachite_test::PublicKey; + + pub fn serialize(key: &PublicKey, s: S) -> Result + where + S: Serializer, + { + key.inner().serialize(s) + } + + pub fn deserialize<'de, D>(d: D) -> Result + where + D: Deserializer<'de>, + { + ed25519_consensus::VerificationKey::deserialize(d).map(PublicKey::new) + } + } +} diff --git a/code/node/bin/main.rs b/code/node/bin/main.rs new file mode 100644 index 000000000..5715f54db --- /dev/null +++ b/code/node/bin/main.rs @@ -0,0 +1,93 @@ +use std::sync::Arc; +use std::time::Duration; + +use malachite_node::network::broadcast; +use malachite_node::network::broadcast::PeerInfo; +use malachite_node::node::{Node, Params}; +use malachite_node::timers; +use malachite_test::utils::{make_validators, RotateProposer}; +use malachite_test::{Address, Height, PrivateKey, TestContext, ValidatorSet}; +use tracing::info; + +mod cli; +use cli::Cli; +mod config; +use config::{Config, PeerConfig}; + +#[tokio::main(flavor = "current_thread")] +pub async fn main() { + tracing_subscriber::fmt::init(); + + let args = Cli::from_env(); + + // Validators keys are deterministic and match the ones in the config file + let vs = make_validators([2, 3, 2]); + + let config = std::fs::read_to_string("node/peers.toml").expect("Error: missing peers.toml"); + let config = toml::from_str::(&config).expect("Error: invalid peers.toml"); + + let peer_config = config + .peers + .iter() + .find(|p| p.id == args.peer_id) + .expect("Error: invalid peer id"); + + let (my_sk, my_addr) = vs + .iter() + .find(|(v, _)| v.public_key == peer_config.public_key) + .map(|(v, pk)| (pk.clone(), v.address)) + .expect("Error: invalid peer id"); + + let (vs, _): (Vec<_>, Vec<_>) = vs.into_iter().unzip(); + + let peer_info = peer_config.peer_info(); + let vs = ValidatorSet::new(vs); + + let node = make_node(vs, my_sk, my_addr, peer_info, &config.peers).await; + + info!("[{}] Starting...", args.peer_id); + + node.run().await; +} + +pub async fn make_node( + vs: ValidatorSet, + pk: PrivateKey, + addr: Address, + peer_info: PeerInfo, + peers: &[PeerConfig], +) -> Node { + let height = Height::new(1); + let ctx = TestContext::new(pk); + let sel = Arc::new(RotateProposer); + + let params = Params { + start_height: height, + proposer_selector: sel, + validator_set: vs, + address: addr, + threshold_params: Default::default(), + }; + + let timers_config = timers::Config { + propose_timeout: Duration::from_secs(3), + prevote_timeout: Duration::from_secs(1), + precommit_timeout: Duration::from_secs(1), + }; + + let network = broadcast::Peer::new(peer_info.clone()); + let handle = network.run().await; + + let timeout = Some(Duration::from_secs(10)); + + let to_connect = peers + .iter() + .filter(|p| p.id != peer_info.id) + .map(|p| p.peer_info()); + + for peer in to_connect { + handle.connect_to_peer(peer, timeout).await; + } + + Node::new(ctx, params, handle, timers_config) +} diff --git a/code/node/peers.toml b/code/node/peers.toml new file mode 100644 index 000000000..cc7490523 --- /dev/null +++ b/code/node/peers.toml @@ -0,0 +1,14 @@ +[[peers]] +id = "node1" +addr = "127.0.0.1:1235" +public_key = [104, 30, 170, 163, 78, 73, 30, 108, 131, 53, 171, 201, 234, 146, 176, 36, 239, 82, 235, 145, 68, 44, 163, 184, 69, 152, 199, 154, 121, 243, 27, 117] + +[[peers]] +id = "node2" +addr = "127.0.0.1:1236" +public_key = [24, 109, 62, 237, 160, 46, 173, 95, 187, 173, 116, 78, 237, 21, 141, 149, 140, 79, 127, 72, 86, 26, 62, 102, 203, 30, 233, 104, 85, 173, 92, 25] + +[[peers]] +id = "node3" +addr = "127.0.0.1:1237" +public_key = [49, 171, 95, 10, 5, 226, 72, 164, 147, 3, 48, 71, 200, 88, 31, 0, 121, 180, 85, 143, 94, 156, 113, 175, 97, 106, 231, 109, 128, 203, 219, 7] diff --git a/code/node/src/network.rs b/code/node/src/network.rs index 9fdc55837..145fec308 100644 --- a/code/node/src/network.rs +++ b/code/node/src/network.rs @@ -1,4 +1,6 @@ use core::fmt; +use std::convert::Infallible; +use std::str::FromStr; pub mod broadcast; mod msg; @@ -10,12 +12,26 @@ pub use self::msg::Msg; #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct PeerId(String); +impl PeerId { + pub fn new(id: impl ToString) -> Self { + Self(id.to_string()) + } +} + impl fmt::Display for PeerId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(&self.0, f) } } +impl FromStr for PeerId { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(Self(s.to_string())) + } +} + #[allow(async_fn_in_trait)] pub trait Network { async fn recv(&mut self) -> Option<(PeerId, Msg)>; diff --git a/code/node/src/network/broadcast.rs b/code/node/src/network/broadcast.rs index 2131b6486..505a9b9a8 100644 --- a/code/node/src/network/broadcast.rs +++ b/code/node/src/network/broadcast.rs @@ -1,23 +1,25 @@ use core::fmt; use std::fmt::Debug; use std::net::SocketAddr; +use std::time::{Duration, Instant}; use futures::channel::oneshot; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::{broadcast, mpsc}; +use tracing::{error, info, warn}; use super::{Msg, Network, PeerId}; pub enum PeerEvent { - ConnectToPeer(PeerInfo, oneshot::Sender<()>), + ConnectToPeer(PeerInfo, Option, oneshot::Sender<()>), Broadcast(Msg, oneshot::Sender<()>), } impl Debug for PeerEvent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - PeerEvent::ConnectToPeer(peer_info, _) => { + PeerEvent::ConnectToPeer(peer_info, _, _) => { write!(f, "ConnectToPeer({peer_info:?})") } PeerEvent::Broadcast(msg, _) => { @@ -65,12 +67,19 @@ impl Peer { tokio::spawn(async move { while let Some(event) = rx_peer_event.recv().await { match event { - PeerEvent::ConnectToPeer(peer_info, done) => { - connect_to_peer(id.clone(), peer_info, done, &tx_broadcast_to_peers).await; + PeerEvent::ConnectToPeer(peer_info, timeout, done) => { + connect_to_peer( + id.clone(), + peer_info, + timeout, + done, + &tx_broadcast_to_peers, + ) + .await; } PeerEvent::Broadcast(msg, done) => { - println!("[{id}] Broadcasting message: {msg:?}"); + info!("[{id}] Broadcasting message: {msg:?}"); tx_broadcast_to_peers.send((id.clone(), msg)).unwrap(); done.send(()).unwrap(); } @@ -91,12 +100,39 @@ impl Peer { async fn connect_to_peer( id: PeerId, peer_info: PeerInfo, + timeout: Option, done: oneshot::Sender<()>, per_peer_tx: &broadcast::Sender<(PeerId, Msg)>, ) { - println!("[{id}] Connecting to {peer_info}..."); + info!("[{id}] Connecting to {peer_info}..."); + + let mut stream = if let Some(timeout) = timeout { + let start = Instant::now(); + + loop { + match TcpStream::connect(peer_info.addr).await { + Ok(stream) => break stream, + Err(e) => warn!("[{id}] Failed to connect to {peer_info}: {e}"), + } + + if start.elapsed() >= timeout { + error!("[{id}] Connection to {peer_info} timed out"); + return; + } + + warn!("[{id}] Retrying in 1 second..."); + tokio::time::sleep(Duration::from_secs(1)).await; + } + } else { + match TcpStream::connect(peer_info.addr).await { + Ok(stream) => stream, + Err(e) => { + error!("[{id}] Failed to connect to {peer_info}: {e}"); + return; + } + } + }; - let mut stream = TcpStream::connect(peer_info.addr).await.unwrap(); done.send(()).unwrap(); let mut per_peer_rx = per_peer_tx.subscribe(); @@ -110,7 +146,7 @@ async fn connect_to_peer( continue; } - println!("[{id}] Sending message to {peer_info}: {msg:?}"); + info!("[{id}] Sending message to {peer_info}: {msg:?}"); Frame::Msg(msg).write(&mut stream).await.unwrap(); } }); @@ -123,20 +159,20 @@ async fn listen( tx_received: mpsc::Sender<(PeerId, Msg)>, ) -> ! { let listener = TcpListener::bind(addr).await.unwrap(); - println!("[{id}] Listening on {addr}..."); + info!("[{id}] Listening on {addr}..."); tx_spawned.send(()).unwrap(); loop { let (mut socket, _) = listener.accept().await.unwrap(); - println!( + info!( "[{id}] Accepted connection from {peer}...", peer = socket.peer_addr().unwrap() ); let Frame::PeerId(peer_id) = Frame::read(&mut socket).await.unwrap() else { - eprintln!("[{id}] Peer did not send its ID"); + error!("[{id}] Peer did not send its ID"); continue; }; @@ -146,11 +182,11 @@ async fn listen( tokio::spawn(async move { loop { let Frame::Msg(msg) = Frame::read(&mut socket).await.unwrap() else { - eprintln!("[{id}] Peer did not send a message"); + error!("[{id}] Peer did not send a message"); return; }; - println!( + info!( "[{id}] Received message from {peer_id} ({addr}): {msg:?}", addr = socket.peer_addr().unwrap(), ); @@ -244,11 +280,11 @@ impl Handle { rx_done.await.unwrap(); } - pub async fn connect_to_peer(&self, peer_info: PeerInfo) { + pub async fn connect_to_peer(&self, peer_info: PeerInfo, timeout: Option) { let (tx_done, rx_done) = oneshot::channel(); self.tx_peer_event - .send(PeerEvent::ConnectToPeer(peer_info, tx_done)) + .send(PeerEvent::ConnectToPeer(peer_info, timeout, tx_done)) .await .unwrap(); @@ -302,14 +338,14 @@ mod tests { let mut handle2 = peer2.run().await; let mut handle3 = peer3.run().await; - handle1.connect_to_peer(peer2_info.clone()).await; - handle1.connect_to_peer(peer3_info.clone()).await; + handle1.connect_to_peer(peer2_info.clone(), None).await; + handle1.connect_to_peer(peer3_info.clone(), None).await; - handle2.connect_to_peer(peer1_info.clone()).await; - handle2.connect_to_peer(peer3_info.clone()).await; + handle2.connect_to_peer(peer1_info.clone(), None).await; + handle2.connect_to_peer(peer3_info.clone(), None).await; - handle3.connect_to_peer(peer1_info.clone()).await; - handle3.connect_to_peer(peer2_info.clone()).await; + handle3.connect_to_peer(peer1_info.clone(), None).await; + handle3.connect_to_peer(peer2_info.clone(), None).await; handle1.broadcast(Msg::Dummy(1)).await; handle1.broadcast(Msg::Dummy(2)).await; diff --git a/code/node/src/node.rs b/code/node/src/node.rs index 02cb1b3c1..254ff4f32 100644 --- a/code/node/src/node.rs +++ b/code/node/src/node.rs @@ -83,10 +83,11 @@ where let height = self.driver.height(); let round = self.driver.round(); - info!("{timeout:?} elapsed at height {height} and round {round}"); + info!("{timeout} elapsed at height {height} and round {round}"); input = Some(Input::TimeoutElapsed(timeout)); } + Some((peer_id, msg)) = self.network.recv() => { info!("Received message from peer {peer_id}: {msg:?}"); @@ -141,13 +142,13 @@ where info!("Decided on value {value:?} at round {round}"); } Output::ScheduleTimeout(timeout) => { - info!("Scheduling {:?} at round {}", timeout.step, timeout.round); + info!("Scheduling {timeout}"); self.timers.schedule_timeout(timeout).await } Output::GetValue(height, round, timeout) => { info!("Requesting value at height {height} and round {round}"); - info!("Scheduling {:?} at round {}", timeout.step, timeout.round); + info!("Scheduling {timeout}"); self.timers.schedule_timeout(timeout).await; } diff --git a/code/test/src/signing.rs b/code/test/src/signing.rs index 25b329ae7..94b52af82 100644 --- a/code/test/src/signing.rs +++ b/code/test/src/signing.rs @@ -81,6 +81,10 @@ impl PublicKey { hasher.update(self.0.as_bytes()); hasher.finalize().into() } + + pub fn inner(&self) -> &ed25519_consensus::VerificationKey { + &self.0 + } } impl Verifier for PublicKey { From 7c284aa54e341374e501619ea1ee24e301ff12d0 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 21 Feb 2024 17:15:27 +0100 Subject: [PATCH 18/50] Allow node to propose a single fixed value --- code/node/bin/main.rs | 14 ++--- code/node/src/network/broadcast.rs | 8 +-- code/node/src/node.rs | 91 +++++++++++++++++++++++++----- 3 files changed, 88 insertions(+), 25 deletions(-) diff --git a/code/node/bin/main.rs b/code/node/bin/main.rs index 5715f54db..b8ada57dd 100644 --- a/code/node/bin/main.rs +++ b/code/node/bin/main.rs @@ -5,8 +5,8 @@ use malachite_node::network::broadcast; use malachite_node::network::broadcast::PeerInfo; use malachite_node::node::{Node, Params}; use malachite_node::timers; -use malachite_test::utils::{make_validators, RotateProposer}; -use malachite_test::{Address, Height, PrivateKey, TestContext, ValidatorSet}; +use malachite_test::utils::{make_validators, FixedProposer}; +use malachite_test::{Address, Height, PrivateKey, TestContext, ValidatorSet, Value}; use tracing::info; mod cli; @@ -59,7 +59,7 @@ pub async fn make_node( ) -> Node { let height = Height::new(1); let ctx = TestContext::new(pk); - let sel = Arc::new(RotateProposer); + let sel = Arc::new(FixedProposer::new(vs.validators[0].address)); let params = Params { start_height: height, @@ -70,9 +70,9 @@ pub async fn make_node( }; let timers_config = timers::Config { - propose_timeout: Duration::from_secs(3), - prevote_timeout: Duration::from_secs(1), - precommit_timeout: Duration::from_secs(1), + propose_timeout: Duration::from_secs(10), + prevote_timeout: Duration::from_secs(5), + precommit_timeout: Duration::from_secs(5), }; let network = broadcast::Peer::new(peer_info.clone()); @@ -89,5 +89,5 @@ pub async fn make_node( handle.connect_to_peer(peer, timeout).await; } - Node::new(ctx, params, handle, timers_config) + Node::new(ctx, params, handle, Value::new(42), timers_config) } diff --git a/code/node/src/network/broadcast.rs b/code/node/src/network/broadcast.rs index 505a9b9a8..3c9eea639 100644 --- a/code/node/src/network/broadcast.rs +++ b/code/node/src/network/broadcast.rs @@ -7,7 +7,7 @@ use futures::channel::oneshot; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::{broadcast, mpsc}; -use tracing::{error, info, warn}; +use tracing::{debug, error, info, warn}; use super::{Msg, Network, PeerId}; @@ -79,7 +79,7 @@ impl Peer { } PeerEvent::Broadcast(msg, done) => { - info!("[{id}] Broadcasting message: {msg:?}"); + debug!("[{id}] Broadcasting message: {msg:?}"); tx_broadcast_to_peers.send((id.clone(), msg)).unwrap(); done.send(()).unwrap(); } @@ -146,7 +146,7 @@ async fn connect_to_peer( continue; } - info!("[{id}] Sending message to {peer_info}: {msg:?}"); + debug!("[{id}] Sending message to {peer_info}: {msg:?}"); Frame::Msg(msg).write(&mut stream).await.unwrap(); } }); @@ -186,7 +186,7 @@ async fn listen( return; }; - info!( + debug!( "[{id}] Received message from {peer_id} ({addr}): {msg:?}", addr = socket.peer_addr().unwrap(), ); diff --git a/code/node/src/node.rs b/code/node/src/node.rs index 254ff4f32..2c9ca3572 100644 --- a/code/node/src/node.rs +++ b/code/node/src/node.rs @@ -2,11 +2,14 @@ use std::fmt::Display; use std::sync::Arc; +use std::time::Duration; use tokio::sync::mpsc; use tracing::info; -use malachite_common::{Context, Proposal, Round, SignedProposal, SignedVote, Timeout, Vote}; +use malachite_common::{ + Context, Proposal, Round, SignedProposal, SignedVote, Timeout, Vote, VoteType, +}; use malachite_driver::{Driver, Input, Output, ProposerSelector, Validity}; use malachite_proto::{self as proto, Protobuf}; use malachite_vote::ThresholdParams; @@ -33,6 +36,7 @@ where network: Net, timers: Timers, timeout_elapsed: mpsc::Receiver, + value: Ctx::Value, } impl Node @@ -43,7 +47,13 @@ where Ctx::Vote: Protobuf, Ctx::Proposal: Protobuf, { - pub fn new(ctx: Ctx, params: Params, network: Net, timers_config: timers::Config) -> Self { + pub fn new( + ctx: Ctx, + params: Params, + network: Net, + value: Ctx::Value, + timers_config: timers::Config, + ) -> Self { let driver = Driver::new( ctx.clone(), params.start_height, @@ -62,30 +72,62 @@ where network, timers, timeout_elapsed, + value, } } pub async fn run(mut self) { let height = self.driver.height(); - let mut input = Some(Input::NewRound(height, Round::new(0))); + + let (tx_input, mut rx_input) = tokio::sync::mpsc::unbounded_channel(); + tx_input + .send(Input::NewRound(height, Round::new(0))) + .unwrap(); loop { - if let Some(input) = input.take() { - let outputs = self.driver.process(input).unwrap(); + tokio::select! { + Some(input) = rx_input.recv() =>{ + dbg!(&input); + match &input { + Input::NewRound(_, _) => { + self.timers.reset().await; + } + Input::ProposeValue(round, _) => { + self.timers.cancel_timeout(&Timeout::propose(*round)).await; + }, + Input::Proposal(proposal, _) => { + let round = Proposal::::round(proposal); + self.timers.cancel_timeout(&Timeout::propose(round)).await; + } + Input::Vote(vote) => { + // FIXME: Only cancel the timeout when we have a quorum of votes + let round = Vote::::round(vote); + let timeout = match Vote::::vote_type(vote) { + VoteType::Prevote => Timeout::prevote(round), + VoteType::Precommit => Timeout::precommit(round), + }; + + self.timers.cancel_timeout(&timeout).await; + } + Input::TimeoutElapsed(_) => (), + } - for output in outputs { - self.process_output(output).await; + let outputs = self.driver.process(input).unwrap(); + + for output in outputs { + if let Some(input) = self.process_output(output).await { + tx_input.send(input).unwrap(); + } + } } - } - tokio::select! { Some(timeout) = self.timeout_elapsed.recv() => { let height = self.driver.height(); let round = self.driver.round(); info!("{timeout} elapsed at height {height} and round {round}"); - input = Some(Input::TimeoutElapsed(timeout)); + tx_input.send(Input::TimeoutElapsed(timeout)).unwrap(); } Some((peer_id, msg)) = self.network.recv() => { @@ -95,12 +137,12 @@ where NetworkMsg::Vote(signed_vote) => { let signed_vote = SignedVote::::from_proto(signed_vote).unwrap(); // self.ctx.verify_signed_vote(signed_vote); - input = Some(Input::Vote(signed_vote.vote)); + tx_input.send(Input::Vote(signed_vote.vote)).unwrap(); } NetworkMsg::Proposal(proposal) => { let signed_proposal = SignedProposal::::from_proto(proposal).unwrap(); let validity = Validity::Valid; // self.ctx.verify_proposal(proposal); - input = Some(Input::Proposal(signed_proposal.proposal, validity)); + tx_input.send(Input::Proposal(signed_proposal.proposal, validity)).unwrap(); } #[cfg(test)] @@ -111,11 +153,14 @@ where } } - pub async fn process_output(&mut self, output: Output) { + #[must_use] + pub async fn process_output(&mut self, output: Output) -> Option> { match output { Output::NewRound(height, round) => { info!("New round at height {height}: {round}"); + Some(Input::NewRound(height, round)) } + Output::Propose(proposal) => { info!( "Proposing value {:?} at round {}", @@ -126,7 +171,9 @@ where let signed_proposal = self.ctx.sign_proposal(proposal); let proto = signed_proposal.to_proto().unwrap(); self.network.broadcast_proposal(proto).await; + None } + Output::Vote(vote) => { info!( "Voting for value {:?} at round {}", @@ -137,21 +184,37 @@ where let signed_vote = self.ctx.sign_vote(vote); let proto = signed_vote.to_proto().unwrap(); self.network.broadcast_vote(proto).await; + None } + Output::Decide(round, value) => { info!("Decided on value {value:?} at round {round}"); + None } + Output::ScheduleTimeout(timeout) => { info!("Scheduling {timeout}"); - self.timers.schedule_timeout(timeout).await + self.timers.schedule_timeout(timeout).await; + None } + Output::GetValue(height, round, timeout) => { info!("Requesting value at height {height} and round {round}"); info!("Scheduling {timeout}"); self.timers.schedule_timeout(timeout).await; + + let value = self.get_value().await; + Some(Input::ProposeValue(round, value)) } } } + + pub async fn get_value(&self) -> Ctx::Value { + // Simulate waiting for a value to be assembled + tokio::time::sleep(Duration::from_secs(1)).await; + + self.value.clone() + } } From 0099fd6475e841ccff1bef6c298d3b888f305cbb Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 21 Feb 2024 17:29:07 +0100 Subject: [PATCH 19/50] Cleanup --- code/driver/src/input.rs | 4 +++- code/node/src/node.rs | 6 +----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/code/driver/src/input.rs b/code/driver/src/input.rs index 6d25d5248..b8eaeef82 100644 --- a/code/driver/src/input.rs +++ b/code/driver/src/input.rs @@ -1,9 +1,11 @@ use malachite_common::{Context, Round, Timeout}; +use derive_where::derive_where; + use crate::Validity; /// Events that can be received by the [`Driver`](crate::Driver). -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive_where(Clone, Debug, PartialEq, Eq)] pub enum Input where Ctx: Context, diff --git a/code/node/src/node.rs b/code/node/src/node.rs index 2c9ca3572..c2c2fb9a8 100644 --- a/code/node/src/node.rs +++ b/code/node/src/node.rs @@ -87,7 +87,6 @@ where loop { tokio::select! { Some(input) = rx_input.recv() =>{ - dbg!(&input); match &input { Input::NewRound(_, _) => { self.timers.reset().await; @@ -199,11 +198,8 @@ where None } - Output::GetValue(height, round, timeout) => { + Output::GetValue(height, round, _timeout) => { info!("Requesting value at height {height} and round {round}"); - info!("Scheduling {timeout}"); - - self.timers.schedule_timeout(timeout).await; let value = self.get_value().await; Some(Input::ProposeValue(round, value)) From 647eb67cd4b80fdd9aef1b0434227fb82ff761b8 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 21 Feb 2024 17:42:54 +0100 Subject: [PATCH 20/50] Feed back proposal and vote into the driver --- code/node/src/node.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/code/node/src/node.rs b/code/node/src/node.rs index c2c2fb9a8..5564fef48 100644 --- a/code/node/src/node.rs +++ b/code/node/src/node.rs @@ -99,7 +99,7 @@ where self.timers.cancel_timeout(&Timeout::propose(round)).await; } Input::Vote(vote) => { - // FIXME: Only cancel the timeout when we have a quorum of votes + // FIXME: Only cancel the timeout when we have received enough* votes let round = Vote::::round(vote); let timeout = match Vote::::vote_type(vote) { VoteType::Prevote => Timeout::prevote(round), @@ -170,7 +170,7 @@ where let signed_proposal = self.ctx.sign_proposal(proposal); let proto = signed_proposal.to_proto().unwrap(); self.network.broadcast_proposal(proto).await; - None + Some(Input::Proposal(signed_proposal.proposal, Validity::Valid)) } Output::Vote(vote) => { @@ -183,7 +183,7 @@ where let signed_vote = self.ctx.sign_vote(vote); let proto = signed_vote.to_proto().unwrap(); self.network.broadcast_vote(proto).await; - None + Some(Input::Vote(signed_vote.vote)) } Output::Decide(round, value) => { From a7a6715ef20ab882b8363cd93c9215065cf7da84 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 21 Feb 2024 17:56:16 +0100 Subject: [PATCH 21/50] Cleanup --- code/node/src/node.rs | 165 ++++++++++++++++++++++++++---------------- 1 file changed, 101 insertions(+), 64 deletions(-) diff --git a/code/node/src/node.rs b/code/node/src/node.rs index 5564fef48..60bad04b7 100644 --- a/code/node/src/node.rs +++ b/code/node/src/node.rs @@ -16,6 +16,7 @@ use malachite_vote::ThresholdParams; use crate::network::Msg as NetworkMsg; use crate::network::Network; +use crate::network::PeerId; use crate::timers::{self, Timers}; pub struct Params { @@ -26,6 +27,8 @@ pub struct Params { pub threshold_params: ThresholdParams, } +type TxInput = mpsc::UnboundedSender>; + pub struct Node where Ctx: Context, @@ -86,78 +89,103 @@ where loop { tokio::select! { - Some(input) = rx_input.recv() =>{ - match &input { - Input::NewRound(_, _) => { - self.timers.reset().await; - } - Input::ProposeValue(round, _) => { - self.timers.cancel_timeout(&Timeout::propose(*round)).await; - }, - Input::Proposal(proposal, _) => { - let round = Proposal::::round(proposal); - self.timers.cancel_timeout(&Timeout::propose(round)).await; - } - Input::Vote(vote) => { - // FIXME: Only cancel the timeout when we have received enough* votes - let round = Vote::::round(vote); - let timeout = match Vote::::vote_type(vote) { - VoteType::Prevote => Timeout::prevote(round), - VoteType::Precommit => Timeout::precommit(round), - }; - - self.timers.cancel_timeout(&timeout).await; - } - Input::TimeoutElapsed(_) => (), - } - - let outputs = self.driver.process(input).unwrap(); - - for output in outputs { - if let Some(input) = self.process_output(output).await { - tx_input.send(input).unwrap(); - } - } + Some(input) = rx_input.recv() => { + self.process_input(input, &tx_input).await; } Some(timeout) = self.timeout_elapsed.recv() => { - let height = self.driver.height(); - let round = self.driver.round(); - - info!("{timeout} elapsed at height {height} and round {round}"); - - tx_input.send(Input::TimeoutElapsed(timeout)).unwrap(); + self.process_timeout(timeout, &tx_input).await; } Some((peer_id, msg)) = self.network.recv() => { - info!("Received message from peer {peer_id}: {msg:?}"); - - match msg { - NetworkMsg::Vote(signed_vote) => { - let signed_vote = SignedVote::::from_proto(signed_vote).unwrap(); - // self.ctx.verify_signed_vote(signed_vote); - tx_input.send(Input::Vote(signed_vote.vote)).unwrap(); - } - NetworkMsg::Proposal(proposal) => { - let signed_proposal = SignedProposal::::from_proto(proposal).unwrap(); - let validity = Validity::Valid; // self.ctx.verify_proposal(proposal); - tx_input.send(Input::Proposal(signed_proposal.proposal, validity)).unwrap(); - } - - #[cfg(test)] - NetworkMsg::Dummy(_) => unreachable!() - } + self.process_network_msg(peer_id, msg, &tx_input).await; } } } } + pub async fn process_input(&mut self, input: Input, tx_input: &TxInput) { + match &input { + Input::NewRound(_, _) => { + self.timers.reset().await; + } + Input::ProposeValue(round, _) => { + self.timers.cancel_timeout(&Timeout::propose(*round)).await; + } + Input::Proposal(proposal, _) => { + let round = Proposal::::round(proposal); + self.timers.cancel_timeout(&Timeout::propose(round)).await; + } + Input::Vote(vote) => { + // FIXME: Only cancel the timeout when we have received enough* votes + let round = Vote::::round(vote); + let timeout = match Vote::::vote_type(vote) { + VoteType::Prevote => Timeout::prevote(round), + VoteType::Precommit => Timeout::precommit(round), + }; + + self.timers.cancel_timeout(&timeout).await; + } + Input::TimeoutElapsed(_) => (), + } + + let outputs = self.driver.process(input).unwrap(); + + for output in outputs { + match self.process_output(output).await { + Next::None => {} + Next::Input(input) => { + tx_input.send(input).unwrap(); + } + Next::Decided(_, _) => { + return; + } + } + } + } + + pub async fn process_timeout(&mut self, timeout: Timeout, tx_input: &TxInput) { + let height = self.driver.height(); + let round = self.driver.round(); + + info!("{timeout} elapsed at height {height} and round {round}"); + + tx_input.send(Input::TimeoutElapsed(timeout)).unwrap(); + } + + pub async fn process_network_msg( + &mut self, + peer_id: PeerId, + msg: NetworkMsg, + tx_input: &TxInput, + ) { + info!("Received message from peer {peer_id}: {msg:?}"); + + match msg { + NetworkMsg::Vote(signed_vote) => { + let signed_vote = SignedVote::::from_proto(signed_vote).unwrap(); + // self.ctx.verify_signed_vote(signed_vote); + tx_input.send(Input::Vote(signed_vote.vote)).unwrap(); + } + NetworkMsg::Proposal(proposal) => { + let signed_proposal = SignedProposal::::from_proto(proposal).unwrap(); + let validity = Validity::Valid; // self.ctx.verify_proposal(proposal); + tx_input + .send(Input::Proposal(signed_proposal.proposal, validity)) + .unwrap(); + } + + #[cfg(test)] + NetworkMsg::Dummy(_) => unreachable!(), + } + } + #[must_use] - pub async fn process_output(&mut self, output: Output) -> Option> { + pub async fn process_output(&mut self, output: Output) -> Next { match output { Output::NewRound(height, round) => { info!("New round at height {height}: {round}"); - Some(Input::NewRound(height, round)) + Next::Input(Input::NewRound(height, round)) } Output::Propose(proposal) => { @@ -170,7 +198,7 @@ where let signed_proposal = self.ctx.sign_proposal(proposal); let proto = signed_proposal.to_proto().unwrap(); self.network.broadcast_proposal(proto).await; - Some(Input::Proposal(signed_proposal.proposal, Validity::Valid)) + Next::Input(Input::Proposal(signed_proposal.proposal, Validity::Valid)) } Output::Vote(vote) => { @@ -183,26 +211,29 @@ where let signed_vote = self.ctx.sign_vote(vote); let proto = signed_vote.to_proto().unwrap(); self.network.broadcast_vote(proto).await; - Some(Input::Vote(signed_vote.vote)) + Next::Input(Input::Vote(signed_vote.vote)) } Output::Decide(round, value) => { info!("Decided on value {value:?} at round {round}"); - None + self.timers.reset().await; + + // TODO: Wait for `timeout_commit` and start the next height + Next::Decided(round, value) } Output::ScheduleTimeout(timeout) => { info!("Scheduling {timeout}"); - self.timers.schedule_timeout(timeout).await; - None + + Next::None } Output::GetValue(height, round, _timeout) => { info!("Requesting value at height {height} and round {round}"); - let value = self.get_value().await; - Some(Input::ProposeValue(round, value)) + + Next::Input(Input::ProposeValue(round, value)) } } } @@ -214,3 +245,9 @@ where self.value.clone() } } + +pub enum Next { + None, + Input(Input), + Decided(Round, Ctx::Value), +} From c0d3a9390ede1351b49e26f5b56f88f81ea933a3 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 21 Feb 2024 18:07:52 +0100 Subject: [PATCH 22/50] Add a commit timeout --- code/common/src/timeout.rs | 14 +++++++++++--- code/driver/src/driver.rs | 3 +++ code/node/bin/main.rs | 1 + code/node/src/node.rs | 27 +++++++++++++++++++++------ code/node/src/timers.rs | 3 +++ 5 files changed, 39 insertions(+), 9 deletions(-) diff --git a/code/common/src/timeout.rs b/code/common/src/timeout.rs index 05f80985d..2b981efe7 100644 --- a/code/common/src/timeout.rs +++ b/code/common/src/timeout.rs @@ -5,14 +5,17 @@ use crate::Round; /// The round step for which the timeout is for. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub enum TimeoutStep { - /// The timeout is for the propose step. + /// Timeout for the propose step. Propose, - /// The timeout is for the prevote step. + /// Timeout for the prevote step. Prevote, - /// The timeout is for the precommit step. + /// Timeout for the precommit step. Precommit, + + /// Timeout for the commit step. + Commit, } /// A timeout for a round step. @@ -45,6 +48,11 @@ impl Timeout { pub const fn precommit(round: Round) -> Self { Self::new(round, TimeoutStep::Precommit) } + + /// Create a new timeout for the commit step of the given round. + pub const fn commit(round: Round) -> Self { + Self::new(round, TimeoutStep::Commit) + } } impl fmt::Display for Timeout { diff --git a/code/driver/src/driver.rs b/code/driver/src/driver.rs index e093085b8..5719bf437 100644 --- a/code/driver/src/driver.rs +++ b/code/driver/src/driver.rs @@ -250,6 +250,9 @@ where TimeoutStep::Propose => RoundInput::TimeoutPropose, TimeoutStep::Prevote => RoundInput::TimeoutPrevote, TimeoutStep::Precommit => RoundInput::TimeoutPrecommit, + + // The driver never receives a commit timeout, so we can just ignore it. + TimeoutStep::Commit => return Ok(None), }; self.apply_input(timeout.round, input) diff --git a/code/node/bin/main.rs b/code/node/bin/main.rs index b8ada57dd..d76e6c65f 100644 --- a/code/node/bin/main.rs +++ b/code/node/bin/main.rs @@ -73,6 +73,7 @@ pub async fn make_node( propose_timeout: Duration::from_secs(10), prevote_timeout: Duration::from_secs(5), precommit_timeout: Duration::from_secs(5), + commit_timeout: Duration::from_secs(5), }; let network = broadcast::Peer::new(peer_info.clone()); diff --git a/code/node/src/node.rs b/code/node/src/node.rs index 60bad04b7..3b8c35238 100644 --- a/code/node/src/node.rs +++ b/code/node/src/node.rs @@ -4,6 +4,7 @@ use std::fmt::Display; use std::sync::Arc; use std::time::Duration; +use malachite_common::TimeoutStep; use tokio::sync::mpsc; use tracing::info; @@ -40,6 +41,9 @@ where timers: Timers, timeout_elapsed: mpsc::Receiver, value: Ctx::Value, + + // Debug only + stop: bool, } impl Node @@ -76,6 +80,7 @@ where timers, timeout_elapsed, value, + stop: false, } } @@ -88,6 +93,10 @@ where .unwrap(); loop { + if self.stop { + break; + } + tokio::select! { Some(input) = rx_input.recv() => { self.process_input(input, &tx_input).await; @@ -126,6 +135,12 @@ where self.timers.cancel_timeout(&timeout).await; } + Input::TimeoutElapsed(timeout) if timeout.step == TimeoutStep::Commit => { + // Debug only + self.stop = true; + // FIXME: Move to next height + return; + } Input::TimeoutElapsed(_) => (), } @@ -133,12 +148,10 @@ where for output in outputs { match self.process_output(output).await { - Next::None => {} - Next::Input(input) => { - tx_input.send(input).unwrap(); - } - Next::Decided(_, _) => { - return; + Next::None => (), + Next::Input(input) => tx_input.send(input).unwrap(), + Next::Decided(round, _) => { + self.timers.schedule_timeout(Timeout::commit(round)).await } } } @@ -148,6 +161,8 @@ where let height = self.driver.height(); let round = self.driver.round(); + // FIXME: Ensure the timeout is for the current round + info!("{timeout} elapsed at height {height} and round {round}"); tx_input.send(Input::TimeoutElapsed(timeout)).unwrap(); diff --git a/code/node/src/timers.rs b/code/node/src/timers.rs index 534b8e543..f3c19df2f 100644 --- a/code/node/src/timers.rs +++ b/code/node/src/timers.rs @@ -12,6 +12,7 @@ pub struct Config { pub propose_timeout: Duration, pub prevote_timeout: Duration, pub precommit_timeout: Duration, + pub commit_timeout: Duration, } pub struct Timers { @@ -68,6 +69,7 @@ impl Timers { TimeoutStep::Propose => self.config.propose_timeout, TimeoutStep::Prevote => self.config.prevote_timeout, TimeoutStep::Precommit => self.config.precommit_timeout, + TimeoutStep::Commit => self.config.commit_timeout, } } } @@ -83,6 +85,7 @@ mod tests { propose_timeout: Duration::from_millis(50), prevote_timeout: Duration::from_millis(100), precommit_timeout: Duration::from_millis(150), + commit_timeout: Duration::from_millis(200), }; const fn timeouts() -> (Timeout, Timeout, Timeout) { From b1ad611454906d2042fa4d47d18f1ac89c0622d2 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 21 Feb 2024 18:09:14 +0100 Subject: [PATCH 23/50] Only process timeouts for the current round --- code/node/src/node.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/code/node/src/node.rs b/code/node/src/node.rs index 3b8c35238..879ce1ee8 100644 --- a/code/node/src/node.rs +++ b/code/node/src/node.rs @@ -6,6 +6,7 @@ use std::time::Duration; use malachite_common::TimeoutStep; use tokio::sync::mpsc; +use tracing::debug; use tracing::info; use malachite_common::{ @@ -161,7 +162,14 @@ where let height = self.driver.height(); let round = self.driver.round(); - // FIXME: Ensure the timeout is for the current round + if timeout.round != round { + debug!( + "Ignoring timeout for round {} at height {}, current round: {round}", + timeout.round, height + ); + + return; + } info!("{timeout} elapsed at height {height} and round {round}"); From 097aa42f231cf61becfe0af6ac5a16ccd6cdd122 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 21 Feb 2024 18:21:20 +0100 Subject: [PATCH 24/50] Allow consensus to move across multiple heights --- code/common/src/height.rs | 2 ++ code/node/src/node.rs | 46 ++++++++++++++++++++++++--------------- code/test/src/height.rs | 6 ++++- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/code/common/src/height.rs b/code/common/src/height.rs index 5f458c123..9bdcdfedd 100644 --- a/code/common/src/height.rs +++ b/code/common/src/height.rs @@ -9,4 +9,6 @@ pub trait Height where Self: Default + Copy + Clone + Debug + PartialEq + Eq + PartialOrd + Ord, { + /// Increment the height by one. + fn increment(&self) -> Self; } diff --git a/code/node/src/node.rs b/code/node/src/node.rs index 879ce1ee8..9a0611f46 100644 --- a/code/node/src/node.rs +++ b/code/node/src/node.rs @@ -1,24 +1,20 @@ -#![allow(dead_code)] - use std::fmt::Display; use std::sync::Arc; use std::time::Duration; -use malachite_common::TimeoutStep; use tokio::sync::mpsc; -use tracing::debug; -use tracing::info; +use tracing::{debug, info, Instrument}; use malachite_common::{ - Context, Proposal, Round, SignedProposal, SignedVote, Timeout, Vote, VoteType, + Context, Height, Proposal, Round, SignedProposal, SignedVote, Timeout, TimeoutStep, Vote, + VoteType, }; use malachite_driver::{Driver, Input, Output, ProposerSelector, Validity}; use malachite_proto::{self as proto, Protobuf}; use malachite_vote::ThresholdParams; use crate::network::Msg as NetworkMsg; -use crate::network::Network; -use crate::network::PeerId; +use crate::network::{Network, PeerId}; use crate::timers::{self, Timers}; pub struct Params { @@ -42,9 +38,7 @@ where timers: Timers, timeout_elapsed: mpsc::Receiver, value: Ctx::Value, - - // Debug only - stop: bool, + done: bool, } impl Node @@ -81,20 +75,38 @@ where timers, timeout_elapsed, value, - stop: false, + done: false, } } pub async fn run(mut self) { - let height = self.driver.height(); + let mut height = self.params.start_height; + + loop { + let span = tracing::error_span!("node", height = %height); + + self.start_height(height).instrument(span).await; + + height = self.driver.height().increment(); + self.driver = self.driver.move_to_height(height); + + debug_assert_eq!(self.driver.height(), height); + debug_assert_eq!(self.driver.round(), Round::Nil); + } + } + + pub async fn start_height(&mut self, height: Ctx::Height) { + let (tx_input, mut rx_input) = mpsc::unbounded_channel(); - let (tx_input, mut rx_input) = tokio::sync::mpsc::unbounded_channel(); tx_input .send(Input::NewRound(height, Round::new(0))) .unwrap(); loop { - if self.stop { + if self.done { + self.done = false; + self.timers.reset().await; + break; } @@ -137,9 +149,7 @@ where self.timers.cancel_timeout(&timeout).await; } Input::TimeoutElapsed(timeout) if timeout.step == TimeoutStep::Commit => { - // Debug only - self.stop = true; - // FIXME: Move to next height + self.done = true; return; } Input::TimeoutElapsed(_) => (), diff --git a/code/test/src/height.rs b/code/test/src/height.rs index 644028b48..74639eb6e 100644 --- a/code/test/src/height.rs +++ b/code/test/src/height.rs @@ -22,7 +22,11 @@ impl fmt::Display for Height { } } -impl malachite_common::Height for Height {} +impl malachite_common::Height for Height { + fn increment(&self) -> Self { + Self(self.0 + 1) + } +} impl proto::Protobuf for Height { type Proto = proto::Height; From 859e00c171ffd41cde6fd3fd59a8937bfff1bf5a Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 21 Feb 2024 19:44:59 +0100 Subject: [PATCH 25/50] Verify signature of votes and proposals --- code/driver/src/util.rs | 9 +++++ code/node/bin/main.rs | 29 +++++++-------- code/node/{bin => src}/config.rs | 5 +-- code/node/src/lib.rs | 2 ++ code/node/src/node.rs | 25 ++++++++++--- code/node/src/peers.rs | 62 ++++++++++++++++++++++++++++++++ 6 files changed, 111 insertions(+), 21 deletions(-) rename code/node/{bin => src}/config.rs (93%) create mode 100644 code/node/src/peers.rs diff --git a/code/driver/src/util.rs b/code/driver/src/util.rs index 9428d6f2c..3c413a7b2 100644 --- a/code/driver/src/util.rs +++ b/code/driver/src/util.rs @@ -12,4 +12,13 @@ impl Validity { pub fn is_valid(self) -> bool { self == Validity::Valid } + + /// Returns `Valid` if given true, `Invalid` if given false. + pub fn from_valid(valid: bool) -> Self { + if valid { + Validity::Valid + } else { + Validity::Invalid + } + } } diff --git a/code/node/bin/main.rs b/code/node/bin/main.rs index d76e6c65f..0f3856609 100644 --- a/code/node/bin/main.rs +++ b/code/node/bin/main.rs @@ -1,9 +1,11 @@ use std::sync::Arc; use std::time::Duration; +use malachite_node::config::Config; use malachite_node::network::broadcast; use malachite_node::network::broadcast::PeerInfo; use malachite_node::node::{Node, Params}; +use malachite_node::peers::Peers; use malachite_node::timers; use malachite_test::utils::{make_validators, FixedProposer}; use malachite_test::{Address, Height, PrivateKey, TestContext, ValidatorSet, Value}; @@ -11,8 +13,6 @@ use tracing::info; mod cli; use cli::Cli; -mod config; -use config::{Config, PeerConfig}; #[tokio::main(flavor = "current_thread")] pub async fn main() { @@ -43,7 +43,7 @@ pub async fn main() { let peer_info = peer_config.peer_info(); let vs = ValidatorSet::new(vs); - let node = make_node(vs, my_sk, my_addr, peer_info, &config.peers).await; + let node = make_node(vs, my_sk, my_addr, peer_info, config.into()).await; info!("[{}] Starting...", args.peer_id); @@ -51,21 +51,22 @@ pub async fn main() { } pub async fn make_node( - vs: ValidatorSet, - pk: PrivateKey, - addr: Address, + validator_set: ValidatorSet, + private_key: PrivateKey, + address: Address, peer_info: PeerInfo, - peers: &[PeerConfig], + peers: Peers, ) -> Node { - let height = Height::new(1); - let ctx = TestContext::new(pk); - let sel = Arc::new(FixedProposer::new(vs.validators[0].address)); + let start_height = Height::new(1); + let ctx = TestContext::new(private_key); + let proposer_selector = Arc::new(FixedProposer::new(validator_set.validators[0].address)); let params = Params { - start_height: height, - proposer_selector: sel, - validator_set: vs, - address: addr, + start_height, + proposer_selector, + validator_set, + address, + peers: peers.clone(), threshold_params: Default::default(), }; diff --git a/code/node/bin/config.rs b/code/node/src/config.rs similarity index 93% rename from code/node/bin/config.rs rename to code/node/src/config.rs index c5600fc0f..1540cbf88 100644 --- a/code/node/bin/config.rs +++ b/code/node/src/config.rs @@ -2,9 +2,10 @@ use std::net::SocketAddr; use serde::{Deserialize, Serialize}; -use malachite_node::network::{broadcast::PeerInfo, PeerId}; use malachite_test::PublicKey; +use crate::network::{broadcast::PeerInfo, PeerId}; + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Config { pub peers: Vec, @@ -34,7 +35,7 @@ pub mod de { pub mod peer_id { use super::*; - use malachite_node::network::PeerId; + use crate::network::PeerId; pub fn serialize(id: &PeerId, s: S) -> Result where diff --git a/code/node/src/lib.rs b/code/node/src/lib.rs index daa3e115a..2651f1030 100644 --- a/code/node/src/lib.rs +++ b/code/node/src/lib.rs @@ -1,3 +1,5 @@ +pub mod config; pub mod network; pub mod node; +pub mod peers; pub mod timers; diff --git a/code/node/src/node.rs b/code/node/src/node.rs index 9a0611f46..31e33e6c0 100644 --- a/code/node/src/node.rs +++ b/code/node/src/node.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; -use tracing::{debug, info, Instrument}; +use tracing::{debug, info, warn, Instrument}; use malachite_common::{ Context, Height, Proposal, Round, SignedProposal, SignedVote, Timeout, TimeoutStep, Vote, @@ -15,6 +15,7 @@ use malachite_vote::ThresholdParams; use crate::network::Msg as NetworkMsg; use crate::network::{Network, PeerId}; +use crate::peers::Peers; use crate::timers::{self, Timers}; pub struct Params { @@ -23,6 +24,7 @@ pub struct Params { pub validator_set: Ctx::ValidatorSet, pub address: Ctx::Address, pub threshold_params: ThresholdParams, + pub peers: Peers, } type TxInput = mpsc::UnboundedSender>; @@ -197,14 +199,27 @@ where match msg { NetworkMsg::Vote(signed_vote) => { let signed_vote = SignedVote::::from_proto(signed_vote).unwrap(); - // self.ctx.verify_signed_vote(signed_vote); - tx_input.send(Input::Vote(signed_vote.vote)).unwrap(); + let peer = self.params.peers.get(&peer_id).unwrap(); // FIXME + + if self.ctx.verify_signed_vote(&signed_vote, &peer.public_key) { + tx_input.send(Input::Vote(signed_vote.vote)).unwrap(); + } else { + warn!("Invalid vote from peer {peer_id}: {signed_vote:?}"); + } } NetworkMsg::Proposal(proposal) => { let signed_proposal = SignedProposal::::from_proto(proposal).unwrap(); - let validity = Validity::Valid; // self.ctx.verify_proposal(proposal); + let peer = self.params.peers.get(&peer_id).unwrap(); // FIXME + + let valid = self + .ctx + .verify_signed_proposal(&signed_proposal, &peer.public_key); + tx_input - .send(Input::Proposal(signed_proposal.proposal, validity)) + .send(Input::Proposal( + signed_proposal.proposal, + Validity::from_valid(valid), + )) .unwrap(); } diff --git a/code/node/src/peers.rs b/code/node/src/peers.rs new file mode 100644 index 000000000..2022b6b6c --- /dev/null +++ b/code/node/src/peers.rs @@ -0,0 +1,62 @@ +use std::net::SocketAddr; + +use derive_where::derive_where; +use malachite_common::{Context, PublicKey}; +use malachite_test::TestContext; + +use crate::config::{Config, PeerConfig}; +use crate::network::broadcast::PeerInfo; +use crate::network::PeerId; + +#[derive_where(Clone, Debug)] +pub struct Peers { + pub peers: Vec>, +} + +impl Peers { + pub fn get(&self, id: &PeerId) -> Option<&Peer> { + self.peers.iter().find(|p| &p.id == id) + } + + pub fn iter(&self) -> impl Iterator> { + self.peers.iter() + } + + pub fn except<'a>(&'a self, id: &'a PeerId) -> impl Iterator> + 'a { + self.iter().filter(move |p| &p.id != id) + } +} + +impl From for Peers { + fn from(config: Config) -> Self { + Self { + peers: config.peers.into_iter().map(Peer::from).collect(), + } + } +} + +#[derive_where(Clone, Debug)] +pub struct Peer { + pub id: PeerId, + pub addr: SocketAddr, + pub public_key: PublicKey, +} + +impl From for Peer { + fn from(peer: PeerConfig) -> Self { + Self { + id: peer.id, + addr: peer.addr, + public_key: peer.public_key, + } + } +} + +impl Peer { + pub fn peer_info(&self) -> PeerInfo { + PeerInfo { + id: self.id.clone(), + addr: self.addr, + } + } +} From 2ad08bc87089a1a95fec55dfc447883c2b23512f Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 21 Feb 2024 20:03:15 +0100 Subject: [PATCH 26/50] Add `ValueBuilder` trait for building values --- code/node/bin/main.rs | 7 +++++-- code/node/src/lib.rs | 1 + code/node/src/node.rs | 32 +++++++++++++++---------------- code/node/src/timers.rs | 6 +++--- code/node/src/value.rs | 42 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 21 deletions(-) create mode 100644 code/node/src/value.rs diff --git a/code/node/bin/main.rs b/code/node/bin/main.rs index 0f3856609..d9307af2c 100644 --- a/code/node/bin/main.rs +++ b/code/node/bin/main.rs @@ -7,8 +7,9 @@ use malachite_node::network::broadcast::PeerInfo; use malachite_node::node::{Node, Params}; use malachite_node::peers::Peers; use malachite_node::timers; +use malachite_node::value::test::TestValueBuilder; use malachite_test::utils::{make_validators, FixedProposer}; -use malachite_test::{Address, Height, PrivateKey, TestContext, ValidatorSet, Value}; +use malachite_test::{Address, Height, PrivateKey, TestContext, ValidatorSet}; use tracing::info; mod cli; @@ -60,10 +61,12 @@ pub async fn make_node( let start_height = Height::new(1); let ctx = TestContext::new(private_key); let proposer_selector = Arc::new(FixedProposer::new(validator_set.validators[0].address)); + let proposal_builder = Arc::new(TestValueBuilder::default()); let params = Params { start_height, proposer_selector, + proposal_builder, validator_set, address, peers: peers.clone(), @@ -91,5 +94,5 @@ pub async fn make_node( handle.connect_to_peer(peer, timeout).await; } - Node::new(ctx, params, handle, Value::new(42), timers_config) + Node::new(ctx, params, handle, timers_config) } diff --git a/code/node/src/lib.rs b/code/node/src/lib.rs index 2651f1030..4875e594c 100644 --- a/code/node/src/lib.rs +++ b/code/node/src/lib.rs @@ -3,3 +3,4 @@ pub mod network; pub mod node; pub mod peers; pub mod timers; +pub mod value; diff --git a/code/node/src/node.rs b/code/node/src/node.rs index 31e33e6c0..0205dc340 100644 --- a/code/node/src/node.rs +++ b/code/node/src/node.rs @@ -1,6 +1,6 @@ use std::fmt::Display; use std::sync::Arc; -use std::time::Duration; +use std::time::Instant; use tokio::sync::mpsc; use tracing::{debug, info, warn, Instrument}; @@ -17,10 +17,12 @@ use crate::network::Msg as NetworkMsg; use crate::network::{Network, PeerId}; use crate::peers::Peers; use crate::timers::{self, Timers}; +use crate::value::ValueBuilder; pub struct Params { pub start_height: Ctx::Height, pub proposer_selector: Arc>, + pub proposal_builder: Arc>, pub validator_set: Ctx::ValidatorSet, pub address: Ctx::Address, pub threshold_params: ThresholdParams, @@ -39,7 +41,6 @@ where network: Net, timers: Timers, timeout_elapsed: mpsc::Receiver, - value: Ctx::Value, done: bool, } @@ -51,13 +52,7 @@ where Ctx::Vote: Protobuf, Ctx::Proposal: Protobuf, { - pub fn new( - ctx: Ctx, - params: Params, - network: Net, - value: Ctx::Value, - timers_config: timers::Config, - ) -> Self { + pub fn new(ctx: Ctx, params: Params, network: Net, timers_config: timers::Config) -> Self { let driver = Driver::new( ctx.clone(), params.start_height, @@ -76,7 +71,6 @@ where network, timers, timeout_elapsed, - value, done: false, } } @@ -277,20 +271,26 @@ where Next::None } - Output::GetValue(height, round, _timeout) => { + Output::GetValue(height, round, timeout) => { info!("Requesting value at height {height} and round {round}"); - let value = self.get_value().await; + + // FIXME: Make this asynchronous, we can't block the event loop + // while we are waiting for a value. + let value = self.get_value(height, &timeout).await; Next::Input(Input::ProposeValue(round, value)) } } } - pub async fn get_value(&self) -> Ctx::Value { - // Simulate waiting for a value to be assembled - tokio::time::sleep(Duration::from_secs(1)).await; + pub async fn get_value(&self, height: Ctx::Height, timeout: &Timeout) -> Ctx::Value { + let deadline = Instant::now() + self.timers.timeout_duration(&timeout.step); - self.value.clone() + self.params + .proposal_builder + .build_proposal(height, deadline) + .await + .unwrap() // FIXME } } diff --git a/code/node/src/timers.rs b/code/node/src/timers.rs index f3c19df2f..69e8cf321 100644 --- a/code/node/src/timers.rs +++ b/code/node/src/timers.rs @@ -46,7 +46,7 @@ impl Timers { pub async fn schedule_timeout(&mut self, timeout: Timeout) { let tx = self.timeout_elapsed.clone(); - let duration = self.timeout_duration(&timeout); + let duration = self.timeout_duration(&timeout.step); let timeouts = self.timeouts.clone(); let handle = tokio::spawn(async move { @@ -64,8 +64,8 @@ impl Timers { } } - fn timeout_duration(&self, timeout: &Timeout) -> Duration { - match timeout.step { + pub fn timeout_duration(&self, step: &TimeoutStep) -> Duration { + match step { TimeoutStep::Propose => self.config.propose_timeout, TimeoutStep::Prevote => self.config.prevote_timeout, TimeoutStep::Precommit => self.config.precommit_timeout, diff --git a/code/node/src/value.rs b/code/node/src/value.rs new file mode 100644 index 000000000..5c7be65dd --- /dev/null +++ b/code/node/src/value.rs @@ -0,0 +1,42 @@ +use std::marker::PhantomData; +use std::time::Instant; + +use derive_where::derive_where; + +use futures::future::BoxFuture; +use malachite_common::Context; + +#[allow(async_fn_in_trait)] +pub trait ValueBuilder { + fn build_proposal( + &self, + height: Ctx::Height, + deadline: Instant, + ) -> BoxFuture>; +} + +pub mod test { + use super::*; + + use futures::FutureExt; + use malachite_test::{Height, TestContext, Value}; + + #[derive_where(Default)] + pub struct TestValueBuilder { + _phantom: PhantomData, + } + + impl ValueBuilder for TestValueBuilder { + fn build_proposal(&self, height: Height, deadline: Instant) -> BoxFuture> { + async move { + let diff = deadline.duration_since(Instant::now()); + let wait = diff / 2; + + tokio::time::sleep(wait).await; + + Some(Value::new(40 + height.as_u64())) + } + .boxed() + } + } +} From 277c3d06b22badbe7312973474aa10bb9787f348 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 21 Feb 2024 21:41:08 +0100 Subject: [PATCH 27/50] Install `protoc` on CI --- .github/workflows/coverage.yml | 16 ++++++++++++---- .github/workflows/mbt.yml | 8 ++++++-- .github/workflows/rust.yml | 10 ++++++++-- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index c9845fced..58043f6bd 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -25,10 +25,14 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - name: Install Protoc + uses: arduino/setup-protoc@v3 + - name: Setup Node + uses: actions/setup-node@v3 with: node-version: "18" - - run: npm install -g @informalsystems/quint + - name: Install Quint + run: npm install -g @informalsystems/quint - name: Setup Rust toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 with: @@ -61,10 +65,14 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - name: Install Protoc + uses: arduino/setup-protoc@v3 + - name: Setup Node + uses: actions/setup-node@v3 with: node-version: "18" - - run: npm install -g @informalsystems/quint + - name: Install Quint + run: npm install -g @informalsystems/quint - name: Setup Rust toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 with: diff --git a/.github/workflows/mbt.yml b/.github/workflows/mbt.yml index 725550ec4..9af99db13 100644 --- a/.github/workflows/mbt.yml +++ b/.github/workflows/mbt.yml @@ -27,10 +27,14 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - name: Install Protoc + uses: arduino/setup-protoc@v3 + - name: Setup Node + uses: actions/setup-node@v3 with: node-version: "18" - - run: npm install -g @informalsystems/quint + - name: Install Quint + run: npm install -g @informalsystems/quint - name: Setup Rust toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 - name: Install cargo-nextest diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 100d38491..af0e39d1d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -31,10 +31,14 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - name: Install Protoc + uses: arduino/setup-protoc@v3 + - name: Setup Node + uses: actions/setup-node@v3 with: node-version: "18" - - run: npm install -g @informalsystems/quint + - name: Install Quint + run: npm install -g @informalsystems/quint - name: Setup Rust toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 - name: Install cargo-nextest @@ -50,6 +54,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + - name: Install Protoc + uses: arduino/setup-protoc@v3 - name: Setup Rust toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 with: From 5418db14fed9bc96bb95c21ec43e81b327757a8c Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 21 Feb 2024 21:52:16 +0100 Subject: [PATCH 28/50] Remove redundant import --- code/node/src/network/broadcast.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/code/node/src/network/broadcast.rs b/code/node/src/network/broadcast.rs index 3c9eea639..af08a8659 100644 --- a/code/node/src/network/broadcast.rs +++ b/code/node/src/network/broadcast.rs @@ -304,8 +304,6 @@ impl Network for Handle { #[cfg(test)] mod tests { - use std::time::Duration; - use tokio::time::timeout; use super::*; From 4915de4457e2f58c58a78a10c43fdc0aa3c2220e Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Thu, 22 Feb 2024 10:28:42 +0100 Subject: [PATCH 29/50] Add integration for 3 non-faulty nodes --- code/Cargo.toml | 1 + code/common/src/height.rs | 2 +- code/common/src/signing.rs | 10 ++-- code/common/src/validator_set.rs | 6 +-- code/common/src/value.rs | 4 +- code/driver/src/proposer.rs | 1 + code/node/Cargo.toml | 1 + code/node/bin/main.rs | 61 ++---------------------- code/node/src/lib.rs | 1 + code/node/src/network.rs | 9 +++- code/node/src/network/broadcast.rs | 1 + code/node/src/node.rs | 76 ++++++++++++++++++++++++------ code/node/src/util/make_node.rs | 58 +++++++++++++++++++++++ code/node/src/util/mod.rs | 2 + code/node/src/value.rs | 2 +- code/node/tests/broadcast_n3f0.rs | 70 +++++++++++++++++++++++++++ 16 files changed, 222 insertions(+), 83 deletions(-) create mode 100644 code/node/src/util/make_node.rs create mode 100644 code/node/src/util/mod.rs create mode 100644 code/node/tests/broadcast_n3f0.rs diff --git a/code/Cargo.toml b/code/Cargo.toml index 02f37f745..ef476fa47 100644 --- a/code/Cargo.toml +++ b/code/Cargo.toml @@ -29,6 +29,7 @@ malachite-round = { version = "0.1.0", path = "round" } malachite-test = { version = "0.1.0", path = "test" } malachite-vote = { version = "0.1.0", path = "vote" } +async-trait = "0.1.77" derive-where = "1.2.7" ed25519-consensus = "2.1.0" futures = "0.3" diff --git a/code/common/src/height.rs b/code/common/src/height.rs index 9bdcdfedd..e3ec96e5d 100644 --- a/code/common/src/height.rs +++ b/code/common/src/height.rs @@ -7,7 +7,7 @@ use core::fmt::Debug; /// A height of 0 represents a chain which has not yet produced a block. pub trait Height where - Self: Default + Copy + Clone + Debug + PartialEq + Eq + PartialOrd + Ord, + Self: Default + Copy + Clone + Debug + PartialEq + Eq + PartialOrd + Ord + Send + Sync, { /// Increment the height by one. fn increment(&self) -> Self; diff --git a/code/common/src/signing.rs b/code/common/src/signing.rs index 3da354c04..52d56ba6c 100644 --- a/code/common/src/signing.rs +++ b/code/common/src/signing.rs @@ -19,13 +19,17 @@ where type DecodingError: Display; /// The type of signatures produced by this signing scheme. - type Signature: Clone + Debug + Eq; + type Signature: Clone + Debug + Eq + Send + Sync; /// The type of public keys produced by this signing scheme. - type PublicKey: Clone + Debug + Eq + Verifier; + type PublicKey: Clone + Debug + Eq + Send + Sync + Verifier; /// The type of private keys produced by this signing scheme. - type PrivateKey: Clone + Signer + Keypair; + type PrivateKey: Clone + + Send + + Sync + + Signer + + Keypair; /// Decode a signature from a byte array. fn decode_signature(bytes: &[u8]) -> Result; diff --git a/code/common/src/validator_set.rs b/code/common/src/validator_set.rs index ccc4dd1f6..390600751 100644 --- a/code/common/src/validator_set.rs +++ b/code/common/src/validator_set.rs @@ -10,14 +10,14 @@ pub type VotingPower = u64; /// Defines the requirements for an address. pub trait Address where - Self: Clone + Debug + Display + Eq + Ord, + Self: Clone + Debug + Display + Eq + Ord + Send + Sync, { } /// Defines the requirements for a validator. pub trait Validator where - Self: Clone + Debug + PartialEq + Eq, + Self: Clone + Debug + PartialEq + Eq + Send + Sync, Ctx: Context, { /// The address of the validator, typically derived from its public key. @@ -35,7 +35,7 @@ where /// A validator set is a collection of validators. pub trait ValidatorSet where - Self: Clone + Debug, + Self: Clone + Debug + Send + Sync, Ctx: Context, { /// The total voting power of the validator set. diff --git a/code/common/src/value.rs b/code/common/src/value.rs index a16ac246e..090bd75dc 100644 --- a/code/common/src/value.rs +++ b/code/common/src/value.rs @@ -56,11 +56,11 @@ impl NilOrVal { /// Defines the requirements for the type of value to decide on. pub trait Value where - Self: Clone + Debug + PartialEq + Eq + PartialOrd + Ord, + Self: Clone + Debug + PartialEq + Eq + PartialOrd + Ord + Send + Sync, { /// The type of the ID of the value. /// Typically a representation of the value with a lower memory footprint. - type Id: Clone + Debug + PartialEq + Eq + PartialOrd + Ord; + type Id: Clone + Debug + PartialEq + Eq + PartialOrd + Ord + Send + Sync; /// The ID of the value. fn id(&self) -> Self::Id; diff --git a/code/driver/src/proposer.rs b/code/driver/src/proposer.rs index eb4abbd4a..00819dcd6 100644 --- a/code/driver/src/proposer.rs +++ b/code/driver/src/proposer.rs @@ -3,6 +3,7 @@ use malachite_common::{Context, Round}; /// Defines how to select a proposer amongst a validator set for a given round. pub trait ProposerSelector where + Self: Send + Sync, Ctx: Context, { /// Select a proposer from the given validator set for the given round. diff --git a/code/node/Cargo.toml b/code/node/Cargo.toml index f77cb3c05..7b4c53790 100644 --- a/code/node/Cargo.toml +++ b/code/node/Cargo.toml @@ -20,6 +20,7 @@ malachite-vote.workspace = true malachite-proto.workspace = true malachite-test.workspace = true +async-trait = { workspace = true } ed25519-consensus = { workspace = true, features = ["serde"] } derive-where = { workspace = true } futures = { workspace = true } diff --git a/code/node/bin/main.rs b/code/node/bin/main.rs index d9307af2c..bb4a01b62 100644 --- a/code/node/bin/main.rs +++ b/code/node/bin/main.rs @@ -1,15 +1,8 @@ -use std::sync::Arc; -use std::time::Duration; - use malachite_node::config::Config; -use malachite_node::network::broadcast; -use malachite_node::network::broadcast::PeerInfo; -use malachite_node::node::{Node, Params}; -use malachite_node::peers::Peers; -use malachite_node::timers; -use malachite_node::value::test::TestValueBuilder; -use malachite_test::utils::{make_validators, FixedProposer}; -use malachite_test::{Address, Height, PrivateKey, TestContext, ValidatorSet}; +use malachite_node::util::make_node; +use malachite_test::utils::make_validators; + +use malachite_test::ValidatorSet; use tracing::info; mod cli; @@ -50,49 +43,3 @@ pub async fn main() { node.run().await; } - -pub async fn make_node( - validator_set: ValidatorSet, - private_key: PrivateKey, - address: Address, - peer_info: PeerInfo, - peers: Peers, -) -> Node { - let start_height = Height::new(1); - let ctx = TestContext::new(private_key); - let proposer_selector = Arc::new(FixedProposer::new(validator_set.validators[0].address)); - let proposal_builder = Arc::new(TestValueBuilder::default()); - - let params = Params { - start_height, - proposer_selector, - proposal_builder, - validator_set, - address, - peers: peers.clone(), - threshold_params: Default::default(), - }; - - let timers_config = timers::Config { - propose_timeout: Duration::from_secs(10), - prevote_timeout: Duration::from_secs(5), - precommit_timeout: Duration::from_secs(5), - commit_timeout: Duration::from_secs(5), - }; - - let network = broadcast::Peer::new(peer_info.clone()); - let handle = network.run().await; - - let timeout = Some(Duration::from_secs(10)); - - let to_connect = peers - .iter() - .filter(|p| p.id != peer_info.id) - .map(|p| p.peer_info()); - - for peer in to_connect { - handle.connect_to_peer(peer, timeout).await; - } - - Node::new(ctx, params, handle, timers_config) -} diff --git a/code/node/src/lib.rs b/code/node/src/lib.rs index 4875e594c..392adc099 100644 --- a/code/node/src/lib.rs +++ b/code/node/src/lib.rs @@ -3,4 +3,5 @@ pub mod network; pub mod node; pub mod peers; pub mod timers; +pub mod util; pub mod value; diff --git a/code/node/src/network.rs b/code/node/src/network.rs index 145fec308..52400e8ab 100644 --- a/code/node/src/network.rs +++ b/code/node/src/network.rs @@ -2,6 +2,8 @@ use core::fmt; use std::convert::Infallible; use std::str::FromStr; +use async_trait::async_trait; + pub mod broadcast; mod msg; @@ -32,8 +34,11 @@ impl FromStr for PeerId { } } -#[allow(async_fn_in_trait)] -pub trait Network { +#[async_trait] +pub trait Network +where + Self: Send + Sync + 'static, +{ async fn recv(&mut self) -> Option<(PeerId, Msg)>; async fn broadcast(&mut self, msg: Msg); diff --git a/code/node/src/network/broadcast.rs b/code/node/src/network/broadcast.rs index af08a8659..b31b69a48 100644 --- a/code/node/src/network/broadcast.rs +++ b/code/node/src/network/broadcast.rs @@ -292,6 +292,7 @@ impl Handle { } } +#[async_trait::async_trait] impl Network for Handle { async fn recv(&mut self) -> Option<(PeerId, Msg)> { Handle::recv(self).await diff --git a/code/node/src/node.rs b/code/node/src/node.rs index 0205dc340..e6620d111 100644 --- a/code/node/src/node.rs +++ b/code/node/src/node.rs @@ -1,8 +1,9 @@ use std::fmt::Display; +use std::marker::PhantomData; use std::sync::Arc; use std::time::Instant; -use tokio::sync::mpsc; +use tokio::sync::{mpsc, oneshot}; use tracing::{debug, info, warn, Instrument}; use malachite_common::{ @@ -31,6 +32,27 @@ pub struct Params { type TxInput = mpsc::UnboundedSender>; +type RxDecision = + mpsc::UnboundedReceiver::Height, Round, ::Value)>>; +type TxDecision = + mpsc::UnboundedSender::Height, Round, ::Value)>>; + +pub struct Handle { + tx_abort: oneshot::Sender<()>, + rx_decision: RxDecision, + _marker: PhantomData, +} + +impl Handle { + pub fn abort(self) { + self.tx_abort.send(()).unwrap(); + } + + pub async fn wait_decision(&mut self) -> Option<(Ctx::Height, Round, Ctx::Value)> { + self.rx_decision.recv().await.flatten() + } +} + pub struct Node where Ctx: Context, @@ -75,23 +97,40 @@ where } } - pub async fn run(mut self) { + pub async fn run(mut self) -> Handle { let mut height = self.params.start_height; - loop { - let span = tracing::error_span!("node", height = %height); + let (tx_abort, mut rx_abort) = oneshot::channel(); + let (tx_decision, rx_decision) = mpsc::unbounded_channel(); - self.start_height(height).instrument(span).await; + tokio::spawn(async move { + loop { + let span = tracing::error_span!("node", height = %height); - height = self.driver.height().increment(); - self.driver = self.driver.move_to_height(height); + self.start_height(height, &tx_decision) + .instrument(span) + .await; - debug_assert_eq!(self.driver.height(), height); - debug_assert_eq!(self.driver.round(), Round::Nil); + height = self.driver.height().increment(); + self.driver = self.driver.move_to_height(height); + + debug_assert_eq!(self.driver.height(), height); + debug_assert_eq!(self.driver.round(), Round::Nil); + + if let Ok(()) = rx_abort.try_recv() { + break; + } + } + }); + + Handle { + tx_abort, + rx_decision, + _marker: PhantomData, } } - pub async fn start_height(&mut self, height: Ctx::Height) { + pub async fn start_height(&mut self, height: Ctx::Height, tx_decision: &TxDecision) { let (tx_input, mut rx_input) = mpsc::unbounded_channel(); tx_input @@ -108,7 +147,7 @@ where tokio::select! { Some(input) = rx_input.recv() => { - self.process_input(input, &tx_input).await; + self.process_input(input, &tx_input, tx_decision).await; } Some(timeout) = self.timeout_elapsed.recv() => { @@ -122,7 +161,12 @@ where } } - pub async fn process_input(&mut self, input: Input, tx_input: &TxInput) { + pub async fn process_input( + &mut self, + input: Input, + tx_input: &TxInput, + tx_decision: &TxDecision, + ) { match &input { Input::NewRound(_, _) => { self.timers.reset().await; @@ -157,8 +201,12 @@ where match self.process_output(output).await { Next::None => (), Next::Input(input) => tx_input.send(input).unwrap(), - Next::Decided(round, _) => { - self.timers.schedule_timeout(Timeout::commit(round)).await + Next::Decided(round, value) => { + self.timers.schedule_timeout(Timeout::commit(round)).await; + + tx_decision + .send(Some((self.driver.height(), round, value))) + .unwrap(); } } } diff --git a/code/node/src/util/make_node.rs b/code/node/src/util/make_node.rs new file mode 100644 index 000000000..c8f9af2e8 --- /dev/null +++ b/code/node/src/util/make_node.rs @@ -0,0 +1,58 @@ +use std::sync::Arc; +use std::time::Duration; + +use malachite_test::utils::FixedProposer; +use malachite_test::{Address, Height, PrivateKey, TestContext, ValidatorSet}; + +use crate::network::broadcast; +use crate::network::broadcast::PeerInfo; +use crate::node::{Node, Params}; +use crate::peers::Peers; +use crate::timers; +use crate::value::test::TestValueBuilder; + +pub async fn make_node( + validator_set: ValidatorSet, + private_key: PrivateKey, + address: Address, + peer_info: PeerInfo, + peers: Peers, +) -> Node { + let start_height = Height::new(1); + let ctx = TestContext::new(private_key); + let proposer_selector = Arc::new(FixedProposer::new(validator_set.validators[0].address)); + let proposal_builder = Arc::new(TestValueBuilder::default()); + + let params = Params { + start_height, + proposer_selector, + proposal_builder, + validator_set, + address, + peers: peers.clone(), + threshold_params: Default::default(), + }; + + let timers_config = timers::Config { + propose_timeout: Duration::from_secs(3), + prevote_timeout: Duration::from_secs(1), + precommit_timeout: Duration::from_secs(1), + commit_timeout: Duration::from_secs(1), + }; + + let network = broadcast::Peer::new(peer_info.clone()); + let handle = network.run().await; + + let timeout = Some(Duration::from_secs(5)); + + let to_connect = peers + .iter() + .filter(|p| p.id != peer_info.id) + .map(|p| p.peer_info()); + + for peer in to_connect { + handle.connect_to_peer(peer, timeout).await; + } + + Node::new(ctx, params, handle, timers_config) +} diff --git a/code/node/src/util/mod.rs b/code/node/src/util/mod.rs new file mode 100644 index 000000000..ad2831098 --- /dev/null +++ b/code/node/src/util/mod.rs @@ -0,0 +1,2 @@ +mod make_node; +pub use make_node::make_node; diff --git a/code/node/src/value.rs b/code/node/src/value.rs index 5c7be65dd..e97324601 100644 --- a/code/node/src/value.rs +++ b/code/node/src/value.rs @@ -7,7 +7,7 @@ use futures::future::BoxFuture; use malachite_common::Context; #[allow(async_fn_in_trait)] -pub trait ValueBuilder { +pub trait ValueBuilder: Send + Sync + 'static { fn build_proposal( &self, height: Ctx::Height, diff --git a/code/node/tests/broadcast_n3f0.rs b/code/node/tests/broadcast_n3f0.rs new file mode 100644 index 000000000..5e671fc2e --- /dev/null +++ b/code/node/tests/broadcast_n3f0.rs @@ -0,0 +1,70 @@ +use malachite_common::Round; +use malachite_node::config::Config; +use malachite_node::util::make_node; +use malachite_test::utils::make_validators; +use malachite_test::{Height, ValidatorSet, Value}; + +#[tokio::test] +pub async fn decide_on_value() { + tracing_subscriber::fmt::init(); + + // Validators keys are deterministic and match the ones in the config file + let vs = make_validators([2, 3, 2]); + + let config = include_str!("../peers.toml"); + let config = toml::from_str::(config).expect("Error: invalid peers.toml"); + + let mut handles = Vec::with_capacity(config.peers.len()); + + for peer_config in &config.peers { + let (my_sk, my_addr) = vs + .iter() + .find(|(v, _)| v.public_key == peer_config.public_key) + .map(|(v, pk)| (pk.clone(), v.address)) + .expect("Error: invalid peer id"); + + let (vs, _): (Vec<_>, Vec<_>) = vs.clone().into_iter().unzip(); + + let peer_info = peer_config.peer_info(); + let vs = ValidatorSet::new(vs); + + let node = tokio::spawn(make_node( + vs, + my_sk, + my_addr, + peer_info, + config.clone().into(), + )); + + handles.push(node); + } + + let mut nodes = Vec::with_capacity(config.peers.len()); + + for handle in handles { + let node = handle.await.expect("Error: node failed to start"); + nodes.push(node); + } + + let mut handles = Vec::with_capacity(config.peers.len()); + + for node in nodes { + let handle = node.run().await; + handles.push(handle); + } + + for height in 1..=3 { + for handle in &mut handles { + let decision = handle.wait_decision().await; + + assert_eq!( + decision, + Some((Height::new(height), Round::new(0), Value::new(40 + height))) + ); + } + } + + for handle in handles { + handle.abort(); + } +} From ff4359bcadc640a3e91275191cdea38f25fae420 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Thu, 22 Feb 2024 10:38:28 +0100 Subject: [PATCH 30/50] Fix node binary, and print each decisions --- code/node/bin/main.rs | 11 ++++++++++- code/node/src/util/make_node.rs | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/code/node/bin/main.rs b/code/node/bin/main.rs index bb4a01b62..7d4dd31e3 100644 --- a/code/node/bin/main.rs +++ b/code/node/bin/main.rs @@ -41,5 +41,14 @@ pub async fn main() { info!("[{}] Starting...", args.peer_id); - node.run().await; + let mut handle = node.run().await; + + loop { + if let Some((height, round, value)) = handle.wait_decision().await { + info!( + "[{}] Decision at height {height} and round {round}: {value:?}", + args.peer_id + ); + } + } } diff --git a/code/node/src/util/make_node.rs b/code/node/src/util/make_node.rs index c8f9af2e8..1b628514d 100644 --- a/code/node/src/util/make_node.rs +++ b/code/node/src/util/make_node.rs @@ -43,7 +43,7 @@ pub async fn make_node( let network = broadcast::Peer::new(peer_info.clone()); let handle = network.run().await; - let timeout = Some(Duration::from_secs(5)); + let timeout = Some(Duration::from_secs(30)); let to_connect = peers .iter() From b4173d6d27ab32eedcf6c6c540cc2a704c4622a1 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Thu, 22 Feb 2024 10:43:16 +0100 Subject: [PATCH 31/50] Exclude binary and Debug impls from coverage --- code/node/bin/cli.rs | 2 ++ code/node/bin/main.rs | 2 ++ code/node/src/network/broadcast.rs | 2 ++ 3 files changed, 6 insertions(+) diff --git a/code/node/bin/cli.rs b/code/node/bin/cli.rs index 919dab5c2..c7716b9c0 100644 --- a/code/node/bin/cli.rs +++ b/code/node/bin/cli.rs @@ -1,3 +1,5 @@ +#![cfg_attr(coverage_nightly, coverage(off))] + use malachite_node::network::PeerId; pub struct Cli { diff --git a/code/node/bin/main.rs b/code/node/bin/main.rs index 7d4dd31e3..db4e4456f 100644 --- a/code/node/bin/main.rs +++ b/code/node/bin/main.rs @@ -1,3 +1,5 @@ +#![cfg_attr(coverage_nightly, coverage(off))] + use malachite_node::config::Config; use malachite_node::util::make_node; use malachite_test::utils::make_validators; diff --git a/code/node/src/network/broadcast.rs b/code/node/src/network/broadcast.rs index b31b69a48..3fa76a5e9 100644 --- a/code/node/src/network/broadcast.rs +++ b/code/node/src/network/broadcast.rs @@ -17,6 +17,7 @@ pub enum PeerEvent { } impl Debug for PeerEvent { + #[cfg_attr(coverage_nightly, coverage(off))] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { PeerEvent::ConnectToPeer(peer_info, _, _) => { @@ -36,6 +37,7 @@ pub struct PeerInfo { } impl fmt::Display for PeerInfo { + #[cfg_attr(coverage_nightly, coverage(off))] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{id} ({addr})", id = self.id, addr = self.addr) } From 378c1523083988edbeab8df1b54e83826d94d50e Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Thu, 22 Feb 2024 10:58:17 +0100 Subject: [PATCH 32/50] Fix coverage --- .github/workflows/coverage.yml | 10 +++++++++- code/node/bin/cli.rs | 2 -- code/node/bin/main.rs | 2 -- code/node/src/lib.rs | 2 ++ 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 58043f6bd..be853e20f 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -43,7 +43,15 @@ jobs: - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - name: Generate code coverage - run: cargo llvm-cov nextest --workspace --exclude malachite-itf --all-features --ignore-run-fail --lcov --output-path lcov.info + run: | + cargo llvm-cov nextest \ + --workspace \ + --exclude malachite-itf \ + --ignore-filename-regex node/bin \ + --all-features \ + --ignore-run-fail \ + --lcov \ + --output-path lcov.info - name: Generate text report run: cargo llvm-cov report - name: Upload coverage to Codecov diff --git a/code/node/bin/cli.rs b/code/node/bin/cli.rs index c7716b9c0..919dab5c2 100644 --- a/code/node/bin/cli.rs +++ b/code/node/bin/cli.rs @@ -1,5 +1,3 @@ -#![cfg_attr(coverage_nightly, coverage(off))] - use malachite_node::network::PeerId; pub struct Cli { diff --git a/code/node/bin/main.rs b/code/node/bin/main.rs index db4e4456f..7d4dd31e3 100644 --- a/code/node/bin/main.rs +++ b/code/node/bin/main.rs @@ -1,5 +1,3 @@ -#![cfg_attr(coverage_nightly, coverage(off))] - use malachite_node::config::Config; use malachite_node::util::make_node; use malachite_test::utils::make_validators; diff --git a/code/node/src/lib.rs b/code/node/src/lib.rs index 392adc099..561c1f595 100644 --- a/code/node/src/lib.rs +++ b/code/node/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg_attr(coverage_nightly, feature(coverage_attribute))] + pub mod config; pub mod network; pub mod node; From 363b6f55aa587814d30ecaf79ab2c113f65c97c3 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Thu, 22 Feb 2024 14:30:13 +0100 Subject: [PATCH 33/50] Add height to `ProposerSelector` --- code/driver/src/driver.rs | 10 +++++++--- code/driver/src/proposer.rs | 7 ++++++- code/test/src/utils.rs | 14 ++++++++++++-- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/code/driver/src/driver.rs b/code/driver/src/driver.rs index 5719bf437..9356da103 100644 --- a/code/driver/src/driver.rs +++ b/code/driver/src/driver.rs @@ -115,10 +115,14 @@ where } /// Return the proposer for the current round. - pub fn get_proposer(&self, round: Round) -> Result<&Ctx::Validator, Error> { + pub fn get_proposer( + &self, + height: Ctx::Height, + round: Round, + ) -> Result<&Ctx::Validator, Error> { let address = self .proposer_selector - .select_proposer(round, &self.validator_set); + .select_proposer(height, round, &self.validator_set); let proposer = self .validator_set @@ -267,7 +271,7 @@ where let round_state = core::mem::take(&mut self.round_state); let current_step = round_state.step; - let proposer = self.get_proposer(round_state.round)?; + let proposer = self.get_proposer(round_state.height, round_state.round)?; let info = Info::new(input_round, &self.address, proposer.address()); // Apply the input to the round state machine diff --git a/code/driver/src/proposer.rs b/code/driver/src/proposer.rs index 00819dcd6..89f2d35d0 100644 --- a/code/driver/src/proposer.rs +++ b/code/driver/src/proposer.rs @@ -14,5 +14,10 @@ where /// # Important /// This function must be deterministic! /// For a given round and validator set, it must always return the same proposer. - fn select_proposer(&self, round: Round, validator_set: &Ctx::ValidatorSet) -> Ctx::Address; + fn select_proposer( + &self, + height: Ctx::Height, + round: Round, + validator_set: &Ctx::ValidatorSet, + ) -> Ctx::Address; } diff --git a/code/test/src/utils.rs b/code/test/src/utils.rs index efac6cd8a..8fd4b6a1b 100644 --- a/code/test/src/utils.rs +++ b/code/test/src/utils.rs @@ -13,7 +13,12 @@ use crate::{ pub struct RotateProposer; impl ProposerSelector for RotateProposer { - fn select_proposer(&self, round: Round, validator_set: &ValidatorSet) -> Address { + fn select_proposer( + &self, + _height: Height, + round: Round, + validator_set: &ValidatorSet, + ) -> Address { let proposer_index = round.as_i64() as usize % validator_set.validators.len(); validator_set.validators[proposer_index].address } @@ -31,7 +36,12 @@ impl FixedProposer { } impl ProposerSelector for FixedProposer { - fn select_proposer(&self, _round: Round, _validator_set: &ValidatorSet) -> Address { + fn select_proposer( + &self, + _height: Height, + _round: Round, + _validator_set: &ValidatorSet, + ) -> Address { self.proposer } } From 70a55cb2434f3ca661552a0a61c12b5de804cd20 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Fri, 23 Feb 2024 14:14:24 +0100 Subject: [PATCH 34/50] Clear prevote/precommit timeout when we get 2/3+ votes for a value --- code/driver/src/driver.rs | 5 +++++ code/node/bin/main.rs | 4 +++- code/node/src/node.rs | 37 ++++++++++++++++++++++++++++++++++--- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/code/driver/src/driver.rs b/code/driver/src/driver.rs index 9356da103..8f0233bca 100644 --- a/code/driver/src/driver.rs +++ b/code/driver/src/driver.rs @@ -114,6 +114,11 @@ where self.round_state.round } + /// Return a reference to the votekeper + pub fn votes(&self) -> &VoteKeeper { + &self.vote_keeper + } + /// Return the proposer for the current round. pub fn get_proposer( &self, diff --git a/code/node/bin/main.rs b/code/node/bin/main.rs index 7d4dd31e3..1b1efa7d5 100644 --- a/code/node/bin/main.rs +++ b/code/node/bin/main.rs @@ -8,6 +8,8 @@ use tracing::info; mod cli; use cli::Cli; +const VOTING_PWERS: [u64; 3] = [5, 20, 10]; + #[tokio::main(flavor = "current_thread")] pub async fn main() { tracing_subscriber::fmt::init(); @@ -15,7 +17,7 @@ pub async fn main() { let args = Cli::from_env(); // Validators keys are deterministic and match the ones in the config file - let vs = make_validators([2, 3, 2]); + let vs = make_validators(VOTING_PWERS); let config = std::fs::read_to_string("node/peers.toml").expect("Error: missing peers.toml"); let config = toml::from_str::(&config).expect("Error: invalid peers.toml"); diff --git a/code/node/src/node.rs b/code/node/src/node.rs index e6620d111..f4694734b 100644 --- a/code/node/src/node.rs +++ b/code/node/src/node.rs @@ -7,12 +7,12 @@ use tokio::sync::{mpsc, oneshot}; use tracing::{debug, info, warn, Instrument}; use malachite_common::{ - Context, Height, Proposal, Round, SignedProposal, SignedVote, Timeout, TimeoutStep, Vote, - VoteType, + Context, Height, NilOrVal, Proposal, Round, SignedProposal, SignedVote, Timeout, TimeoutStep, + Vote, VoteType, }; use malachite_driver::{Driver, Input, Output, ProposerSelector, Validity}; use malachite_proto::{self as proto, Protobuf}; -use malachite_vote::ThresholdParams; +use malachite_vote::{Threshold, ThresholdParams}; use crate::network::Msg as NetworkMsg; use crate::network::{Network, PeerId}; @@ -195,8 +195,39 @@ where Input::TimeoutElapsed(_) => (), } + let check_threshold = if let Input::Vote(vote) = &input { + let round = Vote::::round(vote); + let value = Vote::::value(vote); + + Some((vote.vote_type(), round, value.clone())) + } else { + None + }; + let outputs = self.driver.process(input).unwrap(); + // When we receive a vote, check if we've gotten +2/3 votes for the value we just received a vote for. + if let Some((vote_type, round, value)) = check_threshold { + let threshold = match value { + NilOrVal::Nil => Threshold::Nil, + NilOrVal::Val(value) => Threshold::Value(value), + }; + + if self + .driver + .votes() + .is_threshold_met(&round, vote_type, threshold.clone()) + { + let timeout = match vote_type { + VoteType::Prevote => Timeout::prevote(round), + VoteType::Precommit => Timeout::precommit(round), + }; + + info!("Threshold met for {threshold:?} at round {round}, cancelling {timeout}"); + self.timers.cancel_timeout(&timeout).await; + } + } + for output in outputs { match self.process_output(output).await { Next::None => (), From f2617c1ad3b115196976ad8fe2210a718943e1d0 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Fri, 23 Feb 2024 14:23:20 +0100 Subject: [PATCH 35/50] Track current round in span --- code/node/src/node.rs | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/code/node/src/node.rs b/code/node/src/node.rs index f4694734b..568bca3e5 100644 --- a/code/node/src/node.rs +++ b/code/node/src/node.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use std::time::Instant; use tokio::sync::{mpsc, oneshot}; -use tracing::{debug, info, warn, Instrument}; +use tracing::{debug, error_span, info, warn, Instrument}; use malachite_common::{ Context, Height, NilOrVal, Proposal, Round, SignedProposal, SignedVote, Timeout, TimeoutStep, @@ -31,6 +31,7 @@ pub struct Params { } type TxInput = mpsc::UnboundedSender>; +type RxInput = mpsc::UnboundedReceiver>; type RxDecision = mpsc::UnboundedReceiver::Height, Round, ::Value)>>; @@ -105,7 +106,7 @@ where tokio::spawn(async move { loop { - let span = tracing::error_span!("node", height = %height); + let span = error_span!("node", height = %height); self.start_height(height, &tx_decision) .instrument(span) @@ -138,6 +139,8 @@ where .unwrap(); loop { + let span = error_span!("round", round = %self.driver.round()); + if self.done { self.done = false; self.timers.reset().await; @@ -145,18 +148,29 @@ where break; } - tokio::select! { - Some(input) = rx_input.recv() => { - self.process_input(input, &tx_input, tx_decision).await; - } + self.step(&tx_input, &mut rx_input, tx_decision) + .instrument(span) + .await; + } + } - Some(timeout) = self.timeout_elapsed.recv() => { - self.process_timeout(timeout, &tx_input).await; - } + pub async fn step( + &mut self, + tx_input: &TxInput, + rx_input: &mut RxInput, + tx_decision: &TxDecision, + ) { + tokio::select! { + Some(input) = rx_input.recv() => { + self.process_input(input, tx_input, tx_decision).await; + } - Some((peer_id, msg)) = self.network.recv() => { - self.process_network_msg(peer_id, msg, &tx_input).await; - } + Some(timeout) = self.timeout_elapsed.recv() => { + self.process_timeout(timeout, tx_input).await; + } + + Some((peer_id, msg)) = self.network.recv() => { + self.process_network_msg(peer_id, msg, tx_input).await; } } } From 1ee0e70ea3b8a40e49a00897c888bf0adb2dd76c Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Fri, 23 Feb 2024 14:30:15 +0100 Subject: [PATCH 36/50] Remove `peers.toml` config, generate it on the fly --- code/node/bin/main.rs | 24 +++++++++++++++++++----- code/node/peers.toml | 14 -------------- 2 files changed, 19 insertions(+), 19 deletions(-) delete mode 100644 code/node/peers.toml diff --git a/code/node/bin/main.rs b/code/node/bin/main.rs index 1b1efa7d5..a6e15b3af 100644 --- a/code/node/bin/main.rs +++ b/code/node/bin/main.rs @@ -1,8 +1,11 @@ -use malachite_node::config::Config; +use std::net::{Ipv4Addr, SocketAddr}; + +use malachite_node::config::{Config, PeerConfig}; +use malachite_node::network::PeerId; use malachite_node::util::make_node; use malachite_test::utils::make_validators; -use malachite_test::ValidatorSet; +use malachite_test::{Validator, ValidatorSet}; use tracing::info; mod cli; @@ -10,6 +13,19 @@ use cli::Cli; const VOTING_PWERS: [u64; 3] = [5, 20, 10]; +fn make_config<'a>(vs: impl Iterator) -> Config { + let peers = vs + .enumerate() + .map(|(i, v)| PeerConfig { + id: PeerId::new(format!("node{}", i + 1)), + addr: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1200 + i as u16 + 1), + public_key: v.public_key, + }) + .collect(); + + Config { peers } +} + #[tokio::main(flavor = "current_thread")] pub async fn main() { tracing_subscriber::fmt::init(); @@ -18,9 +34,7 @@ pub async fn main() { // Validators keys are deterministic and match the ones in the config file let vs = make_validators(VOTING_PWERS); - - let config = std::fs::read_to_string("node/peers.toml").expect("Error: missing peers.toml"); - let config = toml::from_str::(&config).expect("Error: invalid peers.toml"); + let config = make_config(vs.iter().map(|(v, _)| v)); let peer_config = config .peers diff --git a/code/node/peers.toml b/code/node/peers.toml deleted file mode 100644 index cc7490523..000000000 --- a/code/node/peers.toml +++ /dev/null @@ -1,14 +0,0 @@ -[[peers]] -id = "node1" -addr = "127.0.0.1:1235" -public_key = [104, 30, 170, 163, 78, 73, 30, 108, 131, 53, 171, 201, 234, 146, 176, 36, 239, 82, 235, 145, 68, 44, 163, 184, 69, 152, 199, 154, 121, 243, 27, 117] - -[[peers]] -id = "node2" -addr = "127.0.0.1:1236" -public_key = [24, 109, 62, 237, 160, 46, 173, 95, 187, 173, 116, 78, 237, 21, 141, 149, 140, 79, 127, 72, 86, 26, 62, 102, 203, 30, 233, 104, 85, 173, 92, 25] - -[[peers]] -id = "node3" -addr = "127.0.0.1:1237" -public_key = [49, 171, 95, 10, 5, 226, 72, 164, 147, 3, 48, 71, 200, 88, 31, 0, 121, 180, 85, 143, 94, 156, 113, 175, 97, 106, 231, 109, 128, 203, 219, 7] From 673796a34e00f15be6c2f9319c1a66fccb918199 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Thu, 22 Feb 2024 14:41:33 +0100 Subject: [PATCH 37/50] Use `turmoil` for writing broadcast test --- code/Cargo.toml | 1 + code/node/Cargo.toml | 5 + code/node/src/network/broadcast.rs | 175 ++++++++++++++++++++++------- 3 files changed, 139 insertions(+), 42 deletions(-) diff --git a/code/Cargo.toml b/code/Cargo.toml index ef476fa47..cd0790a0b 100644 --- a/code/Cargo.toml +++ b/code/Cargo.toml @@ -51,5 +51,6 @@ thiserror = "1.0" tokio = "1.35.1" tokio-stream = "0.1" toml = "0.8.10" +turmoil = "0.6.1" tracing = "0.1.40" tracing-subscriber = "0.3.18" diff --git a/code/node/Cargo.toml b/code/node/Cargo.toml index 7b4c53790..d6b4ae2a6 100644 --- a/code/node/Cargo.toml +++ b/code/node/Cargo.toml @@ -12,6 +12,9 @@ publish.workspace = true name = "malachite-node" path = "bin/main.rs" +[features] +turmoil = ["dep:turmoil"] + [dependencies] malachite-common.workspace = true malachite-driver.workspace = true @@ -33,5 +36,7 @@ toml = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["fmt"] } +turmoil = { workspace = true, optional = true } + [dev-dependencies] malachite-test.workspace = true diff --git a/code/node/src/network/broadcast.rs b/code/node/src/network/broadcast.rs index 3fa76a5e9..aff16128d 100644 --- a/code/node/src/network/broadcast.rs +++ b/code/node/src/network/broadcast.rs @@ -3,9 +3,14 @@ use std::fmt::Debug; use std::net::SocketAddr; use std::time::{Duration, Instant}; +#[cfg(not(feature = "turmoil"))] +use tokio::net::{TcpListener, TcpStream}; + +#[cfg(feature = "turmoil")] +use turmoil::net::{TcpListener, TcpStream}; + use futures::channel::oneshot; use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::{TcpListener, TcpStream}; use tokio::sync::{broadcast, mpsc}; use tracing::{debug, error, info, warn}; @@ -143,13 +148,21 @@ async fn connect_to_peer( tokio::spawn(async move { loop { - let (from, msg) = per_peer_rx.recv().await.unwrap(); - if from == peer_info.id { - continue; - } + match per_peer_rx.recv().await { + Ok((from, msg)) => { + if from == peer_info.id { + continue; + } + + debug!("[{id}] Sending message to {peer_info}: {msg:?}"); + Frame::Msg(msg).write(&mut stream).await.unwrap(); + } - debug!("[{id}] Sending message to {peer_info}: {msg:?}"); - Frame::Msg(msg).write(&mut stream).await.unwrap(); + Err(e) => { + error!("[{id}] Failed to receive message from peer channel: {e}"); + break; + } + } } }); } @@ -183,22 +196,35 @@ async fn listen( tokio::spawn(async move { loop { - let Frame::Msg(msg) = Frame::read(&mut socket).await.unwrap() else { - error!("[{id}] Peer did not send a message"); - return; - }; + let frame = Frame::read(&mut socket).await; - debug!( - "[{id}] Received message from {peer_id} ({addr}): {msg:?}", - addr = socket.peer_addr().unwrap(), - ); + match frame { + Ok(Frame::Msg(msg)) => { + debug!( + "[{id}] Received message from {peer_id} ({addr}): {msg:?}", + addr = socket.peer_addr().unwrap(), + ); - tx_received.send((peer_id.clone(), msg)).await.unwrap(); // FIXME + tx_received.send((peer_id.clone(), msg)).await.unwrap(); + // FIXME + } + + Ok(frame) => { + error!("[{id}] Peer sent an unexpected frame: {frame:?}"); + break; + } + + Err(e) => { + error!("[{id}] Peer did not send a message: {e}"); + break; + } + } } }); } } +#[derive(Debug)] pub enum Frame { PeerId(PeerId), Msg(Msg), @@ -306,61 +332,126 @@ impl Network for Handle { } #[cfg(test)] +#[allow(unused_imports)] mod tests { - use tokio::time::timeout; + use std::sync::atomic::{AtomicUsize, Ordering}; + + use tokio::time::{sleep, timeout}; use super::*; - #[tokio::test] - async fn test_peer() { + #[test] + #[cfg(feature = "turmoil")] + fn test_peer() { + use std::net::{IpAddr, Ipv4Addr}; + use std::sync::Arc; + use turmoil::lookup; + + tracing_subscriber::fmt().init(); + let peer1_id = PeerId("peer-1".to_string()); let peer1_info = PeerInfo { id: peer1_id.clone(), - addr: "127.0.0.1:12001".parse().unwrap(), + addr: (IpAddr::from(Ipv4Addr::UNSPECIFIED), 12001).into(), }; let peer2_id = PeerId("peer-2".to_string()); let peer2_info = PeerInfo { id: peer2_id.clone(), - addr: "127.0.0.1:12002".parse().unwrap(), + addr: (IpAddr::from(Ipv4Addr::UNSPECIFIED), 12002).into(), }; let peer3_id = PeerId("peer-3".to_string()); let peer3_info = PeerInfo { id: peer3_id.clone(), - addr: "127.0.0.1:12003".parse().unwrap(), + addr: (IpAddr::from(Ipv4Addr::UNSPECIFIED), 12003).into(), }; let peer1: Peer = Peer::new(peer1_info.clone()); let peer2: Peer = Peer::new(peer2_info.clone()); let peer3: Peer = Peer::new(peer3_info.clone()); - let handle1 = peer1.run().await; - let mut handle2 = peer2.run().await; - let mut handle3 = peer3.run().await; + let mut sim = turmoil::Builder::new().build(); - handle1.connect_to_peer(peer2_info.clone(), None).await; - handle1.connect_to_peer(peer3_info.clone(), None).await; + let done = Arc::new(AtomicUsize::new(0)); + let deadline = Duration::from_millis(200); - handle2.connect_to_peer(peer1_info.clone(), None).await; - handle2.connect_to_peer(peer3_info.clone(), None).await; + { + let done = Arc::clone(&done); + let mut peer2_info = peer2_info.clone(); + let mut peer3_info = peer3_info.clone(); - handle3.connect_to_peer(peer1_info.clone(), None).await; - handle3.connect_to_peer(peer2_info.clone(), None).await; + sim.client("peer1", async move { + peer2_info.addr.set_ip(lookup("peer2")); + peer3_info.addr.set_ip(lookup("peer3")); - handle1.broadcast(Msg::Dummy(1)).await; - handle1.broadcast(Msg::Dummy(2)).await; + let handle1 = peer1.run().await; + handle1.connect_to_peer(peer2_info, None).await; + handle1.connect_to_peer(peer3_info, None).await; - let deadline = Duration::from_millis(100); + while done.load(Ordering::SeqCst) < 2 { + sleep(Duration::from_millis(50)).await; + } - let msg2 = timeout(deadline, handle2.recv()).await.unwrap(); - dbg!(&msg2); - let msg3 = timeout(deadline, handle3.recv()).await.unwrap(); - dbg!(&msg3); + handle1.broadcast(Msg::Dummy(1)).await; + handle1.broadcast(Msg::Dummy(2)).await; + + Ok(()) + }); + } + + { + let done = Arc::clone(&done); + + let peer1_id = peer1_id.clone(); + let mut peer1_info = peer1_info.clone(); + let mut peer3_info = peer3_info.clone(); + + sim.client("peer2", async move { + peer1_info.addr.set_ip(lookup("peer1")); + peer3_info.addr.set_ip(lookup("peer3")); + + let mut handle2 = peer2.run().await; + handle2.connect_to_peer(peer1_info, None).await; + handle2.connect_to_peer(peer3_info, None).await; + + done.fetch_add(1, Ordering::SeqCst); + + let msg1 = timeout(deadline, handle2.recv()).await.unwrap(); + assert_eq!(msg1, Some((peer1_id.clone(), Msg::Dummy(1)))); + + let msg2 = timeout(deadline, handle2.recv()).await.unwrap(); + assert_eq!(msg2, Some((peer1_id.clone(), Msg::Dummy(2)))); + + Ok(()) + }); + } + + { + let done = Arc::clone(&done); + let mut peer1_info = peer1_info.clone(); + let mut peer2_info = peer2_info.clone(); + + sim.client("peer3", async move { + peer1_info.addr.set_ip(lookup("peer1")); + peer2_info.addr.set_ip(lookup("peer2")); + + let mut handle3 = peer3.run().await; + handle3.connect_to_peer(peer1_info, None).await; + handle3.connect_to_peer(peer2_info, None).await; + + done.fetch_add(1, Ordering::SeqCst); + + let msg1 = timeout(deadline, handle3.recv()).await.unwrap(); + assert_eq!(msg1, Some((peer1_id.clone(), Msg::Dummy(1)))); + + let msg2 = timeout(deadline, handle3.recv()).await.unwrap(); + assert_eq!(msg2, Some((peer1_id.clone(), Msg::Dummy(2)))); + + Ok(()) + }); + } - let msg4 = timeout(deadline, handle2.recv()).await.unwrap(); - dbg!(&msg4); - let msg5 = timeout(deadline, handle3.recv()).await.unwrap(); - dbg!(&msg5); + sim.run().unwrap(); } } From 862d140676da0bb49279328a308d938eab80dd97 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Mon, 26 Feb 2024 12:32:28 +0100 Subject: [PATCH 38/50] Better error handling in broadcast network --- code/node/src/network/broadcast.rs | 35 ++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/code/node/src/network/broadcast.rs b/code/node/src/network/broadcast.rs index aff16128d..f37101cf1 100644 --- a/code/node/src/network/broadcast.rs +++ b/code/node/src/network/broadcast.rs @@ -144,7 +144,10 @@ async fn connect_to_peer( let mut per_peer_rx = per_peer_tx.subscribe(); - Frame::PeerId(id.clone()).write(&mut stream).await.unwrap(); + if let Err(e) = Frame::PeerId(id.clone()).write(&mut stream).await { + error!("[{id}] Failed to send our ID to {peer_info}: {e}"); + return; + }; tokio::spawn(async move { loop { @@ -155,7 +158,11 @@ async fn connect_to_peer( } debug!("[{id}] Sending message to {peer_info}: {msg:?}"); - Frame::Msg(msg).write(&mut stream).await.unwrap(); + + if let Err(e) = Frame::Msg(msg).write(&mut stream).await { + error!("[{id}] Failed to send message to {peer_info}: {e}"); + break; + } } Err(e) => { @@ -172,8 +179,15 @@ async fn listen( addr: SocketAddr, tx_spawned: oneshot::Sender<()>, tx_received: mpsc::Sender<(PeerId, Msg)>, -) -> ! { - let listener = TcpListener::bind(addr).await.unwrap(); +) { + let listener = match TcpListener::bind(addr).await { + Ok(listener) => listener, + Err(e) => { + error!("[{id}] Failed to bind to {addr}: {e}"); + return; + } + }; + info!("[{id}] Listening on {addr}..."); tx_spawned.send(()).unwrap(); @@ -186,9 +200,16 @@ async fn listen( peer = socket.peer_addr().unwrap() ); - let Frame::PeerId(peer_id) = Frame::read(&mut socket).await.unwrap() else { - error!("[{id}] Peer did not send its ID"); - continue; + let peer_id = match Frame::read(&mut socket).await { + Ok(Frame::PeerId(peer_id)) => peer_id, + Ok(frame) => { + error!("[{id}] Peer did not send its ID, got instead: {frame:?}"); + continue; + } + Err(e) => { + error!("[{id}] Peer did not send its ID: {e}"); + continue; + } }; let id = id.clone(); From d2a1881ee569e18780c22e31b0a290ae3e0bf10a Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Mon, 26 Feb 2024 12:36:54 +0100 Subject: [PATCH 39/50] Fix integration test and use 10 validators --- code/node/bin/main.rs | 20 ++------------------ code/node/src/util/make_config.rs | 19 +++++++++++++++++++ code/node/src/util/mod.rs | 3 +++ code/node/tests/broadcast_n3f0.rs | 11 +++++------ 4 files changed, 29 insertions(+), 24 deletions(-) create mode 100644 code/node/src/util/make_config.rs diff --git a/code/node/bin/main.rs b/code/node/bin/main.rs index a6e15b3af..0c8fce17f 100644 --- a/code/node/bin/main.rs +++ b/code/node/bin/main.rs @@ -1,11 +1,8 @@ -use std::net::{Ipv4Addr, SocketAddr}; - -use malachite_node::config::{Config, PeerConfig}; -use malachite_node::network::PeerId; +use malachite_node::util::make_config; use malachite_node::util::make_node; use malachite_test::utils::make_validators; -use malachite_test::{Validator, ValidatorSet}; +use malachite_test::ValidatorSet; use tracing::info; mod cli; @@ -13,19 +10,6 @@ use cli::Cli; const VOTING_PWERS: [u64; 3] = [5, 20, 10]; -fn make_config<'a>(vs: impl Iterator) -> Config { - let peers = vs - .enumerate() - .map(|(i, v)| PeerConfig { - id: PeerId::new(format!("node{}", i + 1)), - addr: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1200 + i as u16 + 1), - public_key: v.public_key, - }) - .collect(); - - Config { peers } -} - #[tokio::main(flavor = "current_thread")] pub async fn main() { tracing_subscriber::fmt::init(); diff --git a/code/node/src/util/make_config.rs b/code/node/src/util/make_config.rs new file mode 100644 index 000000000..34ebe331c --- /dev/null +++ b/code/node/src/util/make_config.rs @@ -0,0 +1,19 @@ +use std::net::{Ipv4Addr, SocketAddr}; + +use malachite_test::Validator; + +use crate::config::{Config, PeerConfig}; +use crate::network::PeerId; + +pub fn make_config<'a>(vs: impl Iterator) -> Config { + let peers = vs + .enumerate() + .map(|(i, v)| PeerConfig { + id: PeerId::new(format!("node{}", i + 1)), + addr: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1200 + i as u16 + 1), + public_key: v.public_key, + }) + .collect(); + + Config { peers } +} diff --git a/code/node/src/util/mod.rs b/code/node/src/util/mod.rs index ad2831098..c25fd0e1c 100644 --- a/code/node/src/util/mod.rs +++ b/code/node/src/util/mod.rs @@ -1,2 +1,5 @@ mod make_node; pub use make_node::make_node; + +mod make_config; +pub use make_config::make_config; diff --git a/code/node/tests/broadcast_n3f0.rs b/code/node/tests/broadcast_n3f0.rs index 5e671fc2e..dcead5db7 100644 --- a/code/node/tests/broadcast_n3f0.rs +++ b/code/node/tests/broadcast_n3f0.rs @@ -1,6 +1,5 @@ use malachite_common::Round; -use malachite_node::config::Config; -use malachite_node::util::make_node; +use malachite_node::util::{make_config, make_node}; use malachite_test::utils::make_validators; use malachite_test::{Height, ValidatorSet, Value}; @@ -8,11 +7,11 @@ use malachite_test::{Height, ValidatorSet, Value}; pub async fn decide_on_value() { tracing_subscriber::fmt::init(); - // Validators keys are deterministic and match the ones in the config file - let vs = make_validators([2, 3, 2]); + let voting_powers = [5, 20, 10, 30, 15, 1, 5, 25, 10, 15]; - let config = include_str!("../peers.toml"); - let config = toml::from_str::(config).expect("Error: invalid peers.toml"); + // Validators keys are deterministic and match the ones in the config file + let vs = make_validators(voting_powers); + let config = make_config(vs.iter().map(|(v, _)| v)); let mut handles = Vec::with_capacity(config.peers.len()); From cb1a5f94c68dcddb3e4b740cf7134b9b1944e967 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Mon, 26 Feb 2024 14:57:47 +0100 Subject: [PATCH 40/50] Basic libp2p-based gossip as a standalone crate --- .github/codespell/codespell.ini | 2 +- .github/codespell/words.txt | 6 +- code/Cargo.toml | 4 +- code/gossip/Cargo.toml | 15 ++ code/gossip/src/lib.rs | 264 ++++++++++++++++++++++++++++++++ code/node/src/network.rs | 9 +- code/node/src/network/gossip.rs | 0 7 files changed, 291 insertions(+), 9 deletions(-) create mode 100644 code/gossip/Cargo.toml create mode 100644 code/gossip/src/lib.rs create mode 100644 code/node/src/network/gossip.rs diff --git a/.github/codespell/codespell.ini b/.github/codespell/codespell.ini index 85fa043e4..68615c7c5 100644 --- a/.github/codespell/codespell.ini +++ b/.github/codespell/codespell.ini @@ -1,3 +1,3 @@ [codespell] -skip = ./code/target +skip = ./code/target,./code/Cargo.lock ignore-words = .github/codespell/words.txt diff --git a/.github/codespell/words.txt b/.github/codespell/words.txt index 8b24813a0..3ebd95862 100644 --- a/.github/codespell/words.txt +++ b/.github/codespell/words.txt @@ -1,5 +1,5 @@ crate -shs -ser -numer manuel +numer +ser +shs diff --git a/code/Cargo.toml b/code/Cargo.toml index cd0790a0b..ba2c0805e 100644 --- a/code/Cargo.toml +++ b/code/Cargo.toml @@ -3,7 +3,8 @@ resolver = "2" members = [ "common", - "driver", + "driver", + "gossip", "itf", "node", "proto", @@ -22,6 +23,7 @@ publish = false [workspace.dependencies] malachite-common = { version = "0.1.0", path = "common" } malachite-driver = { version = "0.1.0", path = "driver" } +malachite-gossip = { version = "0.1.0", path = "gossip" } malachite-itf = { version = "0.1.0", path = "itf" } malachite-node = { version = "0.1.0", path = "node" } malachite-proto = { version = "0.1.0", path = "proto" } diff --git a/code/gossip/Cargo.toml b/code/gossip/Cargo.toml new file mode 100644 index 000000000..06f37c80a --- /dev/null +++ b/code/gossip/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "malachite-gossip" +version.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +libp2p = { version = "0.53.2", features = ["macros", "noise", "identify", "yamux", "tokio", "ed25519", "tcp", "tls", "gossipsub"] } +libp2p-gossipsub = { version = "0.46.1" } + +futures = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } diff --git a/code/gossip/src/lib.rs b/code/gossip/src/lib.rs new file mode 100644 index 000000000..243301834 --- /dev/null +++ b/code/gossip/src/lib.rs @@ -0,0 +1,264 @@ +use std::error::Error; +use std::ops::ControlFlow; +use std::time::Duration; + +use futures::StreamExt; +use libp2p::identity::Keypair; +use libp2p::swarm::{self, NetworkBehaviour, SwarmEvent}; +use libp2p::{gossipsub, identify, noise, tcp, yamux, Multiaddr, PeerId, SwarmBuilder}; +use tokio::sync::mpsc; +use tracing::{debug, error, info}; + +const PROTOCOL_VERSION: &str = "malachite-gossip/v1beta1"; +const TOPIC: &str = "consensus"; + +#[derive(NetworkBehaviour)] +#[behaviour(to_swarm = "Event")] +struct Behaviour { + identify: identify::Behaviour, + gossipsub: gossipsub::Behaviour, +} + +impl Behaviour { + fn new(keypair: &Keypair) -> Self { + Self { + identify: identify::Behaviour::new(identify::Config::new( + PROTOCOL_VERSION.to_string(), + keypair.public(), + )), + gossipsub: gossipsub::Behaviour::new( + gossipsub::MessageAuthenticity::Signed(keypair.clone()), + gossipsub::Config::default(), + ) + .unwrap(), + } + } +} + +#[derive(Debug)] +enum Event { + Identify(identify::Event), + GossipSub(gossipsub::Event), +} + +impl From for Event { + fn from(event: identify::Event) -> Self { + Self::Identify(event) + } +} + +impl From for Event { + fn from(event: gossipsub::Event) -> Self { + Self::GossipSub(event) + } +} + +pub struct Config { + idle_connection_timeout: Duration, +} + +impl Config { + fn apply(self, cfg: swarm::Config) -> swarm::Config { + cfg.with_idle_connection_timeout(self.idle_connection_timeout) + } +} + +impl Default for Config { + fn default() -> Self { + Self { + idle_connection_timeout: Duration::from_secs(30), + } + } +} + +#[derive(Debug)] +pub enum HandleEvent { + Listening(Multiaddr), + Message(PeerId, Vec), + PeerConnected(PeerId), + PeerDisconnected(PeerId), +} + +#[derive(Debug)] +pub enum CtrlMsg { + Broadcast(Vec), + Shutdown, +} + +pub struct Handle { + rx_event: mpsc::Receiver, + tx_ctrl: mpsc::Sender, + task_handle: tokio::task::JoinHandle<()>, +} + +impl Handle { + pub async fn recv(&mut self) -> Option { + self.rx_event.recv().await + } + + pub async fn broadcast(&mut self, data: Vec) -> Result<(), Box> { + self.tx_ctrl.send(CtrlMsg::Broadcast(data)).await?; + Ok(()) + } + + pub async fn shutdown(self) -> Result<(), Box> { + self.tx_ctrl.send(CtrlMsg::Shutdown).await?; + self.task_handle.await?; + Ok(()) + } +} + +pub async fn spawn( + keypair: Keypair, + addr: Multiaddr, + config: Config, +) -> Result> { + let mut swarm = SwarmBuilder::with_existing_identity(keypair) + .with_tokio() + .with_tcp( + tcp::Config::default(), + noise::Config::new, + yamux::Config::default, + )? + .with_behaviour(Behaviour::new)? + .with_swarm_config(|cfg| config.apply(cfg)) + .build(); + + let topic = gossipsub::IdentTopic::new(TOPIC); + swarm.behaviour_mut().gossipsub.subscribe(&topic)?; + + swarm.listen_on(addr)?; + + let (tx_event, rx_event) = mpsc::channel(32); + let (tx_ctrl, rx_ctrl) = mpsc::channel(32); + let task_handle = tokio::task::spawn(run(swarm, topic, rx_ctrl, tx_event)); + + Ok(Handle { + rx_event, + tx_ctrl, + task_handle, + }) +} + +async fn run( + mut swarm: swarm::Swarm, + topic: gossipsub::IdentTopic, + mut rx_ctrl: mpsc::Receiver, + tx_event: mpsc::Sender, +) { + loop { + let result = tokio::select! { + event = swarm.select_next_some() => { + handle_swarm_event(event, &topic, &tx_event).await + } + + Some(ctrl) = rx_ctrl.recv() => { + handle_ctrl_msg(ctrl, &topic, &mut swarm).await + } + }; + + match result { + ControlFlow::Continue(()) => continue, + ControlFlow::Break(()) => break, + } + } +} + +async fn handle_ctrl_msg( + msg: CtrlMsg, + topic: &gossipsub::IdentTopic, + swarm: &mut swarm::Swarm, +) -> ControlFlow<()> { + match msg { + CtrlMsg::Broadcast(data) => { + let result = swarm.behaviour_mut().gossipsub.publish(topic.hash(), data); + + match result { + Ok(message_id) => { + debug!("Broadcasted message {message_id}"); + } + Err(e) => { + error!("Error broadcasting message: {e}"); + } + } + + ControlFlow::Continue(()) + } + + CtrlMsg::Shutdown => ControlFlow::Break(()), + } +} + +async fn handle_swarm_event( + event: SwarmEvent, + topic: &gossipsub::IdentTopic, + tx_event: &mpsc::Sender, +) -> ControlFlow<()> { + match event { + SwarmEvent::NewListenAddr { address, .. } => { + info!("Node is listening on {address}"); + + if let Err(e) = tx_event.send(HandleEvent::Listening(address)).await { + error!("Error sending listening event to handle: {e}"); + return ControlFlow::Break(()); + } + } + + SwarmEvent::Behaviour(Event::Identify(identify::Event::Sent { peer_id })) => { + debug!("Sent identity to {peer_id}"); + } + + SwarmEvent::Behaviour(Event::Identify(identify::Event::Received { peer_id, info: _ })) => { + debug!("Received identity from {peer_id}"); + } + + SwarmEvent::Behaviour(Event::GossipSub(gossipsub::Event::Subscribed { + peer_id, + topic: topic_hash, + })) => { + if topic.hash() != topic_hash { + debug!("Peer {peer_id} subscribed to different topic: {topic_hash}"); + return ControlFlow::Continue(()); + } + + debug!("Peer {peer_id} subscribed to {topic_hash}"); + + if let Err(e) = tx_event.send(HandleEvent::PeerConnected(peer_id)).await { + error!("Error sending peer connected event to handle: {e}"); + return ControlFlow::Break(()); + } + } + + SwarmEvent::Behaviour(Event::GossipSub(gossipsub::Event::Message { + propagation_source: peer_id, + message_id: _, + message, + })) => { + if topic.hash() != message.topic { + debug!( + "Received message from {peer_id} on different topic: {}", + message.topic + ); + + return ControlFlow::Continue(()); + } + + debug!( + "Received message from {peer_id} of {} bytes", + message.data.len() + ); + + if let Err(e) = tx_event + .send(HandleEvent::Message(peer_id, message.data)) + .await + { + error!("Error sending message to handle: {e}"); + return ControlFlow::Break(()); + } + } + + _ => {} + } + + ControlFlow::Continue(()) +} diff --git a/code/node/src/network.rs b/code/node/src/network.rs index 52400e8ab..e2e4ba325 100644 --- a/code/node/src/network.rs +++ b/code/node/src/network.rs @@ -4,12 +4,13 @@ use std::str::FromStr; use async_trait::async_trait; -pub mod broadcast; -mod msg; - use malachite_proto::{SignedProposal, SignedVote}; -pub use self::msg::Msg; +pub mod broadcast; +pub mod gossip; + +mod msg; +pub use msg::Msg; #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct PeerId(String); diff --git a/code/node/src/network/gossip.rs b/code/node/src/network/gossip.rs new file mode 100644 index 000000000..e69de29bb From 37bc4d6af74665c703c4ab3cd88fb130a0ad32fa Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Mon, 26 Feb 2024 17:36:17 +0100 Subject: [PATCH 41/50] Use mDNS-based peer discovery for now --- code/gossip/Cargo.toml | 2 +- code/gossip/src/lib.rs | 60 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/code/gossip/Cargo.toml b/code/gossip/Cargo.toml index 06f37c80a..3dd13f778 100644 --- a/code/gossip/Cargo.toml +++ b/code/gossip/Cargo.toml @@ -7,7 +7,7 @@ license.workspace = true publish.workspace = true [dependencies] -libp2p = { version = "0.53.2", features = ["macros", "noise", "identify", "yamux", "tokio", "ed25519", "tcp", "tls", "gossipsub"] } +libp2p = { version = "0.53.2", features = ["macros", "noise", "mdns", "identify", "yamux", "tokio", "ed25519", "tcp", "tls", "gossipsub"] } libp2p-gossipsub = { version = "0.46.1" } futures = { workspace = true } diff --git a/code/gossip/src/lib.rs b/code/gossip/src/lib.rs index 243301834..5f1a9d599 100644 --- a/code/gossip/src/lib.rs +++ b/code/gossip/src/lib.rs @@ -3,11 +3,13 @@ use std::ops::ControlFlow; use std::time::Duration; use futures::StreamExt; -use libp2p::identity::Keypair; use libp2p::swarm::{self, NetworkBehaviour, SwarmEvent}; -use libp2p::{gossipsub, identify, noise, tcp, yamux, Multiaddr, PeerId, SwarmBuilder}; +use libp2p::{gossipsub, identify, mdns, noise, tcp, yamux, SwarmBuilder}; use tokio::sync::mpsc; -use tracing::{debug, error, info}; +use tracing::{debug, error, error_span, info, Instrument}; + +pub use libp2p::identity::Keypair; +pub use libp2p::{Multiaddr, PeerId}; const PROTOCOL_VERSION: &str = "malachite-gossip/v1beta1"; const TOPIC: &str = "consensus"; @@ -16,6 +18,7 @@ const TOPIC: &str = "consensus"; #[behaviour(to_swarm = "Event")] struct Behaviour { identify: identify::Behaviour, + mdns: mdns::tokio::Behaviour, gossipsub: gossipsub::Behaviour, } @@ -26,6 +29,11 @@ impl Behaviour { PROTOCOL_VERSION.to_string(), keypair.public(), )), + mdns: mdns::tokio::Behaviour::new( + mdns::Config::default(), + keypair.public().to_peer_id(), + ) + .unwrap(), gossipsub: gossipsub::Behaviour::new( gossipsub::MessageAuthenticity::Signed(keypair.clone()), gossipsub::Config::default(), @@ -38,6 +46,7 @@ impl Behaviour { #[derive(Debug)] enum Event { Identify(identify::Event), + Mdns(mdns::Event), GossipSub(gossipsub::Event), } @@ -47,6 +56,12 @@ impl From for Event { } } +impl From for Event { + fn from(event: mdns::Event) -> Self { + Self::Mdns(event) + } +} + impl From for Event { fn from(event: gossipsub::Event) -> Self { Self::GossipSub(event) @@ -131,7 +146,10 @@ pub async fn spawn( let (tx_event, rx_event) = mpsc::channel(32); let (tx_ctrl, rx_ctrl) = mpsc::channel(32); - let task_handle = tokio::task::spawn(run(swarm, topic, rx_ctrl, tx_event)); + + let peer_id = swarm.local_peer_id(); + let span = error_span!("gossip", peer = %peer_id); + let task_handle = tokio::task::spawn(run(swarm, topic, rx_ctrl, tx_event).instrument(span)); Ok(Handle { rx_event, @@ -149,11 +167,11 @@ async fn run( loop { let result = tokio::select! { event = swarm.select_next_some() => { - handle_swarm_event(event, &topic, &tx_event).await + handle_swarm_event(event, &mut swarm, &topic, &tx_event).await } Some(ctrl) = rx_ctrl.recv() => { - handle_ctrl_msg(ctrl, &topic, &mut swarm).await + handle_ctrl_msg(ctrl, &mut swarm, &topic).await } }; @@ -166,8 +184,8 @@ async fn run( async fn handle_ctrl_msg( msg: CtrlMsg, - topic: &gossipsub::IdentTopic, swarm: &mut swarm::Swarm, + topic: &gossipsub::IdentTopic, ) -> ControlFlow<()> { match msg { CtrlMsg::Broadcast(data) => { @@ -191,6 +209,7 @@ async fn handle_ctrl_msg( async fn handle_swarm_event( event: SwarmEvent, + swarm: &mut swarm::Swarm, topic: &gossipsub::IdentTopic, tx_event: &mpsc::Sender, ) -> ControlFlow<()> { @@ -212,6 +231,33 @@ async fn handle_swarm_event( debug!("Received identity from {peer_id}"); } + SwarmEvent::Behaviour(Event::Mdns(mdns::Event::Discovered(peers))) => { + for (peer_id, addr) in peers { + debug!("Discovered peer {peer_id} at {addr}"); + swarm.behaviour_mut().gossipsub.add_explicit_peer(&peer_id); + + // if let Err(e) = tx_event.send(HandleEvent::PeerConnected(peer_id)).await { + // error!("Error sending peer connected event to handle: {e}"); + // return ControlFlow::Break(()); + // } + } + } + + SwarmEvent::Behaviour(Event::Mdns(mdns::Event::Expired(peers))) => { + for (peer_id, _addr) in peers { + debug!("Expired peer: {peer_id}"); + swarm + .behaviour_mut() + .gossipsub + .remove_explicit_peer(&peer_id); + + // if let Err(e) = tx_event.send(HandleEvent::PeerDisconnected(peer_id)).await { + // error!("Error sending peer disconnected event to handle: {e}"); + // return ControlFlow::Break(()); + // } + } + } + SwarmEvent::Behaviour(Event::GossipSub(gossipsub::Event::Subscribed { peer_id, topic: topic_hash, From d12bf3c2146893c6c6277efa22f42cfff0ce7fa4 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Mon, 26 Feb 2024 17:36:40 +0100 Subject: [PATCH 42/50] Add gossip-based integration test with 10 nodes --- code/node/Cargo.toml | 5 +-- code/node/bin/main.rs | 8 ++--- code/node/src/network/gossip.rs | 24 ++++++++++++++ code/node/src/node.rs | 40 +++++++++++++---------- code/node/src/peers.rs | 2 +- code/node/src/util/make_node.rs | 45 ++++++++++++++++++++++++-- code/node/src/util/mod.rs | 2 +- code/node/tests/broadcast_n3f0.rs | 4 +-- code/node/tests/gossip_n10f0.rs | 54 +++++++++++++++++++++++++++++++ code/test/src/signing.rs | 5 +++ 10 files changed, 159 insertions(+), 30 deletions(-) create mode 100644 code/node/tests/gossip_n10f0.rs diff --git a/code/node/Cargo.toml b/code/node/Cargo.toml index d6b4ae2a6..3f9f01172 100644 --- a/code/node/Cargo.toml +++ b/code/node/Cargo.toml @@ -18,10 +18,11 @@ turmoil = ["dep:turmoil"] [dependencies] malachite-common.workspace = true malachite-driver.workspace = true -malachite-round.workspace = true -malachite-vote.workspace = true +malachite-gossip.workspace = true malachite-proto.workspace = true +malachite-round.workspace = true malachite-test.workspace = true +malachite-vote.workspace = true async-trait = { workspace = true } ed25519-consensus = { workspace = true, features = ["serde"] } diff --git a/code/node/bin/main.rs b/code/node/bin/main.rs index 0c8fce17f..b5daceeef 100644 --- a/code/node/bin/main.rs +++ b/code/node/bin/main.rs @@ -1,5 +1,5 @@ +use malachite_node::util::make_broadcast_node; use malachite_node::util::make_config; -use malachite_node::util::make_node; use malachite_test::utils::make_validators; use malachite_test::ValidatorSet; @@ -8,7 +8,7 @@ use tracing::info; mod cli; use cli::Cli; -const VOTING_PWERS: [u64; 3] = [5, 20, 10]; +const VOTING_POWERS: [u64; 3] = [5, 20, 10]; #[tokio::main(flavor = "current_thread")] pub async fn main() { @@ -17,7 +17,7 @@ pub async fn main() { let args = Cli::from_env(); // Validators keys are deterministic and match the ones in the config file - let vs = make_validators(VOTING_PWERS); + let vs = make_validators(VOTING_POWERS); let config = make_config(vs.iter().map(|(v, _)| v)); let peer_config = config @@ -37,7 +37,7 @@ pub async fn main() { let peer_info = peer_config.peer_info(); let vs = ValidatorSet::new(vs); - let node = make_node(vs, my_sk, my_addr, peer_info, config.into()).await; + let node = make_broadcast_node(vs, my_sk, my_addr, peer_info, config.into()).await; info!("[{}] Starting...", args.peer_id); diff --git a/code/node/src/network/gossip.rs b/code/node/src/network/gossip.rs index e69de29bb..cc9cbced5 100644 --- a/code/node/src/network/gossip.rs +++ b/code/node/src/network/gossip.rs @@ -0,0 +1,24 @@ +pub use malachite_gossip::{spawn, Config, CtrlMsg, Handle, HandleEvent, Keypair}; + +use super::{Msg, Network, PeerId}; + +#[async_trait::async_trait] +impl Network for Handle { + async fn recv(&mut self) -> Option<(PeerId, Msg)> { + loop { + match Handle::recv(self).await { + Some(HandleEvent::Message(peer_id, data)) => { + let msg = Msg::from_network_bytes(&data).unwrap(); + let peer_id = PeerId::new(peer_id.to_string()); + return Some((peer_id, msg)); + } + _ => continue, + } + } + } + + async fn broadcast(&mut self, msg: Msg) { + let data = msg.to_network_bytes().unwrap(); + Handle::broadcast(self, data).await.unwrap(); + } +} diff --git a/code/node/src/node.rs b/code/node/src/node.rs index 568bca3e5..659debb8e 100644 --- a/code/node/src/node.rs +++ b/code/node/src/node.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use std::time::Instant; use tokio::sync::{mpsc, oneshot}; +#[allow(unused_imports)] use tracing::{debug, error_span, info, warn, Instrument}; use malachite_common::{ @@ -286,28 +287,33 @@ where match msg { NetworkMsg::Vote(signed_vote) => { let signed_vote = SignedVote::::from_proto(signed_vote).unwrap(); - let peer = self.params.peers.get(&peer_id).unwrap(); // FIXME - - if self.ctx.verify_signed_vote(&signed_vote, &peer.public_key) { - tx_input.send(Input::Vote(signed_vote.vote)).unwrap(); - } else { - warn!("Invalid vote from peer {peer_id}: {signed_vote:?}"); - } + tx_input.send(Input::Vote(signed_vote.vote)).unwrap(); + + // let peer = self.params.peers.get(&peer_id).unwrap(); // FIXME + // if self.ctx.verify_signed_vote(&signed_vote, &peer.public_key) { + // tx_input.send(Input::Vote(signed_vote.vote)).unwrap(); + // } else { + // warn!("Invalid vote from peer {peer_id}: {signed_vote:?}"); + // } } NetworkMsg::Proposal(proposal) => { let signed_proposal = SignedProposal::::from_proto(proposal).unwrap(); - let peer = self.params.peers.get(&peer_id).unwrap(); // FIXME - - let valid = self - .ctx - .verify_signed_proposal(&signed_proposal, &peer.public_key); - tx_input - .send(Input::Proposal( - signed_proposal.proposal, - Validity::from_valid(valid), - )) + .send(Input::Proposal(signed_proposal.proposal, Validity::Valid)) .unwrap(); + + // let peer = self.params.peers.get(&peer_id).unwrap(); // FIXME + // + // let valid = self + // .ctx + // .verify_signed_proposal(&signed_proposal, &peer.public_key); + // + // tx_input + // .send(Input::Proposal( + // signed_proposal.proposal, + // Validity::from_valid(valid), + // )) + // .unwrap(); } #[cfg(test)] diff --git a/code/node/src/peers.rs b/code/node/src/peers.rs index 2022b6b6c..59f87b21a 100644 --- a/code/node/src/peers.rs +++ b/code/node/src/peers.rs @@ -8,7 +8,7 @@ use crate::config::{Config, PeerConfig}; use crate::network::broadcast::PeerInfo; use crate::network::PeerId; -#[derive_where(Clone, Debug)] +#[derive_where(Clone, Debug, Default)] pub struct Peers { pub peers: Vec>, } diff --git a/code/node/src/util/make_node.rs b/code/node/src/util/make_node.rs index 1b628514d..ac9cd74f2 100644 --- a/code/node/src/util/make_node.rs +++ b/code/node/src/util/make_node.rs @@ -5,17 +5,17 @@ use malachite_test::utils::FixedProposer; use malachite_test::{Address, Height, PrivateKey, TestContext, ValidatorSet}; use crate::network::broadcast; -use crate::network::broadcast::PeerInfo; +use crate::network::gossip; use crate::node::{Node, Params}; use crate::peers::Peers; use crate::timers; use crate::value::test::TestValueBuilder; -pub async fn make_node( +pub async fn make_broadcast_node( validator_set: ValidatorSet, private_key: PrivateKey, address: Address, - peer_info: PeerInfo, + peer_info: broadcast::PeerInfo, peers: Peers, ) -> Node { let start_height = Height::new(1); @@ -56,3 +56,42 @@ pub async fn make_node( Node::new(ctx, params, handle, timers_config) } + +pub async fn make_gossip_node( + validator_set: ValidatorSet, + private_key: PrivateKey, + address: Address, +) -> Node { + let keypair = gossip::Keypair::ed25519_from_bytes(private_key.inner().to_bytes()).unwrap(); + + let start_height = Height::new(1); + let ctx = TestContext::new(private_key); + let proposer_selector = Arc::new(FixedProposer::new(validator_set.validators[0].address)); + let proposal_builder = Arc::new(TestValueBuilder::default()); + + let params = Params { + start_height, + proposer_selector, + proposal_builder, + validator_set, + address, + peers: Default::default(), + threshold_params: Default::default(), + }; + + let timers_config = timers::Config { + propose_timeout: Duration::from_secs(3), + prevote_timeout: Duration::from_secs(1), + precommit_timeout: Duration::from_secs(1), + commit_timeout: Duration::from_secs(1), + }; + + let config = malachite_gossip::Config::default(); + + let addr = "/ip4/0.0.0.0/tcp/0".parse().unwrap(); + let handle = malachite_gossip::spawn(keypair, addr, config) + .await + .unwrap(); + + Node::new(ctx, params, handle, timers_config) +} diff --git a/code/node/src/util/mod.rs b/code/node/src/util/mod.rs index c25fd0e1c..ec2ad7abd 100644 --- a/code/node/src/util/mod.rs +++ b/code/node/src/util/mod.rs @@ -1,5 +1,5 @@ mod make_node; -pub use make_node::make_node; +pub use make_node::{make_broadcast_node, make_gossip_node}; mod make_config; pub use make_config::make_config; diff --git a/code/node/tests/broadcast_n3f0.rs b/code/node/tests/broadcast_n3f0.rs index dcead5db7..f2f4c234b 100644 --- a/code/node/tests/broadcast_n3f0.rs +++ b/code/node/tests/broadcast_n3f0.rs @@ -1,5 +1,5 @@ use malachite_common::Round; -use malachite_node::util::{make_config, make_node}; +use malachite_node::util::{make_broadcast_node, make_config}; use malachite_test::utils::make_validators; use malachite_test::{Height, ValidatorSet, Value}; @@ -27,7 +27,7 @@ pub async fn decide_on_value() { let peer_info = peer_config.peer_info(); let vs = ValidatorSet::new(vs); - let node = tokio::spawn(make_node( + let node = tokio::spawn(make_broadcast_node( vs, my_sk, my_addr, diff --git a/code/node/tests/gossip_n10f0.rs b/code/node/tests/gossip_n10f0.rs new file mode 100644 index 000000000..22f01c892 --- /dev/null +++ b/code/node/tests/gossip_n10f0.rs @@ -0,0 +1,54 @@ +use malachite_common::Round; +use malachite_node::util::make_gossip_node; +use malachite_test::utils::make_validators; +use malachite_test::{Height, ValidatorSet, Value}; +use tokio::time::{sleep, Duration}; + +#[tokio::test] +pub async fn decide_on_value() { + tracing_subscriber::fmt::init(); + + let voting_powers = [5, 20, 10, 30, 15, 1, 5, 25, 10, 15]; + + // Validators keys are deterministic and match the ones in the config file + let vals_and_keys = make_validators(voting_powers); + let vs = ValidatorSet::new(vals_and_keys.iter().map(|(v, _)| v.clone())); + + let mut handles = Vec::with_capacity(vals_and_keys.len()); + + for (v, sk) in vals_and_keys { + let node = tokio::spawn(make_gossip_node(vs.clone(), sk, v.address)); + handles.push(node); + } + + sleep(Duration::from_secs(3)).await; + + let mut nodes = Vec::with_capacity(handles.len()); + + for handle in handles { + let node = handle.await.expect("Error: node failed to start"); + nodes.push(node); + } + + let mut handles = Vec::with_capacity(nodes.len()); + + for node in nodes { + let handle = node.run().await; + handles.push(handle); + } + + for height in 1..=3 { + for handle in &mut handles { + let decision = handle.wait_decision().await; + + assert_eq!( + decision, + Some((Height::new(height), Round::new(0), Value::new(40 + height))) + ); + } + } + + for handle in handles { + handle.abort(); + } +} diff --git a/code/test/src/signing.rs b/code/test/src/signing.rs index 94b52af82..b13be62e7 100644 --- a/code/test/src/signing.rs +++ b/code/test/src/signing.rs @@ -51,6 +51,11 @@ impl PrivateKey { pub fn public_key(&self) -> PublicKey { PublicKey::new(self.0.verification_key()) } + + #[cfg_attr(coverage_nightly, coverage(off))] + pub fn inner(&self) -> &ed25519_consensus::SigningKey { + &self.0 + } } impl Signer for PrivateKey { From 59c80ed6bf57e5e61938d611c4bbeb1637cc29f4 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Mon, 26 Feb 2024 17:45:11 +0100 Subject: [PATCH 43/50] Change CLI to use gossip layer --- code/node/bin/cli.rs | 17 ----------------- code/node/bin/main.rs | 37 ++++++++++--------------------------- 2 files changed, 10 insertions(+), 44 deletions(-) delete mode 100644 code/node/bin/cli.rs diff --git a/code/node/bin/cli.rs b/code/node/bin/cli.rs deleted file mode 100644 index 919dab5c2..000000000 --- a/code/node/bin/cli.rs +++ /dev/null @@ -1,17 +0,0 @@ -use malachite_node::network::PeerId; - -pub struct Cli { - pub peer_id: PeerId, -} - -impl Cli { - pub fn from_env() -> Self { - let peer_id = std::env::args() - .nth(1) - .expect("Usage: node ") - .parse() - .expect("Error: Invalid PEER_ID"); - - Self { peer_id } - } -} diff --git a/code/node/bin/main.rs b/code/node/bin/main.rs index b5daceeef..4158c0e52 100644 --- a/code/node/bin/main.rs +++ b/code/node/bin/main.rs @@ -1,54 +1,37 @@ -use malachite_node::util::make_broadcast_node; -use malachite_node::util::make_config; +use malachite_node::util::make_gossip_node; use malachite_test::utils::make_validators; use malachite_test::ValidatorSet; use tracing::info; -mod cli; -use cli::Cli; - const VOTING_POWERS: [u64; 3] = [5, 20, 10]; #[tokio::main(flavor = "current_thread")] pub async fn main() { tracing_subscriber::fmt::init(); - let args = Cli::from_env(); + let index: usize = std::env::args() + .nth(1) + .expect("Error: missing index") + .parse() + .expect("Error: invalid index"); // Validators keys are deterministic and match the ones in the config file let vs = make_validators(VOTING_POWERS); - let config = make_config(vs.iter().map(|(v, _)| v)); - - let peer_config = config - .peers - .iter() - .find(|p| p.id == args.peer_id) - .expect("Error: invalid peer id"); - - let (my_sk, my_addr) = vs - .iter() - .find(|(v, _)| v.public_key == peer_config.public_key) - .map(|(v, pk)| (pk.clone(), v.address)) - .expect("Error: invalid peer id"); + let (val, sk) = vs[index].clone(); let (vs, _): (Vec<_>, Vec<_>) = vs.into_iter().unzip(); - - let peer_info = peer_config.peer_info(); let vs = ValidatorSet::new(vs); - let node = make_broadcast_node(vs, my_sk, my_addr, peer_info, config.into()).await; + let node = make_gossip_node(vs, sk, val.address).await; - info!("[{}] Starting...", args.peer_id); + info!("[{index}] Starting..."); let mut handle = node.run().await; loop { if let Some((height, round, value)) = handle.wait_decision().await { - info!( - "[{}] Decision at height {height} and round {round}: {value:?}", - args.peer_id - ); + info!("[{index}] Decision at height {height} and round {round}: {value:?}",); } } } From d67d73e8b28dc965bcfd5ec905d9e018c6876a69 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 27 Feb 2024 10:18:43 +0100 Subject: [PATCH 44/50] Fix tests on CI --- .github/workflows/rust.yml | 2 +- code/Cargo.toml | 1 - code/node/Cargo.toml | 5 -- code/node/src/network/broadcast.rs | 78 +++++++------------ .../{broadcast_n3f0.rs => broadcast_n10f0.rs} | 0 code/node/tests/gossip_n10f0.rs | 2 +- 6 files changed, 30 insertions(+), 58 deletions(-) rename code/node/tests/{broadcast_n3f0.rs => broadcast_n10f0.rs} (100%) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index af0e39d1d..daec955c9 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -46,7 +46,7 @@ jobs: - name: Build code run: cargo nextest run --workspace --all-features --no-run - name: Run tests - run: cargo nextest run --workspace --all-features + run: cargo nextest run --workspace --all-features --no-capture clippy: name: Clippy diff --git a/code/Cargo.toml b/code/Cargo.toml index ba2c0805e..2351bf64e 100644 --- a/code/Cargo.toml +++ b/code/Cargo.toml @@ -53,6 +53,5 @@ thiserror = "1.0" tokio = "1.35.1" tokio-stream = "0.1" toml = "0.8.10" -turmoil = "0.6.1" tracing = "0.1.40" tracing-subscriber = "0.3.18" diff --git a/code/node/Cargo.toml b/code/node/Cargo.toml index 3f9f01172..621bd45cd 100644 --- a/code/node/Cargo.toml +++ b/code/node/Cargo.toml @@ -12,9 +12,6 @@ publish.workspace = true name = "malachite-node" path = "bin/main.rs" -[features] -turmoil = ["dep:turmoil"] - [dependencies] malachite-common.workspace = true malachite-driver.workspace = true @@ -37,7 +34,5 @@ toml = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["fmt"] } -turmoil = { workspace = true, optional = true } - [dev-dependencies] malachite-test.workspace = true diff --git a/code/node/src/network/broadcast.rs b/code/node/src/network/broadcast.rs index f37101cf1..83b9a6e35 100644 --- a/code/node/src/network/broadcast.rs +++ b/code/node/src/network/broadcast.rs @@ -3,14 +3,9 @@ use std::fmt::Debug; use std::net::SocketAddr; use std::time::{Duration, Instant}; -#[cfg(not(feature = "turmoil"))] -use tokio::net::{TcpListener, TcpStream}; - -#[cfg(feature = "turmoil")] -use turmoil::net::{TcpListener, TcpStream}; - use futures::channel::oneshot; use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpListener, TcpStream}; use tokio::sync::{broadcast, mpsc}; use tracing::{debug, error, info, warn}; @@ -353,7 +348,6 @@ impl Network for Handle { } #[cfg(test)] -#[allow(unused_imports)] mod tests { use std::sync::atomic::{AtomicUsize, Ordering}; @@ -361,51 +355,44 @@ mod tests { use super::*; - #[test] - #[cfg(feature = "turmoil")] - fn test_peer() { + #[tokio::test] + async fn test_peer() { use std::net::{IpAddr, Ipv4Addr}; use std::sync::Arc; - use turmoil::lookup; tracing_subscriber::fmt().init(); let peer1_id = PeerId("peer-1".to_string()); let peer1_info = PeerInfo { id: peer1_id.clone(), - addr: (IpAddr::from(Ipv4Addr::UNSPECIFIED), 12001).into(), + addr: (IpAddr::from(Ipv4Addr::LOCALHOST), 12001).into(), }; let peer2_id = PeerId("peer-2".to_string()); let peer2_info = PeerInfo { id: peer2_id.clone(), - addr: (IpAddr::from(Ipv4Addr::UNSPECIFIED), 12002).into(), + addr: (IpAddr::from(Ipv4Addr::LOCALHOST), 12002).into(), }; let peer3_id = PeerId("peer-3".to_string()); let peer3_info = PeerInfo { id: peer3_id.clone(), - addr: (IpAddr::from(Ipv4Addr::UNSPECIFIED), 12003).into(), + addr: (IpAddr::from(Ipv4Addr::LOCALHOST), 12003).into(), }; let peer1: Peer = Peer::new(peer1_info.clone()); let peer2: Peer = Peer::new(peer2_info.clone()); let peer3: Peer = Peer::new(peer3_info.clone()); - let mut sim = turmoil::Builder::new().build(); - let done = Arc::new(AtomicUsize::new(0)); let deadline = Duration::from_millis(200); - { + let handle1 = { let done = Arc::clone(&done); - let mut peer2_info = peer2_info.clone(); - let mut peer3_info = peer3_info.clone(); - - sim.client("peer1", async move { - peer2_info.addr.set_ip(lookup("peer2")); - peer3_info.addr.set_ip(lookup("peer3")); + let peer2_info = peer2_info.clone(); + let peer3_info = peer3_info.clone(); + tokio::spawn(async move { let handle1 = peer1.run().await; handle1.connect_to_peer(peer2_info, None).await; handle1.connect_to_peer(peer3_info, None).await; @@ -416,22 +403,17 @@ mod tests { handle1.broadcast(Msg::Dummy(1)).await; handle1.broadcast(Msg::Dummy(2)).await; + }) + }; - Ok(()) - }); - } - - { + let handle2 = { let done = Arc::clone(&done); let peer1_id = peer1_id.clone(); - let mut peer1_info = peer1_info.clone(); - let mut peer3_info = peer3_info.clone(); - - sim.client("peer2", async move { - peer1_info.addr.set_ip(lookup("peer1")); - peer3_info.addr.set_ip(lookup("peer3")); + let peer1_info = peer1_info.clone(); + let peer3_info = peer3_info.clone(); + tokio::spawn(async move { let mut handle2 = peer2.run().await; handle2.connect_to_peer(peer1_info, None).await; handle2.connect_to_peer(peer3_info, None).await; @@ -443,20 +425,15 @@ mod tests { let msg2 = timeout(deadline, handle2.recv()).await.unwrap(); assert_eq!(msg2, Some((peer1_id.clone(), Msg::Dummy(2)))); + }) + }; - Ok(()) - }); - } - - { + let handle3 = { let done = Arc::clone(&done); - let mut peer1_info = peer1_info.clone(); - let mut peer2_info = peer2_info.clone(); - - sim.client("peer3", async move { - peer1_info.addr.set_ip(lookup("peer1")); - peer2_info.addr.set_ip(lookup("peer2")); + let peer1_info = peer1_info.clone(); + let peer2_info = peer2_info.clone(); + tokio::spawn(async move { let mut handle3 = peer3.run().await; handle3.connect_to_peer(peer1_info, None).await; handle3.connect_to_peer(peer2_info, None).await; @@ -468,11 +445,12 @@ mod tests { let msg2 = timeout(deadline, handle3.recv()).await.unwrap(); assert_eq!(msg2, Some((peer1_id.clone(), Msg::Dummy(2)))); + }) + }; - Ok(()) - }); - } - - sim.run().unwrap(); + let (res1, res2, res3) = tokio::join!(handle1, handle2, handle3); + res1.unwrap(); + res2.unwrap(); + res3.unwrap(); } } diff --git a/code/node/tests/broadcast_n3f0.rs b/code/node/tests/broadcast_n10f0.rs similarity index 100% rename from code/node/tests/broadcast_n3f0.rs rename to code/node/tests/broadcast_n10f0.rs diff --git a/code/node/tests/gossip_n10f0.rs b/code/node/tests/gossip_n10f0.rs index 22f01c892..530117ce5 100644 --- a/code/node/tests/gossip_n10f0.rs +++ b/code/node/tests/gossip_n10f0.rs @@ -21,7 +21,7 @@ pub async fn decide_on_value() { handles.push(node); } - sleep(Duration::from_secs(3)).await; + sleep(Duration::from_secs(5)).await; let mut nodes = Vec::with_capacity(handles.len()); From c732936a8504ce6accf66dd277f2a02490b617a4 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 27 Feb 2024 20:10:01 +0100 Subject: [PATCH 45/50] Add validator address to proposals --- code/common/src/context.rs | 1 + code/common/src/proposal.rs | 3 + code/itf/tests/consensus/runner.rs | 5 + code/proto/src/malachite.proto | 1 + code/round/src/output.rs | 3 +- code/round/src/state_machine.rs | 19 +++- code/test/src/context.rs | 10 +- code/test/src/proposal.rs | 22 +++- code/test/src/utils.rs | 12 ++- code/test/tests/driver.rs | 40 +++++++- code/test/tests/driver_extra.rs | 155 ++++++++++++++++++++++------- code/test/tests/round.rs | 17 +++- 12 files changed, 233 insertions(+), 55 deletions(-) diff --git a/code/common/src/context.rs b/code/common/src/context.rs index 46c93a70c..51e3241d7 100644 --- a/code/common/src/context.rs +++ b/code/common/src/context.rs @@ -59,6 +59,7 @@ where round: Round, value: Self::Value, pol_round: Round, + address: Self::Address, ) -> Self::Proposal; /// Build a new prevote vote by the validator with the given address, diff --git a/code/common/src/proposal.rs b/code/common/src/proposal.rs index d63168493..c2ba96160 100644 --- a/code/common/src/proposal.rs +++ b/code/common/src/proposal.rs @@ -19,4 +19,7 @@ where /// The POL round for which the proposal is for. fn pol_round(&self) -> Round; + + /// Address of the validator who issued this proposal + fn validator_address(&self) -> &Ctx::Address; } diff --git a/code/itf/tests/consensus/runner.rs b/code/itf/tests/consensus/runner.rs index f45663d50..ac49d3a71 100644 --- a/code/itf/tests/consensus/runner.rs +++ b/code/itf/tests/consensus/runner.rs @@ -81,6 +81,7 @@ impl ItfRunner for ConsensusRunner { input_round, value_from_model(value).unwrap(), Round::Nil, + *some_other_node, ); (data, Input::Proposal(proposal)) } @@ -92,6 +93,7 @@ impl ItfRunner for ConsensusRunner { actual.round, value_from_model(value).unwrap(), Round::new(*valid_round), + *some_other_node, ); (data, Input::ProposalAndPolkaPrevious(proposal)) } @@ -103,6 +105,7 @@ impl ItfRunner for ConsensusRunner { actual.round, value_from_model(value).unwrap(), Round::Nil, + *some_other_node, ); (data, Input::ProposalAndPolkaCurrent(proposal)) } @@ -115,6 +118,7 @@ impl ItfRunner for ConsensusRunner { input_round, value_from_model(value).unwrap(), Round::Nil, + *some_other_node, ); (data, Input::InvalidProposalAndPolkaPrevious(proposal)) } @@ -126,6 +130,7 @@ impl ItfRunner for ConsensusRunner { actual.round, value_from_model(value).unwrap(), Round::Nil, + *some_other_node, ); (data, Input::ProposalAndPrecommitValue(proposal)) } diff --git a/code/proto/src/malachite.proto b/code/proto/src/malachite.proto index 022e499d9..8553e9c27 100644 --- a/code/proto/src/malachite.proto +++ b/code/proto/src/malachite.proto @@ -45,6 +45,7 @@ message Proposal { Round round = 2; Value value = 3; Round pol_round = 4; + Address validator_address = 5; } message SignedProposal { diff --git a/code/round/src/output.rs b/code/round/src/output.rs index 46ff981ed..c90972476 100644 --- a/code/round/src/output.rs +++ b/code/round/src/output.rs @@ -39,8 +39,9 @@ impl Output { round: Round, value: Ctx::Value, pol_round: Round, + address: Ctx::Address, ) -> Self { - Output::Proposal(Ctx::new_proposal(height, round, value, pol_round)) + Output::Proposal(Ctx::new_proposal(height, round, value, pol_round, address)) } /// Build a `Vote` output for a prevote. diff --git a/code/round/src/state_machine.rs b/code/round/src/state_machine.rs index 72426f69a..0ea33aba6 100644 --- a/code/round/src/state_machine.rs +++ b/code/round/src/state_machine.rs @@ -84,7 +84,7 @@ where state.round = round; // We are the proposer - propose_valid_or_get_value(state) + propose_valid_or_get_value(state, info.address) } // L11/L20 @@ -104,7 +104,7 @@ where (Step::Propose, Input::ProposeValue(value)) if this_round => { debug_assert!(info.is_proposer()); - propose(state, value) + propose(state, value, info.address) } // L22 with valid proposal @@ -227,7 +227,7 @@ where /// and ask for a value. /// /// Ref: L15-L18 -pub fn propose_valid_or_get_value(state: State) -> Transition +pub fn propose_valid_or_get_value(state: State, address: &Ctx::Address) -> Transition where Ctx: Context, { @@ -239,7 +239,9 @@ where state.round, round_value.value.clone(), pol_round, + address.clone(), ); + Transition::to(state.with_step(Step::Propose)).with_output(proposal) } None => { @@ -257,11 +259,18 @@ where /// otherwise propose the given value. /// /// Ref: L11/L14 -pub fn propose(state: State, value: Ctx::Value) -> Transition +pub fn propose(state: State, value: Ctx::Value, address: &Ctx::Address) -> Transition where Ctx: Context, { - let proposal = Output::proposal(state.height, state.round, value, Round::Nil); + let proposal = Output::proposal( + state.height, + state.round, + value, + Round::Nil, + address.clone(), + ); + Transition::to(state.with_step(Step::Propose)).with_output(proposal) } diff --git a/code/test/src/context.rs b/code/test/src/context.rs index 00d21aba4..28a57af72 100644 --- a/code/test/src/context.rs +++ b/code/test/src/context.rs @@ -70,8 +70,14 @@ impl Context for TestContext { .is_ok() } - fn new_proposal(height: Height, round: Round, value: Value, pol_round: Round) -> Proposal { - Proposal::new(height, round, value, pol_round) + fn new_proposal( + height: Height, + round: Round, + value: Value, + pol_round: Round, + address: Address, + ) -> Proposal { + Proposal::new(height, round, value, pol_round, address) } fn new_prevote( diff --git a/code/test/src/proposal.rs b/code/test/src/proposal.rs index 222470eb0..ceeff158a 100644 --- a/code/test/src/proposal.rs +++ b/code/test/src/proposal.rs @@ -1,7 +1,7 @@ use malachite_common::Round; use malachite_proto::{self as proto}; -use crate::{Height, TestContext, Value}; +use crate::{Address, Height, TestContext, Value}; /// A proposal for a value in a round #[derive(Clone, Debug, PartialEq, Eq)] @@ -10,15 +10,23 @@ pub struct Proposal { pub round: Round, pub value: Value, pub pol_round: Round, + pub validator_address: Address, } impl Proposal { - pub fn new(height: Height, round: Round, value: Value, pol_round: Round) -> Self { + pub fn new( + height: Height, + round: Round, + value: Value, + pol_round: Round, + validator_address: Address, + ) -> Self { Self { height, round, value, pol_round, + validator_address, } } @@ -43,6 +51,10 @@ impl malachite_common::Proposal for Proposal { fn pol_round(&self) -> Round { self.pol_round } + + fn validator_address(&self) -> &Address { + &self.validator_address + } } impl proto::Protobuf for Proposal { @@ -54,6 +66,7 @@ impl proto::Protobuf for Proposal { round: Some(self.round.to_proto()?), value: Some(self.value.to_proto()?), pol_round: Some(self.pol_round.to_proto()?), + validator_address: Some(self.validator_address.to_proto()?), }) } @@ -79,6 +92,11 @@ impl proto::Protobuf for Proposal { .pol_round .ok_or_else(|| proto::Error::missing_field::("pol_round"))?, )?, + validator_address: Address::from_proto( + proto.validator_address.ok_or_else(|| { + proto::Error::missing_field::("validator_address") + })?, + )?, }) } } diff --git a/code/test/src/utils.rs b/code/test/src/utils.rs index 8fd4b6a1b..25c583f58 100644 --- a/code/test/src/utils.rs +++ b/code/test/src/utils.rs @@ -70,8 +70,13 @@ pub fn new_round_output(round: Round) -> Output { Output::NewRound(Height::new(1), round) } -pub fn proposal_output(round: Round, value: Value, locked_round: Round) -> Output { - let proposal = Proposal::new(Height::new(1), round, value, locked_round); +pub fn proposal_output( + round: Round, + value: Value, + locked_round: Round, + address: Address, +) -> Output { + let proposal = Proposal::new(Height::new(1), round, value, locked_round, address); Output::Propose(proposal) } @@ -80,8 +85,9 @@ pub fn proposal_input( value: Value, locked_round: Round, validity: Validity, + address: Address, ) -> Input { - let proposal = Proposal::new(Height::new(1), round, value, locked_round); + let proposal = Proposal::new(Height::new(1), round, value, locked_round, address); Input::Proposal(proposal, validity) } diff --git a/code/test/tests/driver.rs b/code/test/tests/driver.rs index 9b91ed5ea..309fdb382 100644 --- a/code/test/tests/driver.rs +++ b/code/test/tests/driver.rs @@ -41,7 +41,13 @@ fn driver_steps_proposer() { let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); - let proposal = Proposal::new(Height::new(1), Round::new(0), value, Round::new(-1)); + let proposal = Proposal::new( + Height::new(1), + Round::new(0), + value, + Round::new(-1), + my_addr, + ); let steps = vec![ TestStep { @@ -310,7 +316,13 @@ fn driver_steps_not_proposer_valid() { let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); - let proposal = Proposal::new(Height::new(1), Round::new(0), value, Round::new(-1)); + let proposal = Proposal::new( + Height::new(1), + Round::new(0), + value, + Round::new(-1), + v1.address, + ); let steps = vec![ TestStep { @@ -500,7 +512,13 @@ fn driver_steps_not_proposer_invalid() { let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); - let proposal = Proposal::new(Height::new(1), Round::new(0), value, Round::new(-1)); + let proposal = Proposal::new( + Height::new(1), + Round::new(0), + value, + Round::new(-1), + v1.address, + ); let steps = vec![ TestStep { @@ -617,7 +635,13 @@ fn driver_steps_not_proposer_other_height() { let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); // Proposal is for another height - let proposal = Proposal::new(Height::new(2), Round::new(0), value, Round::new(-1)); + let proposal = Proposal::new( + Height::new(2), + Round::new(0), + value, + Round::new(-1), + v1.address, + ); let steps = vec![ TestStep { @@ -670,7 +694,13 @@ fn driver_steps_not_proposer_other_round() { let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); // Proposal is for another round - let proposal = Proposal::new(Height::new(1), Round::new(1), value, Round::new(-1)); + let proposal = Proposal::new( + Height::new(1), + Round::new(1), + value, + Round::new(-1), + v2.address, + ); let steps = vec![ TestStep { diff --git a/code/test/tests/driver_extra.rs b/code/test/tests/driver_extra.rs index f0f575397..c41937002 100644 --- a/code/test/tests/driver_extra.rs +++ b/code/test/tests/driver_extra.rs @@ -108,7 +108,13 @@ fn driver_steps_decide_current_with_no_locked_no_valid() { }, TestStep { desc: "Receive proposal", - input: proposal_input(Round::new(0), value, Round::Nil, Validity::Valid), + input: proposal_input( + Round::new(0), + value, + Round::Nil, + Validity::Valid, + v1.address, + ), expected_outputs: vec![decide_output(Round::new(0), value)], expected_round: Round::new(0), new_state: decided_state(Round::new(0), value), @@ -205,7 +211,13 @@ fn driver_steps_decide_previous_with_no_locked_no_valid() { }, TestStep { desc: "Receive proposal", - input: proposal_input(Round::new(0), value, Round::Nil, Validity::Valid), + input: proposal_input( + Round::new(0), + value, + Round::Nil, + Validity::Valid, + v1.address, + ), expected_outputs: vec![decide_output(Round::new(0), value)], expected_round: Round::new(1), new_state: decided_state(Round::new(1), value), @@ -250,6 +262,8 @@ fn driver_steps_decide_previous_with_locked_and_valid() { let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); + let proposal = Proposal::new(Height::new(1), Round::new(0), value, Round::Nil, v1.address); + let steps = vec![ TestStep { desc: "Start round 0, we, v3, are not the proposer, start timeout propose", @@ -281,7 +295,13 @@ fn driver_steps_decide_previous_with_locked_and_valid() { }, TestStep { desc: "Receive proposal, L37-L43", - input: proposal_input(Round::new(0), value, Round::Nil, Validity::Valid), + input: proposal_input( + Round::new(0), + value, + Round::Nil, + Validity::Valid, + v1.address, + ), expected_outputs: vec![precommit_output( Round::new(0), Value::new(9999), @@ -290,7 +310,7 @@ fn driver_steps_decide_previous_with_locked_and_valid() { expected_round: Round::new(0), new_state: precommit_state_with_proposal_and_locked_and_valid( Round::new(0), - Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), + proposal.clone(), ), }, TestStep { @@ -300,7 +320,7 @@ fn driver_steps_decide_previous_with_locked_and_valid() { expected_round: Round::new(1), new_state: new_round_with_proposal_and_locked_and_valid( Round::new(1), - Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), + proposal.clone(), ), }, TestStep { @@ -310,7 +330,7 @@ fn driver_steps_decide_previous_with_locked_and_valid() { expected_round: Round::new(1), new_state: propose_state_with_proposal_and_locked_and_valid( Round::new(1), - Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), + proposal.clone(), ), }, TestStep { @@ -320,7 +340,7 @@ fn driver_steps_decide_previous_with_locked_and_valid() { expected_round: Round::new(1), new_state: propose_state_with_proposal_and_locked_and_valid( Round::new(1), - Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), + proposal.clone(), ), }, TestStep { @@ -330,7 +350,7 @@ fn driver_steps_decide_previous_with_locked_and_valid() { expected_round: Round::new(1), new_state: decided_state_with_proposal_and_locked_and_valid( Round::new(1), - Proposal::new(Height::new(1), Round::new(1), value, Round::Nil), + proposal.clone(), ), }, ]; @@ -385,7 +405,13 @@ fn driver_steps_polka_previous_with_locked() { }, TestStep { desc: "receive a proposal from v1 - L22 send prevote", - input: proposal_input(Round::new(0), value, Round::Nil, Validity::Valid), + input: proposal_input( + Round::new(0), + value, + Round::Nil, + Validity::Valid, + v1.address, + ), expected_outputs: vec![prevote_output(Round::new(0), &my_addr)], expected_round: Round::new(0), new_state: prevote_state( @@ -410,7 +436,7 @@ fn driver_steps_polka_previous_with_locked() { expected_round: Round::new(0), new_state: precommit_state_with_proposal_and_locked_and_valid( Round::new(0), - Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), + Proposal::new(Height::new(1), Round::new(0), value, Round::Nil, v1.address), ), }, TestStep { @@ -420,27 +446,44 @@ fn driver_steps_polka_previous_with_locked() { expected_round: Round::new(1), new_state: new_round_with_proposal_and_locked_and_valid( Round::new(1), - Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), + Proposal::new(Height::new(1), Round::new(0), value, Round::Nil, v1.address), ), }, TestStep { desc: "start round 1, we are proposer with a valid value, propose it", input: new_round_input(Round::new(1)), - expected_outputs: vec![proposal_output(Round::new(1), value, Round::new(0))], + expected_outputs: vec![proposal_output( + Round::new(1), + value, + Round::new(0), + v2.address, + )], expected_round: Round::new(1), new_state: propose_state_with_proposal_and_locked_and_valid( Round::new(1), - Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), + Proposal::new(Height::new(1), Round::new(0), value, Round::Nil, v2.address), ), }, TestStep { desc: "Receive our own proposal", - input: proposal_input(Round::new(1), value, Round::new(0), Validity::Valid), - expected_outputs: vec![prevote_output(Round::new(1), &my_addr)], + input: proposal_input( + Round::new(1), + value, + Round::new(0), + Validity::Valid, + v1.address, + ), + expected_outputs: vec![prevote_output(Round::new(1), &v2.address)], expected_round: Round::new(1), new_state: prevote_state_with_proposal_and_locked_and_valid( Round::new(1), - Proposal::new(Height::new(1), Round::new(1), value, Round::new(0)), + Proposal::new( + Height::new(1), + Round::new(1), + value, + Round::new(0), + v2.address, + ), ), }, ]; @@ -528,7 +571,13 @@ fn driver_steps_polka_previous_invalid_proposal() { }, TestStep { desc: "receive an invalid proposal for POL round 0", - input: proposal_input(Round::new(1), value, Round::new(0), Validity::Invalid), + input: proposal_input( + Round::new(1), + value, + Round::new(0), + Validity::Invalid, + v1.address, + ), expected_outputs: vec![prevote_nil_output(Round::new(1), &my_addr)], expected_round: Round::new(1), new_state: prevote_state(Round::new(1)), @@ -584,7 +633,13 @@ fn driver_steps_polka_previous_new_proposal() { }, TestStep { desc: "receive a valid proposal for round 0", - input: proposal_input(Round::new(0), value, Round::Nil, Validity::Valid), + input: proposal_input( + Round::new(0), + value, + Round::Nil, + Validity::Valid, + v1.address, + ), expected_outputs: vec![prevote_output(Round::new(0), &my_addr)], expected_round: Round::new(0), new_state: prevote_state(Round::new(0)), @@ -603,7 +658,7 @@ fn driver_steps_polka_previous_new_proposal() { expected_round: Round::new(0), new_state: precommit_state_with_proposal_and_locked_and_valid( Round::new(0), - Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), + Proposal::new(Height::new(1), Round::new(0), value, Round::Nil, v1.address), ), }, TestStep { @@ -613,7 +668,7 @@ fn driver_steps_polka_previous_new_proposal() { expected_round: Round::new(1), new_state: new_round_with_proposal_and_locked_and_valid( Round::new(1), - Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), + Proposal::new(Height::new(1), Round::new(0), value, Round::Nil, v1.address), ), }, TestStep { @@ -623,17 +678,29 @@ fn driver_steps_polka_previous_new_proposal() { expected_round: Round::new(1), new_state: propose_state_with_proposal_and_locked_and_valid( Round::new(1), - Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), + Proposal::new(Height::new(1), Round::new(0), value, Round::Nil, v1.address), ), }, TestStep { desc: "receive a valid proposal for round 1 with different value", - input: proposal_input(Round::new(1), other_value, Round::Nil, Validity::Valid), + input: proposal_input( + Round::new(1), + other_value, + Round::Nil, + Validity::Valid, + v1.address, + ), expected_outputs: vec![prevote_nil_output(Round::new(1), &my_addr)], expected_round: Round::new(1), new_state: prevote_state_with_proposal_and_locked_and_valid( Round::new(1), - Proposal::new(Height::new(1), Round::new(1), value, Round::new(0)), + Proposal::new( + Height::new(1), + Round::new(1), + value, + Round::new(0), + v1.address, + ), ), }, ]; @@ -694,7 +761,7 @@ fn driver_steps_polka_previous_with_no_locked() { new_state: propose_state(Round::new(0)), }, TestStep { - desc: "Timeout propopse, prevote for nil (v2)", + desc: "Timeout propose, prevote for nil (v2)", input: timeout_propose_input(Round::new(0)), expected_outputs: vec![prevote_nil_output(Round::new(0), &my_addr)], expected_round: Round::new(0), @@ -723,13 +790,19 @@ fn driver_steps_polka_previous_with_no_locked() { }, TestStep { desc: "receive a proposal - L36, we don't lock, we set valid", - input: proposal_input(Round::new(0), value, Round::Nil, Validity::Valid), + input: proposal_input( + Round::new(0), + value, + Round::Nil, + Validity::Valid, + v3.address, + ), expected_outputs: vec![], expected_round: Round::new(0), new_state: precommit_state_with_proposal_and_valid( Round::new(0), Round::new(0), - Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), + Proposal::new(Height::new(1), Round::new(0), value, Round::Nil, v3.address), ), }, TestStep { @@ -739,29 +812,40 @@ fn driver_steps_polka_previous_with_no_locked() { expected_round: Round::new(1), new_state: new_round_with_proposal_and_valid( Round::new(1), - Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), + Proposal::new(Height::new(1), Round::new(0), value, Round::Nil, v3.address), ), }, TestStep { desc: "start round 1, we are proposer with a valid value from round 0, propose it", input: new_round_input(Round::new(1)), - expected_outputs: vec![proposal_output(Round::new(1), value, Round::new(0))], + expected_outputs: vec![proposal_output( + Round::new(1), + value, + Round::new(0), + v2.address, + )], expected_round: Round::new(1), new_state: propose_state_with_proposal_and_valid( Round::new(1), Round::new(0), - Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), + Proposal::new(Height::new(1), Round::new(0), value, Round::Nil, v2.address), ), }, TestStep { desc: "Receive our own proposal, prevote nil as we are not locked on the value", - input: proposal_input(Round::new(1), value, Round::new(0), Validity::Valid), + input: proposal_input( + Round::new(1), + value, + Round::new(0), + Validity::Valid, + v2.address, + ), expected_outputs: vec![prevote_nil_output(Round::new(1), &my_addr)], expected_round: Round::new(1), new_state: prevote_state_with_proposal_and_valid( Round::new(1), Round::new(0), - Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), + Proposal::new(Height::new(1), Round::new(0), value, Round::Nil, v2.address), ), }, ]; @@ -858,7 +942,8 @@ fn driver_steps_polka_value_then_proposal() { let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); - let steps = vec![ + let steps = + vec![ TestStep { desc: "Start round 0, we, v3, are not the proposer, start timeout propose", input: new_round_input(Round::new(0)), @@ -882,7 +967,7 @@ fn driver_steps_polka_value_then_proposal() { }, TestStep { desc: "receive a proposal from v1 - L22 send prevote", - input: proposal_input(Round::new(0), value, Round::Nil, Validity::Valid), + input: proposal_input(Round::new(0), value, Round::Nil, Validity::Valid, v1.address), expected_outputs: vec![ prevote_output(Round::new(0), &my_addr), precommit_output(Round::new(0), value, &my_addr), @@ -890,7 +975,7 @@ fn driver_steps_polka_value_then_proposal() { expected_round: Round::new(0), new_state: precommit_state_with_proposal_and_locked_and_valid( Round::new(0), - Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), + Proposal::new(Height::new(1), Round::new(0), value, Round::Nil, v1.address), ), }, ]; @@ -949,7 +1034,7 @@ fn driver_steps_polka_any_then_proposal_other() { }, TestStep { desc: "receive a proposal from v1 - L22 send prevote, replay polkaAny, start timeout prevote", - input: proposal_input(Round::new(0), value, Round::Nil, Validity::Valid), + input: proposal_input(Round::new(0), value, Round::Nil, Validity::Valid, v1.address), expected_outputs: vec![ prevote_output(Round::new(0), &my_addr), start_prevote_timer_output(Round::new(0)) diff --git a/code/test/tests/round.rs b/code/test/tests/round.rs index ee2dd22fc..4b8a5b297 100644 --- a/code/test/tests/round.rs +++ b/code/test/tests/round.rs @@ -39,7 +39,13 @@ fn test_propose() { assert_eq!(transition.next_state, state); assert_eq!( transition.output.unwrap(), - Output::proposal(Height::new(10), Round::new(0), Value::new(42), Round::Nil) + Output::proposal( + Height::new(10), + Round::new(0), + Value::new(42), + Round::Nil, + ADDRESS + ) ); } @@ -79,6 +85,7 @@ fn test_prevote() { Round::new(1), value, Round::Nil, + OTHER_ADDRESS, )), ); @@ -106,7 +113,13 @@ fn test_input_message_while_commit_step() { ..Default::default() }; - let proposal = Proposal::new(Height::new(1), Round::new(1), value, Round::Nil); + let proposal = Proposal::new( + Height::new(1), + Round::new(1), + value, + Round::Nil, + OTHER_ADDRESS, + ); let data = Info::new(round, &ADDRESS, &OTHER_ADDRESS); From d77f9cf1a29eea940ff8735b31365a7929a069e9 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 27 Feb 2024 20:10:29 +0100 Subject: [PATCH 46/50] Re-add vote and proposal signature verification --- code/node/src/node.rs | 59 ++++++++++++++++++--------------- code/node/src/util/make_node.rs | 2 -- 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/code/node/src/node.rs b/code/node/src/node.rs index 659debb8e..f1d99ebb1 100644 --- a/code/node/src/node.rs +++ b/code/node/src/node.rs @@ -9,7 +9,7 @@ use tracing::{debug, error_span, info, warn, Instrument}; use malachite_common::{ Context, Height, NilOrVal, Proposal, Round, SignedProposal, SignedVote, Timeout, TimeoutStep, - Vote, VoteType, + Validator, ValidatorSet, Vote, VoteType, }; use malachite_driver::{Driver, Input, Output, ProposerSelector, Validity}; use malachite_proto::{self as proto, Protobuf}; @@ -17,7 +17,6 @@ use malachite_vote::{Threshold, ThresholdParams}; use crate::network::Msg as NetworkMsg; use crate::network::{Network, PeerId}; -use crate::peers::Peers; use crate::timers::{self, Timers}; use crate::value::ValueBuilder; @@ -28,7 +27,6 @@ pub struct Params { pub validator_set: Ctx::ValidatorSet, pub address: Ctx::Address, pub threshold_params: ThresholdParams, - pub peers: Peers, } type TxInput = mpsc::UnboundedSender>; @@ -287,33 +285,42 @@ where match msg { NetworkMsg::Vote(signed_vote) => { let signed_vote = SignedVote::::from_proto(signed_vote).unwrap(); - tx_input.send(Input::Vote(signed_vote.vote)).unwrap(); - - // let peer = self.params.peers.get(&peer_id).unwrap(); // FIXME - // if self.ctx.verify_signed_vote(&signed_vote, &peer.public_key) { - // tx_input.send(Input::Vote(signed_vote.vote)).unwrap(); - // } else { - // warn!("Invalid vote from peer {peer_id}: {signed_vote:?}"); - // } + let validator_address = signed_vote.validator_address(); + let Some(validator) = self.params.validator_set.get_by_address(validator_address) + else { + warn!(%peer_id, %validator_address, "Received vote from unknown validator"); + return; + }; + + if self + .ctx + .verify_signed_vote(&signed_vote, validator.public_key()) + { + tx_input.send(Input::Vote(signed_vote.vote)).unwrap(); // FIXME + } else { + warn!(%peer_id, %validator_address, "Received invalid vote: {signed_vote:?}"); + } } + NetworkMsg::Proposal(proposal) => { let signed_proposal = SignedProposal::::from_proto(proposal).unwrap(); + let validator_address = signed_proposal.proposal.validator_address(); + let Some(validator) = self.params.validator_set.get_by_address(validator_address) + else { + warn!(%peer_id, %validator_address, "Received proposal from unknown validator"); + return; + }; + + let valid = self + .ctx + .verify_signed_proposal(&signed_proposal, validator.public_key()); + tx_input - .send(Input::Proposal(signed_proposal.proposal, Validity::Valid)) - .unwrap(); - - // let peer = self.params.peers.get(&peer_id).unwrap(); // FIXME - // - // let valid = self - // .ctx - // .verify_signed_proposal(&signed_proposal, &peer.public_key); - // - // tx_input - // .send(Input::Proposal( - // signed_proposal.proposal, - // Validity::from_valid(valid), - // )) - // .unwrap(); + .send(Input::Proposal( + signed_proposal.proposal, + Validity::from_valid(valid), + )) + .unwrap(); // FIXME } #[cfg(test)] diff --git a/code/node/src/util/make_node.rs b/code/node/src/util/make_node.rs index ac9cd74f2..0a97f0c7e 100644 --- a/code/node/src/util/make_node.rs +++ b/code/node/src/util/make_node.rs @@ -29,7 +29,6 @@ pub async fn make_broadcast_node( proposal_builder, validator_set, address, - peers: peers.clone(), threshold_params: Default::default(), }; @@ -75,7 +74,6 @@ pub async fn make_gossip_node( proposal_builder, validator_set, address, - peers: Default::default(), threshold_params: Default::default(), }; From 6c47d8417a3ee76cf052c3aea1c056d9e8393c54 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 27 Feb 2024 20:44:50 +0100 Subject: [PATCH 47/50] Rotate the proposer each height and round --- code/node/src/util/make_node.rs | 6 +++--- code/test/src/utils.rs | 9 +++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/code/node/src/util/make_node.rs b/code/node/src/util/make_node.rs index 0a97f0c7e..637d8df6e 100644 --- a/code/node/src/util/make_node.rs +++ b/code/node/src/util/make_node.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use std::time::Duration; -use malachite_test::utils::FixedProposer; +use malachite_test::utils::RotateProposer; use malachite_test::{Address, Height, PrivateKey, TestContext, ValidatorSet}; use crate::network::broadcast; @@ -20,7 +20,7 @@ pub async fn make_broadcast_node( ) -> Node { let start_height = Height::new(1); let ctx = TestContext::new(private_key); - let proposer_selector = Arc::new(FixedProposer::new(validator_set.validators[0].address)); + let proposer_selector = Arc::new(RotateProposer); let proposal_builder = Arc::new(TestValueBuilder::default()); let params = Params { @@ -65,7 +65,7 @@ pub async fn make_gossip_node( let start_height = Height::new(1); let ctx = TestContext::new(private_key); - let proposer_selector = Arc::new(FixedProposer::new(validator_set.validators[0].address)); + let proposer_selector = Arc::new(RotateProposer); let proposal_builder = Arc::new(TestValueBuilder::default()); let params = Params { diff --git a/code/test/src/utils.rs b/code/test/src/utils.rs index 25c583f58..ef00c4314 100644 --- a/code/test/src/utils.rs +++ b/code/test/src/utils.rs @@ -15,11 +15,16 @@ pub struct RotateProposer; impl ProposerSelector for RotateProposer { fn select_proposer( &self, - _height: Height, + height: Height, round: Round, validator_set: &ValidatorSet, ) -> Address { - let proposer_index = round.as_i64() as usize % validator_set.validators.len(); + assert!(round != Round::Nil && round.as_i64() >= 0); + + let height = height.as_u64() as usize; + let round = round.as_i64() as usize; + + let proposer_index = (height - 1 + round) % validator_set.validators.len(); validator_set.validators[proposer_index].address } } From b4420809d3e8e7cacea1e119ad0a809ec9ee2b9a Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 27 Feb 2024 22:36:40 +0100 Subject: [PATCH 48/50] Less noisy logging --- code/gossip/src/lib.rs | 4 ++-- code/node/src/node.rs | 10 +++++++--- code/test/src/address.rs | 9 ++++++++- code/test/src/height.rs | 8 +++++++- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/code/gossip/src/lib.rs b/code/gossip/src/lib.rs index 5f1a9d599..1f8fd7605 100644 --- a/code/gossip/src/lib.rs +++ b/code/gossip/src/lib.rs @@ -6,7 +6,7 @@ use futures::StreamExt; use libp2p::swarm::{self, NetworkBehaviour, SwarmEvent}; use libp2p::{gossipsub, identify, mdns, noise, tcp, yamux, SwarmBuilder}; use tokio::sync::mpsc; -use tracing::{debug, error, error_span, info, Instrument}; +use tracing::{debug, error, error_span, Instrument}; pub use libp2p::identity::Keypair; pub use libp2p::{Multiaddr, PeerId}; @@ -215,7 +215,7 @@ async fn handle_swarm_event( ) -> ControlFlow<()> { match event { SwarmEvent::NewListenAddr { address, .. } => { - info!("Node is listening on {address}"); + debug!("Node is listening on {address}"); if let Err(e) = tx_event.send(HandleEvent::Listening(address)).await { error!("Error sending listening event to handle: {e}"); diff --git a/code/node/src/node.rs b/code/node/src/node.rs index f1d99ebb1..cca8666fe 100644 --- a/code/node/src/node.rs +++ b/code/node/src/node.rs @@ -280,12 +280,13 @@ where msg: NetworkMsg, tx_input: &TxInput, ) { - info!("Received message from peer {peer_id}: {msg:?}"); - match msg { NetworkMsg::Vote(signed_vote) => { - let signed_vote = SignedVote::::from_proto(signed_vote).unwrap(); + let signed_vote = SignedVote::::from_proto(signed_vote).unwrap(); // FIXME let validator_address = signed_vote.validator_address(); + + info!(%peer_id, %validator_address, "Received vote: {:?}", signed_vote.vote); + let Some(validator) = self.params.validator_set.get_by_address(validator_address) else { warn!(%peer_id, %validator_address, "Received vote from unknown validator"); @@ -305,6 +306,9 @@ where NetworkMsg::Proposal(proposal) => { let signed_proposal = SignedProposal::::from_proto(proposal).unwrap(); let validator_address = signed_proposal.proposal.validator_address(); + + info!(%peer_id, %validator_address, "Received proposal: {:?}", signed_proposal.proposal); + let Some(validator) = self.params.validator_set.get_by_address(validator_address) else { warn!(%peer_id, %validator_address, "Received proposal from unknown validator"); diff --git a/code/test/src/address.rs b/code/test/src/address.rs index 56f589ec2..4103003c6 100644 --- a/code/test/src/address.rs +++ b/code/test/src/address.rs @@ -4,7 +4,7 @@ use malachite_proto as proto; use crate::signing::PublicKey; -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Address([u8; Self::LENGTH]); impl Address { @@ -34,6 +34,13 @@ impl fmt::Display for Address { } } +impl fmt::Debug for Address { + #[cfg_attr(coverage_nightly, coverage(off))] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Address({})", self) + } +} + impl malachite_common::Address for Address {} impl malachite_proto::Protobuf for Address { diff --git a/code/test/src/height.rs b/code/test/src/height.rs index 74639eb6e..438fcfb7b 100644 --- a/code/test/src/height.rs +++ b/code/test/src/height.rs @@ -3,7 +3,7 @@ use core::fmt; use malachite_proto as proto; /// A blockchain height -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord)] pub struct Height(u64); impl Height { @@ -22,6 +22,12 @@ impl fmt::Display for Height { } } +impl fmt::Debug for Height { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Height({})", self.0) + } +} + impl malachite_common::Height for Height { fn increment(&self) -> Self { Self(self.0 + 1) From 367f4121761e84eb20424614af58949dfb981140 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Mon, 4 Mar 2024 06:04:08 +0100 Subject: [PATCH 49/50] Use QUIC instead of TCP+Noise+Yamux --- code/gossip/Cargo.toml | 2 +- code/gossip/src/lib.rs | 8 ++------ code/node/src/util/make_node.rs | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/code/gossip/Cargo.toml b/code/gossip/Cargo.toml index 3dd13f778..83c075a96 100644 --- a/code/gossip/Cargo.toml +++ b/code/gossip/Cargo.toml @@ -7,7 +7,7 @@ license.workspace = true publish.workspace = true [dependencies] -libp2p = { version = "0.53.2", features = ["macros", "noise", "mdns", "identify", "yamux", "tokio", "ed25519", "tcp", "tls", "gossipsub"] } +libp2p = { version = "0.53.2", features = ["macros", "mdns", "identify", "tokio", "ed25519", "quic", "tls", "gossipsub"] } libp2p-gossipsub = { version = "0.46.1" } futures = { workspace = true } diff --git a/code/gossip/src/lib.rs b/code/gossip/src/lib.rs index 1f8fd7605..70fde4c8e 100644 --- a/code/gossip/src/lib.rs +++ b/code/gossip/src/lib.rs @@ -4,7 +4,7 @@ use std::time::Duration; use futures::StreamExt; use libp2p::swarm::{self, NetworkBehaviour, SwarmEvent}; -use libp2p::{gossipsub, identify, mdns, noise, tcp, yamux, SwarmBuilder}; +use libp2p::{gossipsub, identify, mdns, SwarmBuilder}; use tokio::sync::mpsc; use tracing::{debug, error, error_span, Instrument}; @@ -130,11 +130,7 @@ pub async fn spawn( ) -> Result> { let mut swarm = SwarmBuilder::with_existing_identity(keypair) .with_tokio() - .with_tcp( - tcp::Config::default(), - noise::Config::new, - yamux::Config::default, - )? + .with_quic() .with_behaviour(Behaviour::new)? .with_swarm_config(|cfg| config.apply(cfg)) .build(); diff --git a/code/node/src/util/make_node.rs b/code/node/src/util/make_node.rs index 637d8df6e..5bd7f0d9a 100644 --- a/code/node/src/util/make_node.rs +++ b/code/node/src/util/make_node.rs @@ -86,7 +86,7 @@ pub async fn make_gossip_node( let config = malachite_gossip::Config::default(); - let addr = "/ip4/0.0.0.0/tcp/0".parse().unwrap(); + let addr = "/ip4/0.0.0.0/udp/0/quic-v1".parse().unwrap(); let handle = malachite_gossip::spawn(keypair, addr, config) .await .unwrap(); From 4c71de7440c3e9d0ff65641730447f7e4fddb9bf Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 20 Mar 2024 10:28:20 +0100 Subject: [PATCH 50/50] Add support for multiple gossip channels --- code/gossip/src/lib.rs | 102 ++++++++++++++++++++++++-------- code/node/src/network.rs | 1 + code/node/src/network/gossip.rs | 7 ++- 3 files changed, 82 insertions(+), 28 deletions(-) diff --git a/code/gossip/src/lib.rs b/code/gossip/src/lib.rs index 70fde4c8e..90752805d 100644 --- a/code/gossip/src/lib.rs +++ b/code/gossip/src/lib.rs @@ -1,3 +1,4 @@ +use core::fmt; use std::error::Error; use std::ops::ControlFlow; use std::time::Duration; @@ -11,8 +12,51 @@ use tracing::{debug, error, error_span, Instrument}; pub use libp2p::identity::Keypair; pub use libp2p::{Multiaddr, PeerId}; +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Channel { + Consensus, +} + +impl Channel { + pub fn all() -> &'static [Channel] { + &[Channel::Consensus] + } + + pub fn to_topic(self) -> gossipsub::IdentTopic { + gossipsub::IdentTopic::new(self.as_str()) + } + + pub fn topic_hash(&self) -> gossipsub::TopicHash { + self.to_topic().hash() + } + + pub fn as_str(&self) -> &'static str { + match self { + Channel::Consensus => "/consensus", + } + } + + pub fn has_topic(topic_hash: &gossipsub::TopicHash) -> bool { + Self::all() + .iter() + .any(|channel| &channel.topic_hash() == topic_hash) + } + + pub fn from_topic_hash(topic: &gossipsub::TopicHash) -> Option { + match topic.as_str() { + "/consensus" => Some(Channel::Consensus), + _ => None, + } + } +} + +impl fmt::Display for Channel { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.as_str().fmt(f) + } +} + const PROTOCOL_VERSION: &str = "malachite-gossip/v1beta1"; -const TOPIC: &str = "consensus"; #[derive(NetworkBehaviour)] #[behaviour(to_swarm = "Event")] @@ -89,14 +133,14 @@ impl Default for Config { #[derive(Debug)] pub enum HandleEvent { Listening(Multiaddr), - Message(PeerId, Vec), + Message(PeerId, Channel, Vec), PeerConnected(PeerId), PeerDisconnected(PeerId), } #[derive(Debug)] pub enum CtrlMsg { - Broadcast(Vec), + Broadcast(Channel, Vec), Shutdown, } @@ -111,8 +155,12 @@ impl Handle { self.rx_event.recv().await } - pub async fn broadcast(&mut self, data: Vec) -> Result<(), Box> { - self.tx_ctrl.send(CtrlMsg::Broadcast(data)).await?; + pub async fn broadcast( + &mut self, + channel: Channel, + data: Vec, + ) -> Result<(), Box> { + self.tx_ctrl.send(CtrlMsg::Broadcast(channel, data)).await?; Ok(()) } @@ -135,8 +183,12 @@ pub async fn spawn( .with_swarm_config(|cfg| config.apply(cfg)) .build(); - let topic = gossipsub::IdentTopic::new(TOPIC); - swarm.behaviour_mut().gossipsub.subscribe(&topic)?; + for channel in Channel::all() { + swarm + .behaviour_mut() + .gossipsub + .subscribe(&channel.to_topic())?; + } swarm.listen_on(addr)?; @@ -145,7 +197,7 @@ pub async fn spawn( let peer_id = swarm.local_peer_id(); let span = error_span!("gossip", peer = %peer_id); - let task_handle = tokio::task::spawn(run(swarm, topic, rx_ctrl, tx_event).instrument(span)); + let task_handle = tokio::task::spawn(run(swarm, rx_ctrl, tx_event).instrument(span)); Ok(Handle { rx_event, @@ -156,18 +208,17 @@ pub async fn spawn( async fn run( mut swarm: swarm::Swarm, - topic: gossipsub::IdentTopic, mut rx_ctrl: mpsc::Receiver, tx_event: mpsc::Sender, ) { loop { let result = tokio::select! { event = swarm.select_next_some() => { - handle_swarm_event(event, &mut swarm, &topic, &tx_event).await + handle_swarm_event(event, &mut swarm, &tx_event).await } Some(ctrl) = rx_ctrl.recv() => { - handle_ctrl_msg(ctrl, &mut swarm, &topic).await + handle_ctrl_msg(ctrl, &mut swarm).await } }; @@ -178,14 +229,13 @@ async fn run( } } -async fn handle_ctrl_msg( - msg: CtrlMsg, - swarm: &mut swarm::Swarm, - topic: &gossipsub::IdentTopic, -) -> ControlFlow<()> { +async fn handle_ctrl_msg(msg: CtrlMsg, swarm: &mut swarm::Swarm) -> ControlFlow<()> { match msg { - CtrlMsg::Broadcast(data) => { - let result = swarm.behaviour_mut().gossipsub.publish(topic.hash(), data); + CtrlMsg::Broadcast(channel, data) => { + let result = swarm + .behaviour_mut() + .gossipsub + .publish(channel.topic_hash(), data); match result { Ok(message_id) => { @@ -206,7 +256,6 @@ async fn handle_ctrl_msg( async fn handle_swarm_event( event: SwarmEvent, swarm: &mut swarm::Swarm, - topic: &gossipsub::IdentTopic, tx_event: &mpsc::Sender, ) -> ControlFlow<()> { match event { @@ -258,8 +307,8 @@ async fn handle_swarm_event( peer_id, topic: topic_hash, })) => { - if topic.hash() != topic_hash { - debug!("Peer {peer_id} subscribed to different topic: {topic_hash}"); + if !Channel::has_topic(&topic_hash) { + debug!("Peer {peer_id} tried to subscribe to unknown topic: {topic_hash}"); return ControlFlow::Continue(()); } @@ -276,22 +325,23 @@ async fn handle_swarm_event( message_id: _, message, })) => { - if topic.hash() != message.topic { + let Some(channel) = Channel::from_topic_hash(&message.topic) else { debug!( - "Received message from {peer_id} on different topic: {}", + "Received message from {peer_id} on different channel: {}", message.topic ); return ControlFlow::Continue(()); - } + }; debug!( - "Received message from {peer_id} of {} bytes", + "Received message from {peer_id} on channel {} of {} bytes", + channel, message.data.len() ); if let Err(e) = tx_event - .send(HandleEvent::Message(peer_id, message.data)) + .send(HandleEvent::Message(peer_id, channel, message.data)) .await { error!("Error sending message to handle: {e}"); diff --git a/code/node/src/network.rs b/code/node/src/network.rs index e2e4ba325..e98bfcbfd 100644 --- a/code/node/src/network.rs +++ b/code/node/src/network.rs @@ -41,6 +41,7 @@ where Self: Send + Sync + 'static, { async fn recv(&mut self) -> Option<(PeerId, Msg)>; + async fn broadcast(&mut self, msg: Msg); async fn broadcast_vote(&mut self, vote: SignedVote) { diff --git a/code/node/src/network/gossip.rs b/code/node/src/network/gossip.rs index cc9cbced5..8784eea5c 100644 --- a/code/node/src/network/gossip.rs +++ b/code/node/src/network/gossip.rs @@ -1,3 +1,4 @@ +use malachite_gossip::Channel; pub use malachite_gossip::{spawn, Config, CtrlMsg, Handle, HandleEvent, Keypair}; use super::{Msg, Network, PeerId}; @@ -7,7 +8,7 @@ impl Network for Handle { async fn recv(&mut self) -> Option<(PeerId, Msg)> { loop { match Handle::recv(self).await { - Some(HandleEvent::Message(peer_id, data)) => { + Some(HandleEvent::Message(peer_id, Channel::Consensus, data)) => { let msg = Msg::from_network_bytes(&data).unwrap(); let peer_id = PeerId::new(peer_id.to_string()); return Some((peer_id, msg)); @@ -19,6 +20,8 @@ impl Network for Handle { async fn broadcast(&mut self, msg: Msg) { let data = msg.to_network_bytes().unwrap(); - Handle::broadcast(self, data).await.unwrap(); + Handle::broadcast(self, Channel::Consensus, data) + .await + .unwrap(); } }