add peer config happy path tests

This commit is contained in:
Georgio Nicolas
2026-02-25 17:30:59 +01:00
committed by Jędrzej Stuczyński
parent 7042d155eb
commit 7dd0f6cfa1
+290 -34
View File
@@ -1,69 +1,161 @@
use std::char::MAX;
use crate::LpError;
use rand09::{self, CryptoRng, Rng};
use crate::LpError;
pub const MAX_HOPS: u8 = 16;
pub const LP_PEER_CONFIG_SIZE: usize = 20;
const SEED_LEN: usize = 16;
const CONFIG_LEN: usize = 1;
const FILLER_LEN: usize = LP_PEER_CONFIG_SIZE - SEED_LEN - CONFIG_LEN;
// 20 bytes
#[derive(PartialEq)]
pub struct LpPeerConfig {
// these 3 fields will be packed in one u8
// these FILLER_LEN fields will be packed in one u8
// with 2 bits left at the end
// determine the hop id.
// should be 0 if node_initiator is true
// should be > 1 if is_exit is true
hop_id: u8,
// determine if the recipient should be an exit node
is_exit: bool,
// determine if we are establishing a node<>node connection
// should be false if is_exit is true
node_initiator: bool,
// enable censorship resistance countermeasures
censorship_resistance: bool,
// if we add config params later, we can use this
filler: [u8; 3],
filler: [u8; FILLER_LEN],
seed: [u8; 16],
seed: [u8; SEED_LEN],
}
impl LpPeerConfig {
pub fn new<R>(
pub fn new_client_to_entry<R>(rng: &mut R, censorship_resistance: bool) -> Self
where
R: Rng + CryptoRng,
{
Self::build(
0,
false,
false,
censorship_resistance,
rng.random(),
rng.random(),
)
}
pub fn new_client_to_exit<R>(
rng: &mut R,
hop_id: u8,
is_exit: bool,
censorship_resistance: bool,
) -> Result<Self, LpError>
where
R: Rng + CryptoRng,
{
let seed: [u8; 16] = rng.random();
let filler: [u8; 3] = rng.random();
Self::build(hop_id, is_exit, censorship_resistance, seed, filler)
Self::new(rng, hop_id, true, false, censorship_resistance)
}
pub fn new_client_to_intermediate<R>(
rng: &mut R,
hop_id: u8,
censorship_resistance: bool,
) -> Result<Self, LpError>
where
R: Rng + CryptoRng,
{
Self::new(rng, hop_id, false, false, censorship_resistance)
}
pub fn new_node_to_node<R>(rng: &mut R) -> Result<Self, LpError>
where
R: Rng + CryptoRng,
{
// no need for censorship resistance between nodes (for now)
// hop_id between nodes is 0
Self::new(rng, 0, false, true, false)
}
pub fn new<R>(
rng: &mut R,
hop_id: u8,
is_exit: bool,
node_initiator: bool,
censorship_resistance: bool,
) -> Result<Self, LpError>
where
R: Rng + CryptoRng,
{
Self::build_checked(
hop_id,
is_exit,
node_initiator,
censorship_resistance,
rng.random(),
rng.random(),
)
}
fn build(
hop_id: u8,
is_exit: bool,
node_initiator: bool,
censorship_resistance: bool,
seed: [u8; 16],
filler: [u8; 3],
seed: [u8; SEED_LEN],
filler: [u8; FILLER_LEN],
) -> Self {
Self {
hop_id,
is_exit,
node_initiator,
censorship_resistance,
filler,
seed,
}
}
fn build_checked(
hop_id: u8,
is_exit: bool,
node_initiator: bool,
censorship_resistance: bool,
seed: [u8; SEED_LEN],
filler: [u8; FILLER_LEN],
) -> Result<Self, LpError> {
if hop_id >= MAX_HOPS {
if node_initiator && is_exit {
Err(LpError::Internal(
"A node cannot establish an exit node for itself.".into(),
))
} else if node_initiator && hop_id != 0 {
Err(LpError::Internal(
"Hop id in node to node connections must be zero.".into(),
))
} else if !node_initiator && hop_id >= MAX_HOPS {
Err(LpError::Internal(format!(
"Requested hop index ({}) is greater than the allowed maximum {}.",
hop_id,
MAX_HOPS - 1
)))
} else if is_exit && hop_id == 0 {
} else if !node_initiator && is_exit && hop_id == 0 {
Err(LpError::Internal(
"Hop index for exit node cannot be zero".into(),
"Hop id for exit node cannot be zero.".into(),
))
} else if !node_initiator && !is_exit && hop_id == 15 {
Err(LpError::Internal(
"The hop with id 15 must be an exit node.".into(),
))
} else {
Ok(Self {
Ok(Self::build(
hop_id,
is_exit,
node_initiator,
censorship_resistance,
seed,
filler,
})
))
}
}
@@ -73,7 +165,7 @@ impl LpPeerConfig {
output_bytes[4..].copy_from_slice(&self.seed);
output_bytes
}
pub fn deserialize(bytes: &[u8]) -> Result<Self, LpError> {
pub fn deserialize(bytes: &[u8]) -> Result<Self, LpError> {
if bytes.len() != LP_PEER_CONFIG_SIZE {
Err(LpError::DeserializationError(format!(
"Invalid Lp Config Length ({}), expected ({})",
@@ -81,12 +173,23 @@ impl LpPeerConfig {
LP_PEER_CONFIG_SIZE
)))
} else {
let (hop_id, is_exit, censorship_resistance) = Self::unpack_first_byte(bytes[0]);
let (hop_id, is_exit, node_initiator, censorship_resistance) =
Self::unpack_first_byte(bytes[0]);
let filler: [u8; 3] = [bytes[1], bytes[2], bytes[3]];
let mut seed: [u8; 16] = [0u8; 16];
seed.copy_from_slice(&bytes[4..LP_PEER_CONFIG_SIZE]);
Self::build(hop_id, is_exit, censorship_resistance, seed, filler)
let mut filler: [u8; FILLER_LEN] = [0u8; FILLER_LEN];
filler.copy_from_slice(&bytes[CONFIG_LEN..CONFIG_LEN + FILLER_LEN]);
let mut seed: [u8; SEED_LEN] = [0u8; SEED_LEN];
seed.copy_from_slice(&bytes[CONFIG_LEN + FILLER_LEN..LP_PEER_CONFIG_SIZE]);
Self::build_checked(
hop_id,
is_exit,
node_initiator,
censorship_resistance,
seed,
filler,
)
}
}
@@ -102,28 +205,181 @@ impl LpPeerConfig {
fn pack_first_byte(&self) -> u8 {
let mut byte = self.hop_id;
// Set the 5th bit to determine if the node is an exit node
if self.is_exit {
byte |= 0b0001_0000;
}
if self.censorship_resistance {
// Set the 6th bit to determine if we're establishing a node to node connection
if self.node_initiator {
byte |= 0b0010_0000;
}
// Set the 7th bit to determine if we should use censorship resistance measures
if self.censorship_resistance {
byte |= 0b0100_0000;
}
// there will be 2 free bits at the end
// There will be 1 free bit at the end
byte
}
fn unpack_first_byte(byte: u8) -> (u8, bool, bool) {
fn unpack_first_byte(byte: u8) -> (u8, bool, bool, bool) {
// extract 4 bits
let hop_id = byte & 0b0000_1111;
// extract 5th bit
let is_exit = (byte & 0b0001_0000) >> 4 == 1;
// extract 6th bit
let censorship_resistance = (byte & 0b0010_0000) >> 5 == 1;
let node_initiator = (byte & 0b0010_0000) >> 5 == 1;
// extract 7th bit
let censorship_resistance = (byte & 0b0100_0000) >> 6 == 1;
// if we need to use the last 2 bits, we can add something here
(hop_id, is_exit, censorship_resistance)
// If we need to use the last bit, we can add something here
(hop_id, is_exit, node_initiator, censorship_resistance)
}
pub fn is_client_entry(&self) -> bool {
self.hop_id == 0 && !self.is_exit && !self.node_initiator
}
pub fn is_client_intermediate_node(&self) -> bool {
self.hop_id > 0 && !self.is_exit && !self.node_initiator
}
pub fn is_client_exit(&self) -> bool {
self.hop_id > 0 && self.is_exit && !self.node_initiator
}
pub fn is_node_to_node(&self) -> bool {
self.hop_id == 0 && !self.is_exit && self.node_initiator
}
}
#[cfg(test)]
mod test {
use crate::peer_config::LpPeerConfig;
#[test]
fn test_pack_config() {
let mut rng = rand09::rng();
// Node to node, no censorship resistance
{
let expected_conf = 0b0010_0000;
let conf = LpPeerConfig::new(&mut rng, 0, false, true, false).unwrap();
let conf_bytes = conf.serialize();
let deserialized_conf_first_byte = LpPeerConfig::deserialize(&conf_bytes)
.unwrap()
.pack_config()[0];
assert!(expected_conf == conf_bytes[0]);
assert!(expected_conf == deserialized_conf_first_byte);
assert!(
conf_bytes[0]
== LpPeerConfig::new_node_to_node(&mut rng)
.unwrap()
.serialize()[0]
);
assert!(conf.is_node_to_node());
}
// Node to node, with censorship resistance
{
let expected_conf = 0b0110_0000;
let conf = LpPeerConfig::new(&mut rng, 0, false, true, true).unwrap();
let conf_bytes = conf.serialize();
let deserialized_conf_first_byte = LpPeerConfig::deserialize(&conf_bytes)
.unwrap()
.pack_config()[0];
assert!(expected_conf == conf_bytes[0]);
assert!(expected_conf == deserialized_conf_first_byte);
assert!(conf.is_node_to_node());
}
// Client to Entry, no censorship resistance
{
let expected_conf = 0b0000_0000;
let conf = LpPeerConfig::new(&mut rng, 0, false, false, false).unwrap();
let conf_bytes = conf.serialize();
let deserialized_conf_first_byte = LpPeerConfig::deserialize(&conf_bytes)
.unwrap()
.pack_config()[0];
let conf_alt_first_byte =
LpPeerConfig::new_client_to_entry(&mut rng, false).serialize()[0];
assert!(expected_conf == conf_bytes[0]);
assert!(expected_conf == deserialized_conf_first_byte);
assert!(conf_bytes[0] == conf_alt_first_byte);
assert!(conf.is_client_entry())
}
// Client to Entry, with censorship resistance
{
let expected_conf = 0b0100_0000;
let conf = LpPeerConfig::new(&mut rng, 0, false, false, true).unwrap();
let conf_bytes = conf.serialize();
let deserialized_conf_first_byte = LpPeerConfig::deserialize(&conf_bytes)
.unwrap()
.pack_config()[0];
let conf_alt_first_byte =
LpPeerConfig::new_client_to_entry(&mut rng, true).serialize()[0];
assert!(expected_conf == conf_bytes[0]);
assert!(expected_conf == deserialized_conf_first_byte);
assert!(conf_bytes[0] == conf_alt_first_byte);
assert!(conf.is_client_entry());
}
// Client to Exit(exit hop = 1), with censorship resistance
{
let expected_conf = 0b0101_0001;
let conf = LpPeerConfig::new(&mut rng, 1, true, false, true).unwrap();
let conf_bytes = conf.serialize();
let deserialized_conf_first_byte = LpPeerConfig::deserialize(&conf_bytes)
.unwrap()
.pack_config()[0];
let conf_alt_first_byte =
LpPeerConfig::new_client_to_exit(&mut rng, 1, true).unwrap().serialize()[0];
assert!(expected_conf == conf_bytes[0]);
assert!(expected_conf == deserialized_conf_first_byte);
assert!(conf_bytes[0] == conf_alt_first_byte);
assert!(conf.is_client_exit());
}
// Client to Exit(exit hop = 2), without censorship resistance
{
let expected_conf = 0b0001_0010;
let conf = LpPeerConfig::new(&mut rng, 2, true, false, false).unwrap();
let conf_bytes = conf.serialize();
let deserialized_conf_first_byte = LpPeerConfig::deserialize(&conf_bytes)
.unwrap()
.pack_config()[0];
let conf_alt_first_byte =
LpPeerConfig::new_client_to_exit(&mut rng, 2, false).unwrap().serialize()[0];
assert!(expected_conf == conf_bytes[0]);
assert!(expected_conf == deserialized_conf_first_byte);
assert!(conf_bytes[0] == conf_alt_first_byte);
assert!(conf.is_client_exit());
}
// Client to Intermediate (hop_id = 14), without censorship resistance
{
let expected_conf = 0b0000_1110;
let conf = LpPeerConfig::new(&mut rng, 14, false, false, false).unwrap();
let conf_bytes = conf.serialize();
let deserialized_conf_first_byte = LpPeerConfig::deserialize(&conf_bytes)
.unwrap()
.pack_config()[0];
let conf_alt_first_byte =
LpPeerConfig::new_client_to_intermediate(&mut rng, 14, false).unwrap().serialize()[0];
assert!(expected_conf == conf_bytes[0]);
assert!(expected_conf == deserialized_conf_first_byte);
assert!(conf_bytes[0] == conf_alt_first_byte);
assert!(conf.is_client_intermediate_node());
}
}
}