node: Always send node announcement on handshake

Previously, we only sent the announcement when there were external
addresses. But it contains other information like the node alias,
therefore we always send it.

Since the proof-of-work can be expensive to compute everytime, we add
the ability to load the local node announcement from the file system.

We also change the proof-of-work parameters so that they are relaxed
when running in debug mode, instead of when running tests.

Finally, we remove the boolean validation of the PoW. Instead, we store
the work per-node, in the database, to be used in the future in case of
DoS attacks or network congestion.
This commit is contained in:
Alexis Sellier 2023-06-02 18:38:50 +02:00
parent e86e061a12
commit ff7338de17
No known key found for this signature in database
9 changed files with 182 additions and 86 deletions

View File

@ -4,7 +4,7 @@ use std::io::{BufRead, BufReader};
use std::os::unix::net::UnixListener; use std::os::unix::net::UnixListener;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::{io, net, thread, time}; use std::{fs, io, net, thread, time};
use crossbeam_channel as chan; use crossbeam_channel as chan;
use cyphernet::Ecdh; use cyphernet::Ecdh;
@ -16,16 +16,17 @@ use thiserror::Error;
use radicle::git; use radicle::git;
use radicle::node::address; use radicle::node::address;
use radicle::node::Handle as _; use radicle::node::Handle as _;
use radicle::node::{ADDRESS_DB_FILE, ROUTING_DB_FILE, TRACKING_DB_FILE}; use radicle::node::{ADDRESS_DB_FILE, NODE_ANNOUNCEMENT_FILE, ROUTING_DB_FILE, TRACKING_DB_FILE};
use radicle::profile::Home; use radicle::profile::Home;
use radicle::Storage; use radicle::Storage;
use crate::control; use crate::control;
use crate::crypto::Signer; use crate::crypto::Signer;
use crate::node::{routing, NodeId}; use crate::node::{routing, NodeId};
use crate::service::message::NodeAnnouncement;
use crate::service::{tracking, Event}; use crate::service::{tracking, Event};
use crate::wire;
use crate::wire::Wire; use crate::wire::Wire;
use crate::wire::{self, Decode};
use crate::worker; use crate::worker;
use crate::{service, LocalTime}; use crate::{service, LocalTime};
@ -147,6 +148,31 @@ impl Runtime {
log::info!(target: "node", "Default tracking policy set to '{}'", &config.policy); log::info!(target: "node", "Default tracking policy set to '{}'", &config.policy);
log::info!(target: "node", "Initializing service ({:?})..", network); log::info!(target: "node", "Initializing service ({:?})..", network);
let announcement = if let Some(ann) = fs::read(&node_dir.join(NODE_ANNOUNCEMENT_FILE))
.ok()
.and_then(|ann| NodeAnnouncement::decode(&mut ann.as_slice()).ok())
.and_then(|ann| {
if config.matches(&ann) {
Some(ann)
} else {
None
}
}) {
log::info!(
target: "node",
"Loaded existing node announcement from file (timestamp={}, work={})",
ann.timestamp,
ann.work(),
);
ann
} else {
config
.node(clock.as_secs())
.solve(Default::default())
.expect("Runtime::init: unable to solve proof-of-work puzzle")
};
let emitter: Emitter<Event> = Default::default(); let emitter: Emitter<Event> = Default::default();
let service = service::Service::new( let service = service::Service::new(
config, config,
@ -157,6 +183,7 @@ impl Runtime {
tracking, tracking,
signer.clone(), signer.clone(),
rng, rng,
announcement,
emitter.clone(), emitter.clone(),
); );

View File

@ -12,7 +12,7 @@ use std::collections::hash_map::Entry;
use std::collections::{BTreeMap, HashMap, HashSet}; use std::collections::{BTreeMap, HashMap, HashSet};
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use std::sync::Arc; use std::sync::Arc;
use std::{fmt, net, str}; use std::{fmt, net};
use crossbeam_channel as chan; use crossbeam_channel as chan;
use fastrand::Rng; use fastrand::Rng;
@ -26,7 +26,6 @@ use crate::crypto;
use crate::crypto::{Signer, Verified}; use crate::crypto::{Signer, Verified};
use crate::identity::IdentityError; use crate::identity::IdentityError;
use crate::identity::{Doc, Id}; use crate::identity::{Doc, Id};
use crate::node;
use crate::node::routing; use crate::node::routing;
use crate::node::routing::InsertResult; use crate::node::routing::InsertResult;
use crate::node::{Address, Features, FetchResult, Seed, Seeds}; use crate::node::{Address, Features, FetchResult, Seed, Seeds};
@ -197,6 +196,8 @@ pub struct Service<R, A, S, G> {
clock: LocalTime, clock: LocalTime,
/// I/O outbox. /// I/O outbox.
outbox: Outbox, outbox: Outbox,
/// Cached local node announcement.
node: NodeAnnouncement,
/// Source of entropy. /// Source of entropy.
rng: Rng, rng: Rng,
/// Fetch requests initiated by user, which are waiting for results. /// Fetch requests initiated by user, which are waiting for results.
@ -248,6 +249,7 @@ where
tracking: tracking::Config, tracking: tracking::Config,
signer: G, signer: G,
rng: Rng, rng: Rng,
node: NodeAnnouncement,
emitter: Emitter<Event>, emitter: Emitter<Event>,
) -> Self { ) -> Self {
let sessions = Sessions::new(rng.clone()); let sessions = Sessions::new(rng.clone());
@ -259,6 +261,7 @@ where
tracking, tracking,
signer, signer,
rng, rng,
node,
clock, clock,
routing, routing,
gossip: Gossip::default(), gossip: Gossip::default(),
@ -397,6 +400,21 @@ where
} }
} }
} }
// Ensure that our local node is in our address database.
self.addresses
.insert(
&self.node_id(),
self.node.features,
self.node.alias().unwrap_or_default(),
self.node.work(),
self.node.timestamp,
self.node
.addresses
.iter()
.map(|a| KnownAddress::new(a.clone(), address::Source::Peer)),
)
.expect("Service::initialize: error adding local node to address database");
// Setup subscription filter for tracked repos. // Setup subscription filter for tracked repos.
self.filter = Filter::new( self.filter = Filter::new(
self.tracking self.tracking
@ -968,7 +986,6 @@ where
AnnouncementMessage::Node( AnnouncementMessage::Node(
ann @ NodeAnnouncement { ann @ NodeAnnouncement {
features, features,
alias,
addresses, addresses,
.. ..
}, },
@ -980,12 +997,7 @@ where
return Ok(false); return Ok(false);
} }
if !ann.validate() { let alias = match ann.alias() {
warn!(target: "service", "Dropping node announcement from {announcer}: invalid proof-of-work");
return Ok(false);
}
let alias = match str::from_utf8(alias) {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
warn!(target: "service", "Dropping node announcement from {announcer}: invalid alias: {e}"); warn!(target: "service", "Dropping node announcement from {announcer}: invalid alias: {e}");
@ -1003,6 +1015,7 @@ where
announcer, announcer,
*features, *features,
alias, alias,
ann.work(),
timestamp, timestamp,
addresses addresses
.iter() .iter()
@ -1152,11 +1165,11 @@ where
// much bandwidth. // much bandwidth.
gossip::handshake( gossip::handshake(
self.node.clone(),
self.clock.as_millis(), self.clock.as_millis(),
&self.storage, &self.storage,
&self.signer, &self.signer,
filter, filter,
&self.config,
) )
} }
@ -1287,6 +1300,10 @@ where
warn!(target: "service", "Attempted connection to peer {nid} which already has a session"); warn!(target: "service", "Attempted connection to peer {nid} which already has a session");
return false; return false;
} }
if nid == self.node_id() {
error!(target: "service", "Attempted connection to self");
return false;
}
let persistent = self.config.is_persistent(&nid); let persistent = self.config.is_persistent(&nid);
if let Err(e) = self.addresses.attempted(&nid, &addr, self.time()) { if let Err(e) = self.addresses.attempted(&nid, &addr, self.time()) {
@ -1426,6 +1443,7 @@ where
// even if it's in a disconnected state. Those sessions are re-attempted automatically. // even if it's in a disconnected state. Those sessions are re-attempted automatically.
entries entries
.filter(|(nid, _)| !self.sessions.contains_key(nid)) .filter(|(nid, _)| !self.sessions.contains_key(nid))
.filter(|(nid, _)| nid != &self.node_id())
.take(wanted) .take(wanted)
.collect() .collect()
} }
@ -1796,11 +1814,11 @@ mod gossip {
} }
pub fn handshake<G: Signer, S: ReadStorage>( pub fn handshake<G: Signer, S: ReadStorage>(
node: NodeAnnouncement,
now: Timestamp, now: Timestamp,
storage: &S, storage: &S,
signer: &G, signer: &G,
filter: Filter, filter: Filter,
config: &Config,
) -> Vec<Message> { ) -> Vec<Message> {
let inventory = match storage.inventory() { let inventory = match storage.inventory() {
Ok(i) => i, Ok(i) => i,
@ -1812,44 +1830,15 @@ mod gossip {
} }
}; };
let mut msgs = vec![ vec![
Message::node(node, signer),
Message::inventory(gossip::inventory(now, inventory), signer), Message::inventory(gossip::inventory(now, inventory), signer),
Message::subscribe( Message::subscribe(
filter, filter,
now - SUBSCRIBE_BACKLOG_DELTA.as_millis() as u64, now - SUBSCRIBE_BACKLOG_DELTA.as_millis() as u64,
Timestamp::MAX, Timestamp::MAX,
), ),
]; ]
if let Some(m) = gossip::node(now, config) {
msgs.push(Message::node(m, signer));
};
msgs
}
pub fn node(timestamp: Timestamp, config: &Config) -> Option<NodeAnnouncement> {
let features = node::Features::SEED;
let alias = config.alias();
let addresses: BoundedVec<_, ADDRESS_LIMIT> = config
.external_addresses
.clone()
.try_into()
.expect("external addresses are within the limit");
if addresses.is_empty() {
return None;
}
Some(
NodeAnnouncement {
features,
timestamp,
alias,
addresses,
nonce: 0,
}
.solve(),
)
} }
pub fn inventory(timestamp: Timestamp, inventory: Vec<Id>) -> InventoryAnnouncement { pub fn inventory(timestamp: Timestamp, inventory: Vec<Id>) -> InventoryAnnouncement {

View File

@ -1,7 +1,10 @@
use localtime::LocalDuration; use localtime::LocalDuration;
use radicle::node;
use radicle::node::Address; use radicle::node::Address;
use crate::bounded::BoundedVec;
use crate::service::message::{NodeAnnouncement, ADDRESS_LIMIT};
use crate::service::tracking::{Policy, Scope}; use crate::service::tracking::{Policy, Scope};
use crate::service::NodeId; use crate::service::NodeId;
@ -88,13 +91,43 @@ impl Config {
self.connect.iter().any(|(i, _)| i == id) self.connect.iter().any(|(i, _)| i == id)
} }
pub fn features(&self) -> node::Features {
node::Features::SEED
}
/// Check if a node announcement matches this configuration.
pub fn matches(&self, other: &NodeAnnouncement) -> bool {
let ann = self.node(other.timestamp);
ann.features == other.features
&& ann.alias == other.alias
&& ann.addresses == other.addresses
}
pub fn alias(&self) -> [u8; 32] { pub fn alias(&self) -> [u8; 32] {
let mut alias = [0u8; 32]; let mut alias = [0u8; 32];
if let Some(name) = &self.alias { if let Some(name) = &self.alias {
alias[..name.len()].copy_from_slice(name.as_bytes()); alias[..name.len()].copy_from_slice(name.as_bytes());
} }
alias alias
} }
pub fn node(&self, timestamp: node::Timestamp) -> NodeAnnouncement {
let features = self.features();
let alias = self.alias();
let addresses: BoundedVec<_, ADDRESS_LIMIT> = self
.external_addresses
.clone()
.try_into()
.expect("external addresses are within the limit");
NodeAnnouncement {
features,
timestamp,
alias,
addresses,
nonce: 0,
}
}
} }

View File

@ -1,4 +1,4 @@
use std::{fmt, io, mem}; use std::{fmt, io, mem, str};
use crate::crypto; use crate::crypto;
use crate::crypto::Unverified; use crate::crypto::Unverified;
@ -66,17 +66,22 @@ pub struct NodeAnnouncement {
} }
impl NodeAnnouncement { impl NodeAnnouncement {
/// Validate a node announcement message. /// Calculate the amount of work that went into creating this announcement.
/// ///
/// Checks that the proof-of-work is valid, by generating a single byte that /// Proof-of-work uses the [`scrypt`] algorithm with the parameters in
/// must be zero. /// [`Announcement::POW_PARAMS`]. The "work" is calculated by counting the number of leading
/// zero bits after running `scrypt` on a serialized [`NodeAnnouncement`] using
/// [`wire::serialize`].
/// ///
/// `scrypt(encode(announcement)) == 0` /// In other words, `work = leading-zeros(scrypt(serialize(announcement)))`.
/// ///
pub fn validate(&self) -> bool { /// Higher numbers mean higher difficulty. For each increase in work, difficulty is doubled.
/// For instance, an output of `7` is *four* times more work than an output of `5`.
///
pub fn work(&self) -> u32 {
let (n, r, p) = Announcement::POW_PARAMS; let (n, r, p) = Announcement::POW_PARAMS;
let params = scrypt::Params::new(n, r, p).expect("proof-of-work parameters are valid"); let params = scrypt::Params::new(n, r, p).expect("proof-of-work parameters are valid");
let mut output = [0; 1]; let mut output = vec![0; 32];
scrypt::scrypt( scrypt::scrypt(
wire::serialize(self).as_ref(), wire::serialize(self).as_ref(),
@ -86,26 +91,37 @@ impl NodeAnnouncement {
) )
.expect("proof-of-work output vector is a valid length"); .expect("proof-of-work output vector is a valid length");
output == [0] // Calculate the number of leading zero bits in the output vector.
if let Some((zero_bytes, non_zero)) = output.iter().enumerate().find(|(_, &x)| x != 0) {
zero_bytes as u32 * 8 + non_zero.leading_zeros()
} else {
output.len() as u32 * 8
}
} }
/// Solve the proof-of-work of a node announcement by iterating through different nonces. /// Solve the proof-of-work of a node announcement for the given target, by iterating through
pub fn solve(mut self) -> Self { /// different nonces.
///
/// If the given difficulty target is too high, there may not be a result. In that case, `None`
/// is returned.
pub fn solve(mut self, target: u32) -> Option<Self> {
loop { loop {
if let Some(nonce) = self.nonce.checked_add(1) { if let Some(nonce) = self.nonce.checked_add(1) {
self.nonce = nonce; self.nonce = nonce;
if self.validate() { if self.work() >= target {
break; break;
} }
} else { } else {
// If a very high difficulty is chosen, it's possible to iterate through all return None;
// possible values of the nonce without solving the puzzle. However, with "normal"
// values, this is virtually impossible.
panic!("could not solve proof-of-work!");
} }
} }
self Some(self)
}
/// Get the alias as a UTF-8 string.
pub fn alias(&self) -> Result<&str, std::str::Utf8Error> {
str::from_utf8(&self.alias)
} }
} }
@ -307,9 +323,9 @@ impl Announcement {
/// ///
/// `15, 8, 1` are usually the recommended parameters. /// `15, 8, 1` are usually the recommended parameters.
/// ///
#[cfg(test)] #[cfg(debug_assertions)]
pub const POW_PARAMS: (u8, u32, u32) = (1, 1, 1); pub const POW_PARAMS: (u8, u32, u32) = (1, 1, 1);
#[cfg(not(test))] #[cfg(not(debug_assertions))]
pub const POW_PARAMS: (u8, u32, u32) = (15, 8, 1); pub const POW_PARAMS: (u8, u32, u32) = (15, 8, 1);
/// Salt used for generating PoW. /// Salt used for generating PoW.
pub const POW_SALT: &[u8] = &[b'r', b'a', b'd']; pub const POW_SALT: &[u8] = &[b'r', b'a', b'd'];
@ -608,7 +624,9 @@ mod tests {
nonce: 0, nonce: 0,
}; };
assert!(!ann.validate()); assert_eq!(ann.work(), 0);
assert!(ann.solve().validate()); assert_eq!(ann.clone().solve(1).unwrap().work(), 4);
assert_eq!(ann.clone().solve(8).unwrap().work(), 9);
assert_eq!(ann.solve(14).unwrap().work(), 16);
} }
} }

View File

@ -162,6 +162,7 @@ where
// Make sure the peer address is advertized. // Make sure the peer address is advertized.
config.config.external_addresses.push(local_addr.into()); config.config.external_addresses.push(local_addr.into());
let announcement = config.config.node(config.local_time.as_secs());
let emitter: Emitter<Event> = Default::default(); let emitter: Emitter<Event> = Default::default();
let service = Service::new( let service = Service::new(
config.config, config.config,
@ -172,6 +173,7 @@ where
tracking, tracking,
config.signer, config.signer,
config.rng.clone(), config.rng.clone(),
announcement,
emitter, emitter,
); );
@ -213,6 +215,7 @@ where
&peer.node_id(), &peer.node_id(),
radicle::node::Features::default(), radicle::node::Features::default(),
peer.name, peer.name,
0,
timestamp, timestamp,
Some(known_address), Some(known_address),
) )
@ -266,7 +269,8 @@ where
addresses: Some(net::SocketAddr::from((self.ip, node::DEFAULT_PORT)).into()).into(), addresses: Some(net::SocketAddr::from((self.ip, node::DEFAULT_PORT)).into()).into(),
nonce: 0, nonce: 0,
} }
.solve(), .solve(0)
.unwrap(),
self.signer(), self.signer(),
) )
} }

View File

@ -37,6 +37,12 @@ pub const ROUTING_DB_FILE: &str = "routing.db";
pub const ADDRESS_DB_FILE: &str = "addresses.db"; pub const ADDRESS_DB_FILE: &str = "addresses.db";
/// Filename of tracking table database under the node directory. /// Filename of tracking table database under the node directory.
pub const TRACKING_DB_FILE: &str = "tracking.db"; pub const TRACKING_DB_FILE: &str = "tracking.db";
/// Filename of last node announcement, when running in debug mode.
#[cfg(debug_assertions)]
pub const NODE_ANNOUNCEMENT_FILE: &str = "announcement.wire.debug";
/// Filename of last node announcement.
#[cfg(not(debug_assertions))]
pub const NODE_ANNOUNCEMENT_FILE: &str = "announcement.wire";
/// Milliseconds since epoch. /// Milliseconds since epoch.
pub type Timestamp = u64; pub type Timestamp = u64;

View File

@ -8,6 +8,8 @@ create table if not exists "nodes" (
"features" integer not null, "features" integer not null,
-- Node alias. -- Node alias.
"alias" text default null, "alias" text default null,
--- Node announcement proof-of-work.
"pow" integer default 0,
-- Node announcement timestamp. -- Node announcement timestamp.
"timestamp" integer not null "timestamp" integer not null
-- --

View File

@ -71,7 +71,7 @@ impl Store for Book {
fn get(&self, node: &NodeId) -> Result<Option<types::Node>, Error> { fn get(&self, node: &NodeId) -> Result<Option<types::Node>, Error> {
let mut stmt = self let mut stmt = self
.db .db
.prepare("SELECT features, alias, timestamp FROM nodes WHERE id = ?")?; .prepare("SELECT features, alias, pow, timestamp FROM nodes WHERE id = ?")?;
stmt.bind((1, node))?; stmt.bind((1, node))?;
@ -79,6 +79,7 @@ impl Store for Book {
let features = row.read::<node::Features, _>("features"); let features = row.read::<node::Features, _>("features");
let alias = row.read::<&str, _>("alias").to_owned(); let alias = row.read::<&str, _>("alias").to_owned();
let timestamp = row.read::<i64, _>("timestamp") as Timestamp; let timestamp = row.read::<i64, _>("timestamp") as Timestamp;
let pow = row.read::<i64, _>("pow") as u32;
let mut addrs = Vec::new(); let mut addrs = Vec::new();
let mut stmt = self let mut stmt = self
@ -103,6 +104,7 @@ impl Store for Book {
Ok(Some(types::Node { Ok(Some(types::Node {
features, features,
alias, alias,
pow,
timestamp, timestamp,
addrs, addrs,
})) }))
@ -129,22 +131,24 @@ impl Store for Book {
node: &NodeId, node: &NodeId,
features: node::Features, features: node::Features,
alias: &str, alias: &str,
pow: u32,
timestamp: Timestamp, timestamp: Timestamp,
addrs: impl IntoIterator<Item = KnownAddress>, addrs: impl IntoIterator<Item = KnownAddress>,
) -> Result<bool, Error> { ) -> Result<bool, Error> {
transaction(&self.db, move |db| { transaction(&self.db, move |db| {
let mut stmt = db.prepare( let mut stmt = db.prepare(
"INSERT INTO nodes (id, features, alias, timestamp) "INSERT INTO nodes (id, features, alias, pow, timestamp)
VALUES (?1, ?2, ?3, ?4) VALUES (?1, ?2, ?3, ?4, ?5)
ON CONFLICT DO UPDATE ON CONFLICT DO UPDATE
SET features = ?2, alias = ?3, timestamp = ?4 SET features = ?2, alias = ?3, pow = ?4, timestamp = ?5
WHERE timestamp < ?4", WHERE timestamp < ?5",
)?; )?;
stmt.bind((1, node))?; stmt.bind((1, node))?;
stmt.bind((2, features))?; stmt.bind((2, features))?;
stmt.bind((3, alias))?; stmt.bind((3, alias))?;
stmt.bind((4, timestamp as i64))?; stmt.bind((4, pow as i64))?;
stmt.bind((5, timestamp as i64))?;
stmt.next()?; stmt.next()?;
for addr in addrs { for addr in addrs {
@ -275,6 +279,7 @@ pub trait Store {
node: &NodeId, node: &NodeId,
features: node::Features, features: node::Features,
alias: &str, alias: &str,
pow: u32,
timestamp: Timestamp, timestamp: Timestamp,
addrs: impl IntoIterator<Item = KnownAddress>, addrs: impl IntoIterator<Item = KnownAddress>,
) -> Result<bool, Error>; ) -> Result<bool, Error>;
@ -405,13 +410,14 @@ mod test {
last_attempt: None, last_attempt: None,
}; };
let inserted = cache let inserted = cache
.insert(&alice, features, "alice", timestamp, [ka.clone()]) .insert(&alice, features, "alice", 16, timestamp, [ka.clone()])
.unwrap(); .unwrap();
assert!(inserted); assert!(inserted);
let node = cache.get(&alice).unwrap().unwrap(); let node = cache.get(&alice).unwrap().unwrap();
assert_eq!(node.features, features); assert_eq!(node.features, features);
assert_eq!(node.pow, 16);
assert_eq!(node.timestamp, timestamp); assert_eq!(node.timestamp, timestamp);
assert_eq!(node.alias.as_str(), "alice"); assert_eq!(node.alias.as_str(), "alice");
assert_eq!(node.addrs, vec![ka]); assert_eq!(node.addrs, vec![ka]);
@ -431,12 +437,12 @@ mod test {
last_attempt: None, last_attempt: None,
}; };
let inserted = cache let inserted = cache
.insert(&alice, features, "alice", timestamp, [ka.clone()]) .insert(&alice, features, "alice", 0, timestamp, [ka.clone()])
.unwrap(); .unwrap();
assert!(inserted); assert!(inserted);
let inserted = cache let inserted = cache
.insert(&alice, features, "alice", timestamp, [ka]) .insert(&alice, features, "alice", 0, timestamp, [ka])
.unwrap(); .unwrap();
assert!(!inserted); assert!(!inserted);
@ -457,31 +463,39 @@ mod test {
}; };
let updated = cache let updated = cache
.insert(&alice, features, "alice", timestamp, [ka.clone()]) .insert(&alice, features, "alice", 0, timestamp, [ka.clone()])
.unwrap(); .unwrap();
assert!(updated); assert!(updated);
let updated = cache let updated = cache
.insert(&alice, features, "~alice~", timestamp, []) .insert(&alice, features, "~alice~", 0, timestamp, [])
.unwrap(); .unwrap();
assert!(!updated, "Can't update using the same timestamp"); assert!(!updated, "Can't update using the same timestamp");
let updated = cache let updated = cache
.insert(&alice, features, "~alice~", timestamp - 1, []) .insert(&alice, features, "~alice~", 0, timestamp - 1, [])
.unwrap(); .unwrap();
assert!(!updated, "Can't update using a smaller timestamp"); assert!(!updated, "Can't update using a smaller timestamp");
let node = cache.get(&alice).unwrap().unwrap(); let node = cache.get(&alice).unwrap().unwrap();
assert_eq!(node.alias, "alice"); assert_eq!(node.alias, "alice");
assert_eq!(node.timestamp, timestamp); assert_eq!(node.timestamp, timestamp);
assert_eq!(node.pow, 0);
let updated = cache let updated = cache
.insert(&alice, features, "~alice~", timestamp + 1, []) .insert(&alice, features, "~alice~", 0, timestamp + 1, [])
.unwrap(); .unwrap();
assert!(updated, "Can update with a larger timestamp"); assert!(updated, "Can update with a larger timestamp");
let updated = cache let updated = cache
.insert(&alice, node::Features::NONE, "~alice~", timestamp + 2, []) .insert(
&alice,
node::Features::NONE,
"~alice~",
1,
timestamp + 2,
[],
)
.unwrap(); .unwrap();
assert!(updated); assert!(updated);
@ -489,6 +503,7 @@ mod test {
assert_eq!(node.features, node::Features::NONE); assert_eq!(node.features, node::Features::NONE);
assert_eq!(node.alias, "~alice~"); assert_eq!(node.alias, "~alice~");
assert_eq!(node.timestamp, timestamp + 2); assert_eq!(node.timestamp, timestamp + 2);
assert_eq!(node.pow, 1);
assert_eq!(node.addrs, vec![ka]); assert_eq!(node.addrs, vec![ka]);
} }
@ -512,10 +527,10 @@ mod test {
last_attempt: None, last_attempt: None,
}; };
cache cache
.insert(&alice, features, "alice", timestamp, [ka.clone()]) .insert(&alice, features, "alice", 0, timestamp, [ka.clone()])
.unwrap(); .unwrap();
cache cache
.insert(&bob, features, "bob", timestamp, [ka]) .insert(&bob, features, "bob", 0, timestamp, [ka])
.unwrap(); .unwrap();
} }
assert_eq!(cache.len().unwrap(), 6); assert_eq!(cache.len().unwrap(), 6);
@ -550,7 +565,7 @@ mod test {
}; };
expected.push((id, ka.clone())); expected.push((id, ka.clone()));
cache cache
.insert(&id, features, "alias", timestamp, [ka]) .insert(&id, features, "alias", 0, timestamp, [ka])
.unwrap(); .unwrap();
} }

View File

@ -83,6 +83,8 @@ pub struct Node {
pub features: node::Features, pub features: node::Features,
/// Advertized addresses /// Advertized addresses
pub addrs: Vec<KnownAddress>, pub addrs: Vec<KnownAddress>,
/// Proof-of-work included in node announcement.
pub pow: u32,
/// When this data was published. /// When this data was published.
pub timestamp: Timestamp, pub timestamp: Timestamp,
} }