chore(blend): restructure existing core crates (#3404)

This commit is contained in:
Antonio
2026-08-26 06:52:56 +00:00
committed by GitHub
parent 7ebe8ec15e
commit 33c862ffe5
48 changed files with 571 additions and 460 deletions
Generated
+34 -5
View File
@@ -4008,9 +4008,11 @@ name = "logos-blockchain-blend"
version = "0.0.0"
dependencies = [
"logos-blockchain-blend-crypto",
"logos-blockchain-blend-membership",
"logos-blockchain-blend-message",
"logos-blockchain-blend-network",
"logos-blockchain-blend-proofs",
"logos-blockchain-blend-provers",
"logos-blockchain-blend-scheduling",
]
@@ -4028,6 +4030,16 @@ dependencies = [
"thiserror 2.0.18",
]
[[package]]
name = "logos-blockchain-blend-membership"
version = "0.0.0"
dependencies = [
"logos-blockchain-key-management-system-keys",
"multiaddr",
"rand 0.8.6",
"serde",
]
[[package]]
name = "logos-blockchain-blend-message"
version = "0.0.0"
@@ -4067,9 +4079,10 @@ dependencies = [
"libp2p",
"libp2p-stream",
"libp2p-swarm-test",
"logos-blockchain-blend-membership",
"logos-blockchain-blend-message",
"logos-blockchain-blend-proofs",
"logos-blockchain-blend-scheduling",
"logos-blockchain-blend-provers",
"logos-blockchain-cryptarchia-engine",
"logos-blockchain-groth16",
"logos-blockchain-key-management-system-keys",
@@ -4107,18 +4120,17 @@ dependencies = [
]
[[package]]
name = "logos-blockchain-blend-scheduling"
name = "logos-blockchain-blend-provers"
version = "0.0.0"
dependencies = [
"async-trait",
"derivative",
"fork_stream",
"futures",
"hex",
"libp2p",
"logos-blockchain-blend-membership",
"logos-blockchain-blend-message",
"logos-blockchain-blend-proofs",
"logos-blockchain-codec",
"logos-blockchain-core",
"logos-blockchain-cryptarchia-engine",
"logos-blockchain-groth16",
@@ -4128,8 +4140,25 @@ dependencies = [
"multiaddr",
"rand 0.8.6",
"rayon",
"serde",
"test-log",
"tokio",
"tracing",
]
[[package]]
name = "logos-blockchain-blend-scheduling"
version = "0.0.0"
dependencies = [
"fork_stream",
"futures",
"logos-blockchain-blend-membership",
"logos-blockchain-blend-message",
"logos-blockchain-blend-proofs",
"logos-blockchain-blend-provers",
"logos-blockchain-cryptarchia-engine",
"logos-blockchain-log-targets",
"logos-blockchain-utils",
"rand 0.8.6",
"thiserror 2.0.18",
"tokio",
"tokio-stream",
+4
View File
@@ -31,9 +31,11 @@ strip = "none" # Don't strip symbols to allow for better profiling results
members = [
"blend/core",
"blend/crypto",
"blend/membership",
"blend/message",
"blend/network",
"blend/proofs",
"blend/provers",
"blend/scheduling",
"c-bindings",
"codec",
@@ -110,9 +112,11 @@ lb-api-service = { default-features = false, package = "logo
lb-blake2btree = { default-features = false, package = "logos-blockchain-blake2btree", path = "merkle/blake2btree" }
lb-blend = { default-features = false, package = "logos-blockchain-blend", path = "./blend/core" }
lb-blend-crypto = { default-features = false, package = "logos-blockchain-blend-crypto", path = "./blend/crypto" }
lb-blend-membership = { default-features = false, package = "logos-blockchain-blend-membership", path = "./blend/membership" }
lb-blend-message = { default-features = false, package = "logos-blockchain-blend-message", path = "./blend/message" }
lb-blend-network = { default-features = false, package = "logos-blockchain-blend-network", path = "./blend/network" }
lb-blend-proofs = { default-features = false, package = "logos-blockchain-blend-proofs", path = "./blend/proofs" }
lb-blend-provers = { default-features = false, package = "logos-blockchain-blend-provers", path = "./blend/provers" }
lb-blend-scheduling = { default-features = false, package = "logos-blockchain-blend-scheduling", path = "./blend/scheduling" }
lb-blend-service = { default-features = false, package = "logos-blockchain-blend-service", path = "./services/blend" }
lb-chain-broadcast-service = { default-features = false, package = "logos-blockchain-chain-broadcast-service", path = "./services/chain/broadcast-service" }
+2
View File
@@ -14,9 +14,11 @@ workspace = true
[dependencies]
lb-blend-crypto = { workspace = true }
lb-blend-membership = { workspace = true }
lb-blend-message = { workspace = true }
lb-blend-network = { workspace = true }
lb-blend-proofs = { workspace = true }
lb-blend-provers = { workspace = true }
lb-blend-scheduling = { workspace = true }
[features]
+2
View File
@@ -1,5 +1,7 @@
pub use lb_blend_crypto as crypto;
pub use lb_blend_membership as membership;
pub use lb_blend_message as message;
pub use lb_blend_network as network;
pub use lb_blend_proofs as proofs;
pub use lb_blend_provers as provers;
pub use lb_blend_scheduling as scheduling;
+25
View File
@@ -0,0 +1,25 @@
[package]
categories = { workspace = true }
description = { workspace = true }
edition = { workspace = true }
keywords = { workspace = true }
license = { workspace = true }
name = "logos-blockchain-blend-membership"
readme = { workspace = true }
repository = { workspace = true }
version = { workspace = true }
[lints]
workspace = true
[dependencies]
lb-key-management-system-keys = { workspace = true }
multiaddr = { workspace = true }
rand = { features = ["alloc"], workspace = true }
serde = { workspace = true }
[dev-dependencies]
lb-key-management-system-keys = { workspace = true }
[features]
unsafe-test-functions = []
+147
View File
@@ -0,0 +1,147 @@
use std::{
collections::{HashMap, HashSet},
hash::Hash,
};
use lb_key_management_system_keys::keys::Ed25519PublicKey;
use multiaddr::Multiaddr;
use rand::{Rng, seq::IteratorRandom as _};
use serde::{Deserialize, Serialize};
#[cfg(test)]
mod tests;
/// A set of core nodes in an epoch.
#[derive(Clone, Debug)]
pub struct Membership<NodeId> {
/// All nodes, including local and remote.
core_nodes: HashMap<NodeId, Node<NodeId>>,
/// List of node indices, used for proof of selection generation. It
/// contains all nodes in the `nodes` map.
node_indices: Vec<NodeId>,
/// ID of the local node in the `node_indices` vector, if present (i.e., if
/// the local node is a core node).
local_node_index: Option<usize>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Node<Id> {
/// An unique identifier of the node,
/// which is usually corresponding to the network node identifier
/// but depending on the network backend.
pub id: Id,
/// A listening address
pub address: Multiaddr,
/// A public key used for the blend message encryption
pub public_key: Ed25519PublicKey,
}
impl<NodeId> Membership<NodeId>
where
NodeId: Clone + Hash + Eq,
{
#[must_use]
pub fn new(nodes: &[Node<NodeId>], local_public_key: &Ed25519PublicKey) -> Self {
let mut core_nodes = HashMap::with_capacity(nodes.len());
let mut node_indices = Vec::with_capacity(nodes.len());
let mut local_node_index = None;
for (index, node) in nodes.iter().enumerate() {
assert!(
core_nodes.insert(node.id.clone(), node.clone()).is_none(),
"Membership info contained a duplicate node."
);
node_indices.push(node.id.clone());
if node.public_key == *local_public_key {
local_node_index = Some(index);
}
}
Self {
core_nodes,
node_indices,
local_node_index,
}
}
#[cfg(any(test, feature = "unsafe-test-functions"))]
#[must_use]
pub fn new_without_local(nodes: &[Node<NodeId>]) -> Self {
use lb_key_management_system_keys::keys::ED25519_PUBLIC_KEY_SIZE;
Self::new(
nodes,
&Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE]).unwrap(),
)
}
}
impl<NodeId> Membership<NodeId>
where
NodeId: Eq + Hash,
{
/// Choose `amount` random remote nodes.
pub fn choose_remote_nodes<R: Rng>(
&self,
rng: &mut R,
amount: usize,
) -> impl Iterator<Item = &Node<NodeId>> + use<'_, R, NodeId> {
self.filter_and_choose_remote_nodes(rng, amount, &HashSet::new())
}
/// Choose `amount` random remote nodes excluding the given set of node IDs.
pub fn filter_and_choose_remote_nodes<R: Rng>(
&self,
rng: &mut R,
amount: usize,
exclude_peers: &HashSet<NodeId>,
) -> impl Iterator<Item = &Node<NodeId>> + use<'_, R, NodeId> {
self.node_indices
.iter()
.enumerate()
// Filter out excluded peers.
.filter(|(_, node_id)| !exclude_peers.contains(node_id))
// Filter out local node, if the local node is a core node.
.filter(|(index, _)| self.local_node_index != Some(*index))
// Discard index after it's used.
.map(|(_, node)| node)
.choose_multiple(rng, amount)
.into_iter()
.map(|id| {
self.core_nodes
.get(id)
.expect("Node ID must exist in core nodes.")
})
}
pub fn contains(&self, node_id: &NodeId) -> bool {
self.core_nodes.contains_key(node_id)
}
#[must_use]
pub fn get_node_at(&self, index: usize) -> Option<&Node<NodeId>> {
self.core_nodes.get(self.node_indices.get(index)?)
}
}
impl<NodeId> Membership<NodeId> {
#[must_use]
pub const fn local_index(&self) -> Option<usize> {
self.local_node_index
}
#[must_use]
pub const fn contains_local(&self) -> bool {
self.local_node_index.is_some()
}
/// Returns the number of all nodes, including local and remote.
#[must_use]
pub fn size(&self) -> usize {
self.core_nodes.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.core_nodes.is_empty()
}
}
+167
View File
@@ -0,0 +1,167 @@
use std::collections::HashSet;
use lb_key_management_system_keys::keys::{Ed25519PublicKey, UnsecuredEd25519Key};
use multiaddr::Multiaddr;
use rand::rngs::OsRng;
use crate::{Membership, Node};
#[test]
fn test_membership_new_with_local_node() {
let nodes = vec![node(1, 1), node(2, 2), node(3, 3)];
let local_key = key(2);
let membership = Membership::new(&nodes, &local_key);
assert_eq!(membership.size(), 3);
assert_eq!(
membership
.core_nodes
.keys()
.copied()
.collect::<HashSet<_>>(),
HashSet::from([1, 2, 3])
);
assert_eq!(membership.node_indices, vec![1, 2, 3]);
assert_eq!(membership.local_node_index, Some(1));
assert!(membership.contains_local());
}
#[test]
fn test_membership_new_without_local_node() {
let nodes = vec![node(1, 1), node(2, 2), node(3, 3)];
let local_key = key(99);
let membership = Membership::new(&nodes, &local_key);
assert_eq!(membership.size(), 3);
assert_eq!(
membership
.core_nodes
.keys()
.copied()
.collect::<HashSet<_>>(),
HashSet::from([1, 2, 3])
);
assert_eq!(membership.node_indices, vec![1, 2, 3]);
assert!(membership.local_node_index.is_none());
assert!(!membership.contains_local());
}
#[test]
fn test_membership_new_empty() {
let local_key = key(99);
let membership = Membership::<u32>::new(&[], &local_key);
assert_eq!(membership.size(), 0);
assert!(membership.core_nodes.keys().next().is_none());
assert!(membership.node_indices.is_empty());
assert!(membership.local_node_index.is_none());
assert!(!membership.contains_local());
}
#[test]
fn test_choose_remote_nodes() {
let nodes = vec![node(1, 1), node(2, 2), node(3, 3), node(4, 4)];
let local_key = key(99);
let membership = Membership::new(&nodes, &local_key);
let chosen: HashSet<_> = membership
.choose_remote_nodes(&mut OsRng, 2)
.map(|node| node.id)
.collect();
assert_eq!(chosen.len(), 2);
}
#[test]
fn test_choose_remote_nodes_more_than_available() {
let nodes = vec![node(1, 1), node(2, 2)];
let local_key = key(99);
let membership = Membership::new(&nodes, &local_key);
let chosen: HashSet<_> = membership
.choose_remote_nodes(&mut OsRng, 5)
.map(|node| node.id)
.collect();
assert_eq!(chosen.len(), 2);
}
#[test]
fn test_choose_remote_nodes_zero() {
let nodes = vec![node(1, 1), node(2, 2)];
let local_key = key(99);
let membership = Membership::new(&nodes, &local_key);
let mut rng = OsRng;
let mut chosen = membership.choose_remote_nodes(&mut rng, 0);
assert!(chosen.next().is_none());
}
#[test]
fn test_filter_and_choose_remote_nodes() {
let nodes = vec![node(1, 1), node(2, 2), node(3, 3)];
let local_key = key(99);
let membership = Membership::new(&nodes, &local_key);
let exclude_peers = HashSet::from([3]);
let chosen: HashSet<_> = membership
.filter_and_choose_remote_nodes(&mut OsRng, 2, &exclude_peers)
.map(|node| node.id)
.collect();
assert_eq!(chosen.len(), 2);
}
#[test]
fn test_filter_and_choose_remote_nodes_all_excluded() {
let nodes = vec![node(1, 1), node(2, 2)];
let local_key = key(99);
let membership = Membership::new(&nodes, &local_key);
let exclude_peers = HashSet::from([1, 2]);
let chosen: HashSet<_> = membership
.filter_and_choose_remote_nodes(&mut OsRng, 2, &exclude_peers)
.map(|node| node.id)
.collect();
assert!(chosen.is_empty());
}
#[test]
fn test_contains() {
let nodes = vec![node(1, 1)];
let local_key = key(99);
let membership = Membership::new(&nodes, &local_key);
assert!(membership.contains(&1));
assert!(!membership.contains(&2));
}
#[test]
fn get_node_at() {
let nodes = vec![node(1, 1)];
let local_key = key(99);
let membership = Membership::new(&nodes, &local_key);
assert_eq!(membership.get_node_at(0), Some(&node(1, 1)));
assert_eq!(membership.get_node_at(1), None);
}
#[test]
#[should_panic(expected = "Membership info contained a duplicate node.")]
fn duplicate_remote_node() {
let nodes = vec![node(1, 1), node(1, 2)];
let local_key = key(99);
drop(Membership::new(&nodes, &local_key));
}
fn key(seed: u8) -> Ed25519PublicKey {
UnsecuredEd25519Key::from_bytes(&[seed; 32]).public_key()
}
fn node(id: u32, seed: u8) -> Node<u32> {
Node {
id,
address: Multiaddr::empty(),
public_key: key(seed),
}
}
+50
View File
@@ -0,0 +1,50 @@
//! Wire helpers for whole encapsulated messages.
//!
//! Thin wrappers over the [`lb_codec`] impls, kept here so a crate that only
//! moves messages around — the network behaviours, say — does not have to
//! depend on anything that knows how to *produce* one.
use core::num::NonZeroU64;
use lb_codec::{BinaryDecode as _, BinaryEncode as _};
use crate::{
Error,
encap::{
encapsulated::EncapsulatedMessage,
validated::{
EncapsulatedMessageWithVerifiedPublicHeader, EncapsulatedMessageWithVerifiedSignature,
},
},
};
#[must_use]
pub fn serialize_encapsulated_message_with_verified_public_header(
message: &EncapsulatedMessageWithVerifiedPublicHeader,
) -> Vec<u8> {
message.encode_to_vec()
}
#[must_use]
pub fn serialize_encapsulated_message_with_verified_signature(
message: &EncapsulatedMessageWithVerifiedSignature,
) -> Vec<u8> {
message.encode_to_vec()
}
/// Decodes a whole encapsulated message, rejecting trailing bytes.
///
/// # Errors
///
/// [`Error::MessageDeserializationFailed`] if the input does not decode, or
/// decodes with bytes left over.
pub fn deserialize_encapsulated_message(
message: &[u8],
num_blend_layers: &NonZeroU64,
) -> Result<EncapsulatedMessage, Error> {
let (remaining, deserialized_message) = EncapsulatedMessage::decode(message, num_blend_layers)?;
if !remaining.is_empty() {
return Err(Error::MessageDeserializationFailed);
}
Ok(deserialized_message)
}
+5
View File
@@ -1,3 +1,4 @@
pub mod codec;
pub mod crypto;
pub mod encap;
pub mod input;
@@ -7,6 +8,10 @@ mod error;
mod fixtures;
mod message;
pub use codec::{
deserialize_encapsulated_message, serialize_encapsulated_message_with_verified_public_header,
serialize_encapsulated_message_with_verified_signature,
};
pub use encap::encapsulated::MessageIdentifier;
pub use error::Error;
pub use message::payload::{MAX_PAYLOAD_BODY_SIZE, PaddedPayloadBody, PayloadType};
+3 -2
View File
@@ -17,9 +17,9 @@ either = { workspace = true }
futures = { workspace = true }
futures-timer = { workspace = true }
hex = { workspace = true }
lb-blend-membership = { workspace = true }
lb-blend-message = { workspace = true }
lb-blend-proofs = { workspace = true }
lb-blend-scheduling = { workspace = true }
lb-cryptarchia-engine = { workspace = true }
lb-groth16 = { workspace = true }
lb-key-management-system-keys = { workspace = true }
@@ -32,8 +32,9 @@ tracing = { workspace = true }
[dev-dependencies]
async-trait = { workspace = true }
lb-blend-membership = { features = ["unsafe-test-functions"], workspace = true }
lb-blend-message = { features = ["unsafe-test-functions"], workspace = true }
lb-blend-scheduling = { features = ["unsafe-test-functions"], workspace = true }
lb-blend-provers = { features = ["unsafe-test-functions"], workspace = true }
libp2p = { features = ["plaintext", "tcp", "yamux"], workspace = true }
libp2p-stream = { workspace = true }
libp2p-swarm-test = { features = ["tokio"], workspace = true }
+1 -1
View File
@@ -6,7 +6,7 @@ mod poq_verification;
#[cfg(test)]
mod tests;
use lb_blend_scheduling::membership::Membership;
use lb_blend_membership::Membership;
use lb_cryptarchia_engine::Epoch;
use libp2p::{PeerId, StreamProtocol};
+1 -1
View File
@@ -15,7 +15,7 @@ use lb_blend_proofs::{
quota::{ProofOfQuota, VerifiedProofOfQuota},
selection::{ProofOfSelection, VerifiedProofOfSelection, inputs::VerifyInputs},
};
use lb_blend_scheduling::message_blend::provers::BlendLayerProof;
use lb_blend_provers::provers::BlendLayerProof;
use lb_cryptarchia_engine::Epoch;
use lb_key_management_system_keys::keys::{
Ed25519PublicKey, Ed25519Signature, UnsecuredEd25519Key,
@@ -12,10 +12,10 @@ use std::{
use either::Either;
use futures::{Stream, StreamExt as _};
use lb_blend_membership::Membership;
use lb_blend_message::encap::{
ProofsVerifier as ProofsVerifierTrait, validated::EncapsulatedMessageWithVerifiedPublicHeader,
};
use lb_blend_scheduling::membership::Membership;
use lb_cryptarchia_engine::Epoch;
use lb_groth16::fr_to_bytes;
use lb_log_targets::blend;
@@ -377,9 +377,7 @@ impl<ObservationWindowClockProvider, ProofsVerifier>
epoch: Epoch,
) -> Result<(), SendError> {
let serialized_message =
lb_blend_scheduling::serialize_encapsulated_message_with_verified_public_header(
message,
);
lb_blend_message::serialize_encapsulated_message_with_verified_public_header(message);
self.force_send_serialized_message_to_peer_at_epoch(serialized_message, peer_id, epoch)
}
@@ -2,8 +2,8 @@ use core::time::Duration;
use either::Either;
use futures::StreamExt as _;
use lb_blend_membership::Membership;
use lb_blend_message::encap::validated::EncapsulatedMessageWithVerifiedPublicHeader;
use lb_blend_scheduling::membership::Membership;
use lb_cryptarchia_engine::Epoch;
use lb_libp2p::{NetworkBehaviour as _, SwarmEvent};
use libp2p::{Multiaddr, swarm::ConnectionId};
@@ -2,7 +2,7 @@ use core::time::Duration;
use std::collections::HashSet;
use futures::StreamExt as _;
use lb_blend_scheduling::serialize_encapsulated_message_with_verified_public_header;
use lb_blend_message::serialize_encapsulated_message_with_verified_public_header;
use lb_libp2p::SwarmEvent;
use libp2p_swarm_test::SwarmExt as _;
use test_log::test;
@@ -11,8 +11,8 @@ use std::{
use async_trait::async_trait;
use futures::{Stream, StreamExt as _, select};
use lb_blend_membership::{Membership, Node};
use lb_blend_message::crypto::key_ext::Ed25519SecretKeyExt as _;
use lb_blend_scheduling::membership::{Membership, Node};
use lb_key_management_system_keys::keys::{Ed25519PublicKey, UnsecuredEd25519Key};
use lb_libp2p::{NetworkBehaviour, SwarmEvent};
use libp2p::{
@@ -2,11 +2,10 @@ use core::{convert::Infallible, num::NonZeroU64, task::Waker};
use std::{collections::VecDeque, sync::Arc};
use either::Either;
use lb_blend_message::encap::{
ProofsVerifier, validated::EncapsulatedMessageWithVerifiedPublicHeader,
};
use lb_blend_scheduling::{
deserialize_encapsulated_message, serialize_encapsulated_message_with_verified_public_header,
use lb_blend_message::{
deserialize_encapsulated_message,
encap::{ProofsVerifier, validated::EncapsulatedMessageWithVerifiedPublicHeader},
serialize_encapsulated_message_with_verified_public_header,
};
use lb_cryptarchia_engine::Epoch;
use libp2p::{
@@ -10,10 +10,14 @@ use std::{
use either::Either;
use futures::StreamExt as _;
use lb_blend_message::encap::{
ProofsVerifier as ProofsVerifierTrait, validated::EncapsulatedMessageWithVerifiedPublicHeader,
use lb_blend_membership::Membership;
use lb_blend_message::{
deserialize_encapsulated_message,
encap::{
ProofsVerifier as ProofsVerifierTrait,
validated::EncapsulatedMessageWithVerifiedPublicHeader,
},
};
use lb_blend_scheduling::{deserialize_encapsulated_message, membership::Membership};
use lb_cryptarchia_engine::Epoch;
use lb_log_targets::blend;
use libp2p::{
@@ -1,7 +1,7 @@
use core::time::Duration;
use futures::StreamExt as _;
use lb_blend_scheduling::membership::{Membership, Node};
use lb_blend_membership::{Membership, Node};
use lb_key_management_system_keys::keys::{ED25519_PUBLIC_KEY_SIZE, Ed25519PublicKey};
use lb_libp2p::SwarmEvent;
use libp2p::{Multiaddr, PeerId};
@@ -2,7 +2,7 @@ use core::time::Duration;
use futures::{FutureExt as _, StreamExt as _, select};
use futures_timer::Delay;
use lb_blend_scheduling::serialize_encapsulated_message_with_verified_public_header;
use lb_blend_message::serialize_encapsulated_message_with_verified_public_header;
use lb_libp2p::SwarmEvent;
use libp2p::PeerId;
use libp2p_stream::Behaviour as StreamBehaviour;
@@ -9,7 +9,7 @@ use std::{
use async_trait::async_trait;
use futures::StreamExt as _;
use lb_blend_scheduling::membership::{Membership, Node};
use lb_blend_membership::{Membership, Node};
use lb_key_management_system_keys::keys::{ED25519_PUBLIC_KEY_SIZE, Ed25519PublicKey};
use lb_libp2p::SwarmEvent;
use libp2p::{Multiaddr, PeerId, Stream, Swarm};
+45
View File
@@ -0,0 +1,45 @@
[package]
categories = { workspace = true }
description = { workspace = true }
edition = { workspace = true }
keywords = { workspace = true }
license = { workspace = true }
name = "logos-blockchain-blend-provers"
readme = { workspace = true }
repository = { workspace = true }
version = { workspace = true }
[lints]
workspace = true
[dependencies]
async-trait = { workspace = true }
derivative = { workspace = true }
futures = { workspace = true }
hex = { workspace = true }
lb-blend-membership = { workspace = true }
lb-blend-message = { workspace = true }
lb-blend-proofs = { workspace = true }
lb-core = { workspace = true }
lb-cryptarchia-engine = { workspace = true }
lb-groth16 = { workspace = true }
lb-key-management-system-keys = { workspace = true }
lb-log-targets = { workspace = true }
lb-utils = { features = ["rng", "tokio"], workspace = true }
rand = { features = ["alloc"], workspace = true }
rayon = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
[dev-dependencies]
lb-blend-membership = { features = ["unsafe-test-functions"], workspace = true }
lb-blend-message = { features = ["unsafe-test-functions"], workspace = true }
lb-blend-proofs = { features = ["unsafe-test-functions"], workspace = true }
lb-groth16 = { workspace = true }
libp2p = { workspace = true }
multiaddr = { workspace = true }
test-log = { features = ["trace"], workspace = true }
[features]
tokio-task-names = ["lb-utils/tokio-task-names"]
unsafe-test-functions = []
@@ -1,3 +1,4 @@
use lb_blend_membership::Membership;
use lb_blend_message::{
Error,
crypto::proofs::PoQVerificationInputsMinusSigningKey,
@@ -12,9 +13,7 @@ use lb_blend_message::{
use lb_cryptarchia_engine::Epoch;
use lb_key_management_system_keys::keys::X25519PrivateKey;
use crate::{
membership::Membership, message_blend::crypto::EncapsulatedMessageWithVerifiedPublicHeader,
};
use crate::crypto::EncapsulatedMessageWithVerifiedPublicHeader;
/// [`EpochCryptographicProcessor`] is responsible for only unwrapping the
/// messages addressed to the local node.
@@ -1,6 +1,7 @@
use core::{hash::Hash, marker::PhantomData};
use std::{num::NonZeroU64, sync::Arc};
use lb_blend_membership::Membership;
use lb_blend_message::{
Error, PaddedPayloadBody, PayloadType, crypto::proofs::PoQVerificationInputsMinusSigningKey,
input::EncapsulationInput,
@@ -11,13 +12,10 @@ use lb_groth16::fr_to_bytes;
use rayon::ThreadPool;
use crate::{
membership::Membership,
message_blend::{
crypto::EncapsulatedMessageWithVerifiedPublicHeader,
provers::{
BlendLayerProof, ProofsGeneratorSettings, WinningPolInfoStream,
core_leader_and_pow::CoreLeaderAndPowProofsGenerator,
},
crypto::EncapsulatedMessageWithVerifiedPublicHeader,
provers::{
BlendLayerProof, ProofsGeneratorSettings, WinningPolInfoStream,
core_leader_and_pow::CoreLeaderAndPowProofsGenerator,
},
};
@@ -214,6 +212,7 @@ mod test {
use std::{num::NonZeroU64, sync::Arc};
use futures::{StreamExt as _, stream::repeat};
use lb_blend_membership::{Membership, Node};
use lb_blend_message::crypto::proofs::PoQVerificationInputsMinusSigningKey;
use lb_blend_proofs::quota::{
Quota,
@@ -231,11 +230,8 @@ mod test {
use rayon::ThreadPoolBuilder;
use super::EpochCryptographicProcessor;
use crate::{
membership::{Membership, Node},
message_blend::crypto::test_utils::{
MockCorePoQGenerator, TestEpochChangeCoreAndLeaderProofsGenerator,
},
use crate::crypto::test_utils::{
MockCorePoQGenerator, TestEpochChangeCoreAndLeaderProofsGenerator,
};
#[tokio::test]
@@ -1,5 +1,6 @@
use core::ops::{Deref, DerefMut};
use lb_blend_membership::Membership;
use lb_blend_message::{
Error,
crypto::proofs::PoQVerificationInputsMinusSigningKey,
@@ -8,17 +9,14 @@ use lb_blend_message::{
use lb_cryptarchia_engine::Epoch;
use crate::{
membership::Membership,
message_blend::{
crypto::{
EncapsulatedMessageWithVerifiedPublicHeader, EpochCryptographicProcessorSettings,
core_and_leader::{
receive::EpochCryptographicProcessor as ReceiverEpochCryptographicProcessor,
send::EpochCryptographicProcessor as SenderEpochCryptographicProcessor,
},
crypto::{
EncapsulatedMessageWithVerifiedPublicHeader, EpochCryptographicProcessorSettings,
core_and_leader::{
receive::EpochCryptographicProcessor as ReceiverEpochCryptographicProcessor,
send::EpochCryptographicProcessor as SenderEpochCryptographicProcessor,
},
provers::core_leader_and_pow::CoreLeaderAndPowProofsGenerator,
},
provers::core_leader_and_pow::CoreLeaderAndPowProofsGenerator,
};
/// [`EpochCryptographicProcessor`] is responsible for wrapping both cover and
@@ -133,6 +131,7 @@ mod test {
use std::{num::NonZeroU64, sync::Arc};
use futures::{StreamExt as _, stream::repeat};
use lb_blend_membership::{Membership, Node};
use lb_blend_message::crypto::proofs::PoQVerificationInputsMinusSigningKey;
use lb_blend_proofs::quota::{
Quota,
@@ -148,15 +147,12 @@ mod test {
use multiaddr::{Multiaddr, PeerId};
use rayon::ThreadPoolBuilder;
use crate::{
membership::{Membership, Node},
message_blend::crypto::{
EpochCryptographicProcessorSettings,
core_and_leader::send_and_receive::EpochCryptographicProcessor,
test_utils::{
MockCorePoQGenerator, TestEpochChangeCoreAndLeaderProofsGenerator,
TestEpochChangeProofsVerifier,
},
use crate::crypto::{
EpochCryptographicProcessorSettings,
core_and_leader::send_and_receive::EpochCryptographicProcessor,
test_utils::{
MockCorePoQGenerator, TestEpochChangeCoreAndLeaderProofsGenerator,
TestEpochChangeProofsVerifier,
},
};
@@ -1,6 +1,7 @@
use core::hash::Hash;
use std::{num::NonZeroU64, sync::Arc};
use lb_blend_membership::Membership;
use lb_blend_message::{
Error, PaddedPayloadBody, crypto::proofs::PoQVerificationInputsMinusSigningKey,
input::EncapsulationInput,
@@ -9,13 +10,10 @@ use lb_cryptarchia_engine::Epoch;
use rayon::ThreadPool;
use crate::{
membership::Membership,
message_blend::{
crypto::EncapsulatedMessageWithVerifiedPublicHeader,
provers::{
BlendLayerProof, ProofsGeneratorSettings, WinningPolInfoStream,
leader_and_pow::LeaderAndPowProofsGenerator,
},
crypto::EncapsulatedMessageWithVerifiedPublicHeader,
provers::{
BlendLayerProof, ProofsGeneratorSettings, WinningPolInfoStream,
leader_and_pow::LeaderAndPowProofsGenerator,
},
};
@@ -1,17 +1,8 @@
use std::{num::NonZeroU64, sync::Arc};
use derivative::Derivative;
use lb_blend_message::{
Error,
encap::{
encapsulated::EncapsulatedMessage,
validated::{
EncapsulatedMessageWithVerifiedPublicHeader, EncapsulatedMessageWithVerifiedSignature,
},
},
};
pub use lb_blend_message::encap::validated::EncapsulatedMessageWithVerifiedPublicHeader;
use lb_blend_proofs::quota::Quota;
use lb_codec::{BinaryDecode as _, BinaryEncode as _};
use lb_key_management_system_keys::keys::X25519PrivateKey;
use rayon::ThreadPool;
@@ -45,28 +36,3 @@ pub struct EpochCryptographicProcessorSettings {
/// generator resumes rather than replaying key nullifiers.
pub spent_core_quota: Quota,
}
#[must_use]
pub fn serialize_encapsulated_message_with_verified_public_header(
message: &EncapsulatedMessageWithVerifiedPublicHeader,
) -> Vec<u8> {
message.encode_to_vec()
}
#[must_use]
pub fn serialize_encapsulated_message_with_verified_signature(
message: &EncapsulatedMessageWithVerifiedSignature,
) -> Vec<u8> {
message.encode_to_vec()
}
pub fn deserialize_encapsulated_message(
message: &[u8],
num_blend_layers: &NonZeroU64,
) -> Result<EncapsulatedMessage, Error> {
let (remaining, deserialized_message) = EncapsulatedMessage::decode(message, num_blend_layers)?;
if !remaining.is_empty() {
return Err(Error::MessageDeserializationFailed);
}
Ok(deserialized_message)
}
@@ -13,7 +13,7 @@ use lb_core::crypto::ZkHash;
use lb_cryptarchia_engine::Epoch;
use lb_key_management_system_keys::keys::Ed25519PublicKey;
use crate::message_blend::{
use crate::{
CoreProofOfQuotaGenerator,
provers::{
BlendLayerProof, ProofsGeneratorSettings, WinningPolInfoStream,
@@ -15,7 +15,7 @@ use lb_log_targets::blend;
use lb_utils::tokio::{stream::Buffered, task::spawn};
use tokio::time::Instant;
use crate::message_blend::{
use crate::{
CoreProofOfQuotaGenerator, buffer_size,
provers::{BlendLayerProof, ProofsGeneratorSettings},
};
@@ -8,7 +8,7 @@ use lb_cryptarchia_engine::Epoch;
use rayon::ThreadPoolBuilder;
use test_log::test;
use crate::message_blend::provers::{
use crate::provers::{
ProofsGeneratorSettings,
core::{CoreProofsGenerator as _, RealCoreProofsGenerator},
test_utils::{
@@ -6,7 +6,7 @@ use lb_blend_proofs::quota::KeyIndex;
use lb_cryptarchia_engine::Epoch;
use lb_log_targets::blend;
use crate::message_blend::{
use crate::{
CoreProofOfQuotaGenerator,
provers::{
BlendLayerProof, ProofsGeneratorSettings, WinningPolInfoStream,
@@ -63,7 +63,7 @@ impl<CorePoQGenerator> RealCoreAndLeaderProofsGenerator<CorePoQGenerator> {
#[cfg(test)]
pub fn override_settings(
&mut self,
new_settings: crate::message_blend::provers::core::CoreProofsGeneratorSettings,
new_settings: crate::provers::core::CoreProofsGeneratorSettings,
) {
self.core_proofs_generator.settings = new_settings.clone();
if let Some(leader_proofs_generator) = &mut self.leader_proofs_generator {
@@ -9,7 +9,7 @@ use lb_cryptarchia_engine::Epoch;
use rayon::ThreadPoolBuilder;
use test_log::test;
use crate::message_blend::provers::{
use crate::provers::{
ProofsGeneratorSettings,
core::CoreProofsGeneratorSettings,
core_and_leader::{CoreAndLeaderProofsGenerator as _, RealCoreAndLeaderProofsGenerator},
@@ -3,7 +3,7 @@ use lb_blend_proofs::quota::KeyIndex;
use lb_cryptarchia_engine::Epoch;
use lb_log_targets::blend;
use crate::message_blend::{
use crate::{
CoreProofOfQuotaGenerator,
provers::{
BlendLayerProof, ProofsGeneratorSettings, WinningPolInfoStream,
@@ -8,7 +8,7 @@ use lb_cryptarchia_engine::Epoch;
use rayon::ThreadPoolBuilder;
use test_log::test;
use crate::message_blend::provers::{
use crate::provers::{
ProofsGeneratorSettings,
core_leader_and_pow::{
CoreLeaderAndPowProofsGenerator as _, RealCoreLeaderAndPowProofsGenerator,
@@ -19,7 +19,7 @@ use lb_log_targets::blend;
use lb_utils::tokio::{stream::Buffered, task::spawn_blocking};
use tokio::time::Instant;
use crate::message_blend::{
use crate::{
buffer_size,
provers::{BlendLayerProof, ProofsGeneratorSettings, WinningPolInfoStream},
};
@@ -8,7 +8,7 @@ use rayon::ThreadPoolBuilder;
use test_log::test;
use tokio::time::timeout;
use crate::message_blend::provers::{
use crate::provers::{
ProofsGeneratorSettings,
leader::{LeaderProofsGenerator as _, RealLeaderProofsGenerator},
test_utils::{
@@ -1,7 +1,7 @@
use async_trait::async_trait;
use lb_log_targets::blend;
use crate::message_blend::provers::{
use crate::provers::{
BlendLayerProof, ProofsGeneratorSettings, WinningPolInfoStream,
leader::{LeaderProofsGenerator as _, RealLeaderProofsGenerator},
pow::{PowProofsGenerator as _, RealPowProofsGenerator},
@@ -6,7 +6,7 @@ use lb_cryptarchia_engine::Epoch;
use rayon::ThreadPoolBuilder;
use test_log::test;
use crate::message_blend::provers::{
use crate::provers::{
ProofsGeneratorSettings,
leader_and_pow::{LeaderAndPowProofsGenerator as _, RealLeaderAndPowProofsGenerator},
test_utils::{
@@ -29,7 +29,7 @@ use rand::rngs::OsRng;
use rayon::{ThreadPool, ThreadPoolBuilder};
use tokio::{sync::oneshot, time::Instant};
use crate::message_blend::provers::{BlendLayerProof, ProofsGeneratorSettings};
use crate::provers::{BlendLayerProof, ProofsGeneratorSettings};
#[cfg(test)]
mod tests;
@@ -11,7 +11,7 @@ use rayon::ThreadPoolBuilder;
use test_log::test;
use tokio::time::timeout;
use crate::message_blend::provers::{
use crate::provers::{
ProofsGeneratorSettings,
pow::{PowProofsGenerator as _, RealPowProofsGenerator},
test_utils::{
@@ -14,7 +14,7 @@ use lb_blend_proofs::quota::{
use lb_core::crypto::ZkHash;
use lb_key_management_system_keys::keys::{ED25519_PUBLIC_KEY_SIZE, Ed25519PublicKey};
use crate::message_blend::CoreProofOfQuotaGenerator;
use crate::CoreProofOfQuotaGenerator;
pub const fn poq_public_inputs_from_epoch_public_inputs_and_signing_key(
(PoQVerificationInputsMinusSigningKey { core, leader, pow }, signing_key): (
+14 -25
View File
@@ -13,35 +13,24 @@ version = { workspace = true }
workspace = true
[dependencies]
async-trait = { workspace = true }
derivative = { workspace = true }
fork_stream = { workspace = true }
futures = { workspace = true }
hex = { workspace = true }
lb-blend-message = { workspace = true }
lb-blend-proofs = { workspace = true }
lb-codec = { workspace = true }
lb-core = { workspace = true }
lb-cryptarchia-engine = { workspace = true }
lb-groth16 = { workspace = true }
lb-key-management-system-keys = { workspace = true }
lb-log-targets = { workspace = true }
lb-utils = { features = ["rng", "tokio"], workspace = true }
multiaddr = { workspace = true }
rand = { features = ["alloc"], workspace = true }
rayon = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
tracing = { workspace = true }
fork_stream = { workspace = true }
futures = { workspace = true }
lb-blend-membership = { workspace = true }
lb-blend-message = { workspace = true }
lb-blend-proofs = { workspace = true }
lb-blend-provers = { workspace = true }
lb-cryptarchia-engine = { workspace = true }
lb-log-targets = { workspace = true }
lb-utils = { features = ["rng", "tokio"], workspace = true }
rand = { features = ["alloc"], workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
tracing = { workspace = true }
[dev-dependencies]
lb-blend-message = { features = ["unsafe-test-functions"], workspace = true }
lb-blend-proofs = { features = ["unsafe-test-functions"], workspace = true }
lb-groth16 = { workspace = true }
libp2p = { workspace = true }
test-log = { features = ["trace"], workspace = true }
[features]
tokio-task-names = ["lb-utils/tokio-task-names"]
+7 -6
View File
@@ -1,13 +1,14 @@
pub mod epoch;
pub mod membership;
pub mod message_blend;
pub use message_blend::crypto::{
deserialize_encapsulated_message, serialize_encapsulated_message_with_verified_public_header,
serialize_encapsulated_message_with_verified_signature,
};
pub mod message_scheduler;
pub use message_scheduler::EpochMessageScheduler;
pub mod stream;
pub use lb_blend_membership as membership;
pub use lb_blend_message::{
deserialize_encapsulated_message, serialize_encapsulated_message_with_verified_public_header,
serialize_encapsulated_message_with_verified_signature,
};
pub use lb_blend_provers as message_blend;
mod cover_traffic;
mod release_delayer;
-312
View File
@@ -1,312 +0,0 @@
use std::{
collections::{HashMap, HashSet},
hash::Hash,
};
use lb_key_management_system_keys::keys::Ed25519PublicKey;
use multiaddr::Multiaddr;
use rand::{Rng, seq::IteratorRandom as _};
use serde::{Deserialize, Serialize};
/// A set of core nodes in an epoch.
#[derive(Clone, Debug)]
pub struct Membership<NodeId> {
/// All nodes, including local and remote.
core_nodes: HashMap<NodeId, Node<NodeId>>,
/// List of node indices, used for proof of selection generation. It
/// contains all nodes in the `nodes` map.
node_indices: Vec<NodeId>,
/// ID of the local node in the `node_indices` vector, if present (i.e., if
/// the local node is a core node).
local_node_index: Option<usize>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Node<Id> {
/// An unique identifier of the node,
/// which is usually corresponding to the network node identifier
/// but depending on the network backend.
pub id: Id,
/// A listening address
pub address: Multiaddr,
/// A public key used for the blend message encryption
pub public_key: Ed25519PublicKey,
}
impl<NodeId> Membership<NodeId>
where
NodeId: Clone + Hash + Eq,
{
#[must_use]
pub fn new(nodes: &[Node<NodeId>], local_public_key: &Ed25519PublicKey) -> Self {
let mut core_nodes = HashMap::with_capacity(nodes.len());
let mut node_indices = Vec::with_capacity(nodes.len());
let mut local_node_index = None;
for (index, node) in nodes.iter().enumerate() {
assert!(
core_nodes.insert(node.id.clone(), node.clone()).is_none(),
"Membership info contained a duplicate node."
);
node_indices.push(node.id.clone());
if node.public_key == *local_public_key {
local_node_index = Some(index);
}
}
Self {
core_nodes,
node_indices,
local_node_index,
}
}
#[cfg(any(test, feature = "unsafe-test-functions"))]
#[must_use]
pub fn new_without_local(nodes: &[Node<NodeId>]) -> Self {
use lb_key_management_system_keys::keys::ED25519_PUBLIC_KEY_SIZE;
Self::new(
nodes,
&Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE]).unwrap(),
)
}
}
impl<NodeId> Membership<NodeId>
where
NodeId: Eq + Hash,
{
/// Choose `amount` random remote nodes.
pub fn choose_remote_nodes<R: Rng>(
&self,
rng: &mut R,
amount: usize,
) -> impl Iterator<Item = &Node<NodeId>> + use<'_, R, NodeId> {
self.filter_and_choose_remote_nodes(rng, amount, &HashSet::new())
}
/// Choose `amount` random remote nodes excluding the given set of node IDs.
pub fn filter_and_choose_remote_nodes<R: Rng>(
&self,
rng: &mut R,
amount: usize,
exclude_peers: &HashSet<NodeId>,
) -> impl Iterator<Item = &Node<NodeId>> + use<'_, R, NodeId> {
self.node_indices
.iter()
.enumerate()
// Filter out excluded peers.
.filter(|(_, node_id)| !exclude_peers.contains(node_id))
// Filter out local node, if the local node is a core node.
.filter(|(index, _)| self.local_node_index != Some(*index))
// Discard index after it's used.
.map(|(_, node)| node)
.choose_multiple(rng, amount)
.into_iter()
.map(|id| {
self.core_nodes
.get(id)
.expect("Node ID must exist in core nodes.")
})
}
pub fn contains(&self, node_id: &NodeId) -> bool {
self.core_nodes.contains_key(node_id)
}
#[must_use]
pub fn get_node_at(&self, index: usize) -> Option<&Node<NodeId>> {
self.core_nodes.get(self.node_indices.get(index)?)
}
}
impl<NodeId> Membership<NodeId> {
#[must_use]
pub const fn local_index(&self) -> Option<usize> {
self.local_node_index
}
#[must_use]
pub const fn contains_local(&self) -> bool {
self.local_node_index.is_some()
}
/// Returns the number of all nodes, including local and remote.
#[must_use]
pub fn size(&self) -> usize {
self.core_nodes.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.core_nodes.is_empty()
}
}
#[cfg(test)]
mod tests {
use lb_key_management_system_keys::keys::UnsecuredEd25519Key;
use rand::rngs::OsRng;
use super::*;
#[test]
fn test_membership_new_with_local_node() {
let nodes = vec![node(1, 1), node(2, 2), node(3, 3)];
let local_key = key(2);
let membership = Membership::new(&nodes, &local_key);
assert_eq!(membership.size(), 3);
assert_eq!(
membership
.core_nodes
.keys()
.copied()
.collect::<HashSet<_>>(),
HashSet::from([1, 2, 3])
);
assert_eq!(membership.node_indices, vec![1, 2, 3]);
assert_eq!(membership.local_node_index, Some(1));
assert!(membership.contains_local());
}
#[test]
fn test_membership_new_without_local_node() {
let nodes = vec![node(1, 1), node(2, 2), node(3, 3)];
let local_key = key(99);
let membership = Membership::new(&nodes, &local_key);
assert_eq!(membership.size(), 3);
assert_eq!(
membership
.core_nodes
.keys()
.copied()
.collect::<HashSet<_>>(),
HashSet::from([1, 2, 3])
);
assert_eq!(membership.node_indices, vec![1, 2, 3]);
assert!(membership.local_node_index.is_none());
assert!(!membership.contains_local());
}
#[test]
fn test_membership_new_empty() {
let local_key = key(99);
let membership = Membership::<u32>::new(&[], &local_key);
assert_eq!(membership.size(), 0);
assert!(membership.core_nodes.keys().next().is_none());
assert!(membership.node_indices.is_empty());
assert!(membership.local_node_index.is_none());
assert!(!membership.contains_local());
}
#[test]
fn test_choose_remote_nodes() {
let nodes = vec![node(1, 1), node(2, 2), node(3, 3), node(4, 4)];
let local_key = key(99);
let membership = Membership::new(&nodes, &local_key);
let chosen: HashSet<_> = membership
.choose_remote_nodes(&mut OsRng, 2)
.map(|node| node.id)
.collect();
assert_eq!(chosen.len(), 2);
}
#[test]
fn test_choose_remote_nodes_more_than_available() {
let nodes = vec![node(1, 1), node(2, 2)];
let local_key = key(99);
let membership = Membership::new(&nodes, &local_key);
let chosen: HashSet<_> = membership
.choose_remote_nodes(&mut OsRng, 5)
.map(|node| node.id)
.collect();
assert_eq!(chosen.len(), 2);
}
#[test]
fn test_choose_remote_nodes_zero() {
let nodes = vec![node(1, 1), node(2, 2)];
let local_key = key(99);
let membership = Membership::new(&nodes, &local_key);
let mut rng = OsRng;
let mut chosen = membership.choose_remote_nodes(&mut rng, 0);
assert!(chosen.next().is_none());
}
#[test]
fn test_filter_and_choose_remote_nodes() {
let nodes = vec![node(1, 1), node(2, 2), node(3, 3)];
let local_key = key(99);
let membership = Membership::new(&nodes, &local_key);
let exclude_peers = HashSet::from([3]);
let chosen: HashSet<_> = membership
.filter_and_choose_remote_nodes(&mut OsRng, 2, &exclude_peers)
.map(|node| node.id)
.collect();
assert_eq!(chosen.len(), 2);
}
#[test]
fn test_filter_and_choose_remote_nodes_all_excluded() {
let nodes = vec![node(1, 1), node(2, 2)];
let local_key = key(99);
let membership = Membership::new(&nodes, &local_key);
let exclude_peers = HashSet::from([1, 2]);
let chosen: HashSet<_> = membership
.filter_and_choose_remote_nodes(&mut OsRng, 2, &exclude_peers)
.map(|node| node.id)
.collect();
assert!(chosen.is_empty());
}
#[test]
fn test_contains() {
let nodes = vec![node(1, 1)];
let local_key = key(99);
let membership = Membership::new(&nodes, &local_key);
assert!(membership.contains(&1));
assert!(!membership.contains(&2));
}
#[test]
fn get_node_at() {
let nodes = vec![node(1, 1)];
let local_key = key(99);
let membership = Membership::new(&nodes, &local_key);
assert_eq!(membership.get_node_at(0), Some(&node(1, 1)));
assert_eq!(membership.get_node_at(1), None);
}
#[test]
#[should_panic(expected = "Membership info contained a duplicate node.")]
fn duplicate_remote_node() {
let nodes = vec![node(1, 1), node(1, 2)];
let local_key = key(99);
drop(Membership::new(&nodes, &local_key));
}
fn key(seed: u8) -> Ed25519PublicKey {
UnsecuredEd25519Key::from_bytes(&[seed; 32]).public_key()
}
fn node(id: u32, seed: u8) -> Node<u32> {
Node {
id,
address: Multiaddr::empty(),
public_key: key(seed),
}
}
}