node: Add agent and version to node announcement
1. Adds a user-agent string to the node announcement. This lets us keep track of what software version everyone is running. Especially useful to see what percentage of the network has upgraded to a new release. For old nodes, this defaults to the string `/radicle/`. The user agent format is roughly based on BIP 0014. 2. Advertize protocol version in node announcement. By advertizing the protocol version, nodes can decide to only connect to peers with a compatible version. The default and current version is `1`. We bundle these two changes as they both modify the node announcement and node database. 3. Swap the order of the fields in announcements to make the message extensible.
This commit is contained in:
parent
5c0d1b10e0
commit
76edc0c523
|
|
@ -8,6 +8,7 @@ use radicle::node::address::Store as _;
|
||||||
use radicle::node::config::seeds::{RADICLE_COMMUNITY_NODE, RADICLE_TEAM_NODE};
|
use radicle::node::config::seeds::{RADICLE_COMMUNITY_NODE, RADICLE_TEAM_NODE};
|
||||||
use radicle::node::routing::Store as _;
|
use radicle::node::routing::Store as _;
|
||||||
use radicle::node::Handle as _;
|
use radicle::node::Handle as _;
|
||||||
|
use radicle::node::UserAgent;
|
||||||
use radicle::node::{Address, Alias, DEFAULT_TIMEOUT};
|
use radicle::node::{Address, Alias, DEFAULT_TIMEOUT};
|
||||||
use radicle::prelude::{NodeId, RepoId};
|
use radicle::prelude::{NodeId, RepoId};
|
||||||
use radicle::profile;
|
use radicle::profile;
|
||||||
|
|
@ -21,6 +22,7 @@ use radicle_node::service::Event;
|
||||||
use radicle_node::test::environment::{Config, Environment, Node};
|
use radicle_node::test::environment::{Config, Environment, Node};
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use radicle_node::test::logger;
|
use radicle_node::test::logger;
|
||||||
|
use radicle_node::PROTOCOL_VERSION;
|
||||||
|
|
||||||
/// Seed used in tests.
|
/// Seed used in tests.
|
||||||
const RAD_SEED: &str = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
|
const RAD_SEED: &str = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
|
||||||
|
|
@ -1317,9 +1319,11 @@ fn rad_clone_partial_fail() {
|
||||||
.addresses_mut()
|
.addresses_mut()
|
||||||
.insert(
|
.insert(
|
||||||
&carol,
|
&carol,
|
||||||
|
PROTOCOL_VERSION,
|
||||||
node::Features::SEED,
|
node::Features::SEED,
|
||||||
Alias::new("carol"),
|
Alias::new("carol"),
|
||||||
0,
|
0,
|
||||||
|
&UserAgent::default(),
|
||||||
localtime::LocalTime::now().into(),
|
localtime::LocalTime::now().into(),
|
||||||
[node::KnownAddress::new(
|
[node::KnownAddress::new(
|
||||||
// Eve will fail to connect to this address.
|
// Eve will fail to connect to this address.
|
||||||
|
|
@ -1363,6 +1367,7 @@ fn rad_clone_connect() {
|
||||||
let bob = environment.node(Config::test(Alias::new("bob")));
|
let bob = environment.node(Config::test(Alias::new("bob")));
|
||||||
let mut eve = environment.node(Config::test(Alias::new("eve")));
|
let mut eve = environment.node(Config::test(Alias::new("eve")));
|
||||||
let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
|
let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
|
||||||
|
let ua = UserAgent::default();
|
||||||
let now = localtime::LocalTime::now().into();
|
let now = localtime::LocalTime::now().into();
|
||||||
|
|
||||||
fixtures::repository(working.join("acme"));
|
fixtures::repository(working.join("acme"));
|
||||||
|
|
@ -1383,9 +1388,11 @@ fn rad_clone_connect() {
|
||||||
.addresses_mut()
|
.addresses_mut()
|
||||||
.insert(
|
.insert(
|
||||||
&alice.id,
|
&alice.id,
|
||||||
|
PROTOCOL_VERSION,
|
||||||
node::Features::SEED,
|
node::Features::SEED,
|
||||||
Alias::new("alice"),
|
Alias::new("alice"),
|
||||||
0,
|
0,
|
||||||
|
&ua,
|
||||||
now,
|
now,
|
||||||
[node::KnownAddress::new(
|
[node::KnownAddress::new(
|
||||||
node::Address::from(alice.addr),
|
node::Address::from(alice.addr),
|
||||||
|
|
@ -1397,9 +1404,11 @@ fn rad_clone_connect() {
|
||||||
.addresses_mut()
|
.addresses_mut()
|
||||||
.insert(
|
.insert(
|
||||||
&bob.id,
|
&bob.id,
|
||||||
|
PROTOCOL_VERSION,
|
||||||
node::Features::SEED,
|
node::Features::SEED,
|
||||||
Alias::new("bob"),
|
Alias::new("bob"),
|
||||||
0,
|
0,
|
||||||
|
&ua,
|
||||||
now,
|
now,
|
||||||
[node::KnownAddress::new(
|
[node::KnownAddress::new(
|
||||||
node::Address::from(bob.addr),
|
node::Address::from(bob.addr),
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,23 @@ pub mod tests;
|
||||||
pub mod wire;
|
pub mod wire;
|
||||||
pub mod worker;
|
pub mod worker;
|
||||||
|
|
||||||
|
use radicle::version::Version;
|
||||||
|
|
||||||
pub use localtime::{LocalDuration, LocalTime};
|
pub use localtime::{LocalDuration, LocalTime};
|
||||||
pub use netservices::Direction as Link;
|
pub use netservices::Direction as Link;
|
||||||
|
pub use radicle::node::PROTOCOL_VERSION;
|
||||||
pub use radicle::prelude::Timestamp;
|
pub use radicle::prelude::Timestamp;
|
||||||
pub use radicle::{collections, crypto, git, identity, node, profile, rad, storage};
|
pub use radicle::{collections, crypto, git, identity, node, profile, rad, storage};
|
||||||
pub use runtime::Runtime;
|
pub use runtime::Runtime;
|
||||||
|
|
||||||
|
/// Node version.
|
||||||
|
pub const VERSION: Version = Version {
|
||||||
|
name: env!("CARGO_PKG_NAME"),
|
||||||
|
commit: env!("GIT_HEAD"),
|
||||||
|
version: env!("RADICLE_VERSION"),
|
||||||
|
timestamp: env!("GIT_COMMIT_TIME"),
|
||||||
|
};
|
||||||
|
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use crate::bounded::BoundedVec;
|
pub use crate::bounded::BoundedVec;
|
||||||
pub use crate::crypto::{PublicKey, Signature, Signer};
|
pub use crate::crypto::{PublicKey, Signature, Signer};
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,10 @@ use crossbeam_channel as chan;
|
||||||
use radicle::logger;
|
use radicle::logger;
|
||||||
use radicle::prelude::Signer;
|
use radicle::prelude::Signer;
|
||||||
use radicle::profile;
|
use radicle::profile;
|
||||||
use radicle::version::Version;
|
|
||||||
use radicle_node::crypto::ssh::keystore::{Keystore, MemorySigner};
|
use radicle_node::crypto::ssh::keystore::{Keystore, MemorySigner};
|
||||||
use radicle_node::Runtime;
|
use radicle_node::{Runtime, VERSION};
|
||||||
use radicle_signals as signals;
|
use radicle_signals as signals;
|
||||||
|
|
||||||
pub const VERSION: Version = Version {
|
|
||||||
name: env!("CARGO_PKG_NAME"),
|
|
||||||
commit: env!("GIT_HEAD"),
|
|
||||||
version: env!("RADICLE_VERSION"),
|
|
||||||
timestamp: env!("GIT_COMMIT_TIME"),
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const HELP_MSG: &str = r#"
|
pub const HELP_MSG: &str = r#"
|
||||||
Usage
|
Usage
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ use radicle::node::address;
|
||||||
use radicle::node::address::Store as _;
|
use radicle::node::address::Store as _;
|
||||||
use radicle::node::notifications;
|
use radicle::node::notifications;
|
||||||
use radicle::node::Handle as _;
|
use radicle::node::Handle as _;
|
||||||
|
use radicle::node::UserAgent;
|
||||||
use radicle::profile::Home;
|
use radicle::profile::Home;
|
||||||
use radicle::{cob, git, storage, Storage};
|
use radicle::{cob, git, storage, Storage};
|
||||||
|
|
||||||
|
|
@ -127,7 +128,6 @@ impl Runtime {
|
||||||
for (key, _) in &config.extra {
|
for (key, _) in &config.extra {
|
||||||
log::warn!(target: "node", "Unused or deprecated configuration attribute {:?}", key);
|
log::warn!(target: "node", "Unused or deprecated configuration attribute {:?}", key);
|
||||||
}
|
}
|
||||||
log::info!(target: "node", "Opening node database..");
|
|
||||||
|
|
||||||
log::info!(target: "node", "Opening policy database..");
|
log::info!(target: "node", "Opening policy database..");
|
||||||
let policies = home.policies_mut()?;
|
let policies = home.policies_mut()?;
|
||||||
|
|
@ -174,6 +174,7 @@ impl Runtime {
|
||||||
.expect("Runtime::init: unable to solve proof-of-work puzzle")
|
.expect("Runtime::init: unable to solve proof-of-work puzzle")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
log::info!(target: "node", "Opening node database..");
|
||||||
let db = home
|
let db = home
|
||||||
.database_mut()?
|
.database_mut()?
|
||||||
.journal_mode(node::db::JournalMode::default())?
|
.journal_mode(node::db::JournalMode::default())?
|
||||||
|
|
@ -181,6 +182,7 @@ impl Runtime {
|
||||||
&id,
|
&id,
|
||||||
announcement.features,
|
announcement.features,
|
||||||
announcement.alias.clone(),
|
announcement.alias.clone(),
|
||||||
|
&announcement.agent,
|
||||||
announcement.timestamp,
|
announcement.timestamp,
|
||||||
announcement.addresses.iter(),
|
announcement.addresses.iter(),
|
||||||
)?;
|
)?;
|
||||||
|
|
@ -189,14 +191,16 @@ impl Runtime {
|
||||||
if config.connect.is_empty() && stores.addresses().is_empty()? {
|
if config.connect.is_empty() && stores.addresses().is_empty()? {
|
||||||
log::info!(target: "node", "Address book is empty. Adding bootstrap nodes..");
|
log::info!(target: "node", "Address book is empty. Adding bootstrap nodes..");
|
||||||
|
|
||||||
for (alias, addr) in config.network.bootstrap() {
|
for (alias, version, addr) in config.network.bootstrap() {
|
||||||
let (id, addr) = addr.into();
|
let (id, addr) = addr.into();
|
||||||
|
|
||||||
stores.addresses_mut().insert(
|
stores.addresses_mut().insert(
|
||||||
&id,
|
&id,
|
||||||
|
version,
|
||||||
radicle::node::Features::SEED,
|
radicle::node::Features::SEED,
|
||||||
alias,
|
alias,
|
||||||
0,
|
0,
|
||||||
|
&UserAgent::default(),
|
||||||
clock.into(),
|
clock.into(),
|
||||||
[node::KnownAddress::new(addr, address::Source::Bootstrap)],
|
[node::KnownAddress::new(addr, address::Source::Bootstrap)],
|
||||||
)?;
|
)?;
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ use radicle::storage::refs::SIGREFS_BRANCH;
|
||||||
use radicle::storage::RepositoryError;
|
use radicle::storage::RepositoryError;
|
||||||
use radicle_fetch::policy::SeedingPolicy;
|
use radicle_fetch::policy::SeedingPolicy;
|
||||||
|
|
||||||
use crate::crypto;
|
|
||||||
use crate::crypto::{Signer, Verified};
|
use crate::crypto::{Signer, Verified};
|
||||||
use crate::identity::{Doc, RepoId};
|
use crate::identity::{Doc, RepoId};
|
||||||
use crate::node::routing;
|
use crate::node::routing;
|
||||||
|
|
@ -56,6 +55,7 @@ use crate::storage::{refs::RefsAt, Namespaces, ReadStorage};
|
||||||
use crate::worker::fetch;
|
use crate::worker::fetch;
|
||||||
use crate::worker::FetchError;
|
use crate::worker::FetchError;
|
||||||
use crate::Link;
|
use crate::Link;
|
||||||
|
use crate::{crypto, PROTOCOL_VERSION};
|
||||||
|
|
||||||
pub use crate::node::events::{Event, Events};
|
pub use crate::node::events::{Event, Events};
|
||||||
pub use crate::node::{config::Network, Config, NodeId};
|
pub use crate::node::{config::Network, Config, NodeId};
|
||||||
|
|
@ -1693,9 +1693,11 @@ where
|
||||||
|
|
||||||
match self.db.addresses_mut().insert(
|
match self.db.addresses_mut().insert(
|
||||||
announcer,
|
announcer,
|
||||||
*features,
|
ann.version,
|
||||||
|
ann.features,
|
||||||
ann.alias.clone(),
|
ann.alias.clone(),
|
||||||
ann.work(),
|
ann.work(),
|
||||||
|
&ann.agent,
|
||||||
timestamp,
|
timestamp,
|
||||||
addresses
|
addresses
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -2380,6 +2382,7 @@ where
|
||||||
// Nb. we don't want to connect to any peers that already have a session with us,
|
// Nb. we don't want to connect to any peers that already have a session with us,
|
||||||
// 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.
|
||||||
let mut peers = entries
|
let mut peers = entries
|
||||||
|
.filter(|entry| entry.version == PROTOCOL_VERSION)
|
||||||
.filter(|entry| !entry.address.banned)
|
.filter(|entry| !entry.address.banned)
|
||||||
.filter(|entry| !entry.penalty.is_connect_threshold_reached())
|
.filter(|entry| !entry.penalty.is_connect_threshold_reached())
|
||||||
.filter(|entry| !self.sessions.contains_key(&entry.node))
|
.filter(|entry| !self.sessions.contains_key(&entry.node))
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,20 @@
|
||||||
pub mod store;
|
pub mod store;
|
||||||
|
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::{PROTOCOL_VERSION, VERSION};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use radicle::node::UserAgent;
|
||||||
|
|
||||||
pub use store::{AnnouncementId, Error, RelayStatus, Store};
|
pub use store::{AnnouncementId, Error, RelayStatus, Store};
|
||||||
|
|
||||||
|
/// This node's user agent string.
|
||||||
|
pub static USER_AGENT: Lazy<UserAgent> = Lazy::new(|| {
|
||||||
|
FromStr::from_str(format!("/radicle:{}/", VERSION.version).as_str())
|
||||||
|
.expect("user agent is valid")
|
||||||
|
});
|
||||||
|
|
||||||
pub fn node(config: &Config, timestamp: Timestamp) -> NodeAnnouncement {
|
pub fn node(config: &Config, timestamp: Timestamp) -> NodeAnnouncement {
|
||||||
let features = config.features();
|
let features = config.features();
|
||||||
let alias = config.alias.clone();
|
let alias = config.alias.clone();
|
||||||
|
|
@ -12,13 +23,17 @@ pub fn node(config: &Config, timestamp: Timestamp) -> NodeAnnouncement {
|
||||||
.clone()
|
.clone()
|
||||||
.try_into()
|
.try_into()
|
||||||
.expect("external addresses are within the limit");
|
.expect("external addresses are within the limit");
|
||||||
|
let agent = USER_AGENT.clone();
|
||||||
|
let version = PROTOCOL_VERSION;
|
||||||
|
|
||||||
NodeAnnouncement {
|
NodeAnnouncement {
|
||||||
features,
|
features,
|
||||||
|
version,
|
||||||
timestamp,
|
timestamp,
|
||||||
alias,
|
alias,
|
||||||
addresses,
|
addresses,
|
||||||
nonce: 0,
|
nonce: 0,
|
||||||
|
agent,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use radicle::storage::refs::RefsAt;
|
||||||
use crate::crypto;
|
use crate::crypto;
|
||||||
use crate::identity::RepoId;
|
use crate::identity::RepoId;
|
||||||
use crate::node;
|
use crate::node;
|
||||||
use crate::node::{Address, Alias};
|
use crate::node::{Address, Alias, UserAgent};
|
||||||
use crate::prelude::BoundedVec;
|
use crate::prelude::BoundedVec;
|
||||||
use crate::service::filter::Filter;
|
use crate::service::filter::Filter;
|
||||||
use crate::service::{Link, NodeId, Timestamp};
|
use crate::service::{Link, NodeId, Timestamp};
|
||||||
|
|
@ -54,6 +54,8 @@ impl Subscribe {
|
||||||
/// Node announcing itself to the network.
|
/// Node announcing itself to the network.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct NodeAnnouncement {
|
pub struct NodeAnnouncement {
|
||||||
|
/// Supported protocol version.
|
||||||
|
pub version: u8,
|
||||||
/// Advertized features.
|
/// Advertized features.
|
||||||
pub features: node::Features,
|
pub features: node::Features,
|
||||||
/// Monotonic timestamp.
|
/// Monotonic timestamp.
|
||||||
|
|
@ -64,6 +66,8 @@ pub struct NodeAnnouncement {
|
||||||
pub addresses: BoundedVec<Address, ADDRESS_LIMIT>,
|
pub addresses: BoundedVec<Address, ADDRESS_LIMIT>,
|
||||||
/// Nonce used for announcement proof-of-work.
|
/// Nonce used for announcement proof-of-work.
|
||||||
pub nonce: u64,
|
pub nonce: u64,
|
||||||
|
/// User-agent string.
|
||||||
|
pub agent: UserAgent,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NodeAnnouncement {
|
impl NodeAnnouncement {
|
||||||
|
|
@ -125,11 +129,13 @@ impl wire::Encode for NodeAnnouncement {
|
||||||
fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
|
fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
|
||||||
let mut n = 0;
|
let mut n = 0;
|
||||||
|
|
||||||
|
n += self.version.encode(writer)?;
|
||||||
n += self.features.encode(writer)?;
|
n += self.features.encode(writer)?;
|
||||||
n += self.timestamp.encode(writer)?;
|
n += self.timestamp.encode(writer)?;
|
||||||
n += self.alias.encode(writer)?;
|
n += self.alias.encode(writer)?;
|
||||||
n += self.addresses.encode(writer)?;
|
n += self.addresses.encode(writer)?;
|
||||||
n += self.nonce.encode(writer)?;
|
n += self.nonce.encode(writer)?;
|
||||||
|
n += self.agent.encode(writer)?;
|
||||||
|
|
||||||
Ok(n)
|
Ok(n)
|
||||||
}
|
}
|
||||||
|
|
@ -137,18 +143,26 @@ impl wire::Encode for NodeAnnouncement {
|
||||||
|
|
||||||
impl wire::Decode for NodeAnnouncement {
|
impl wire::Decode for NodeAnnouncement {
|
||||||
fn decode<R: std::io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
|
fn decode<R: std::io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
|
||||||
|
let version = u8::decode(reader)?;
|
||||||
let features = node::Features::decode(reader)?;
|
let features = node::Features::decode(reader)?;
|
||||||
let timestamp = Timestamp::decode(reader)?;
|
let timestamp = Timestamp::decode(reader)?;
|
||||||
let alias = wire::Decode::decode(reader)?;
|
let alias = wire::Decode::decode(reader)?;
|
||||||
let addresses = BoundedVec::<Address, ADDRESS_LIMIT>::decode(reader)?;
|
let addresses = BoundedVec::<Address, ADDRESS_LIMIT>::decode(reader)?;
|
||||||
let nonce = u64::decode(reader)?;
|
let nonce = u64::decode(reader)?;
|
||||||
|
let agent = match UserAgent::decode(reader) {
|
||||||
|
Ok(ua) => ua,
|
||||||
|
Err(e) if e.is_eof() => UserAgent::default(),
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
version,
|
||||||
features,
|
features,
|
||||||
timestamp,
|
timestamp,
|
||||||
alias,
|
alias,
|
||||||
addresses,
|
addresses,
|
||||||
nonce,
|
nonce,
|
||||||
|
agent,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -330,10 +344,10 @@ impl fmt::Debug for AnnouncementMessage {
|
||||||
pub struct Announcement {
|
pub struct Announcement {
|
||||||
/// Node identifier.
|
/// Node identifier.
|
||||||
pub node: NodeId,
|
pub node: NodeId,
|
||||||
/// Unsigned node announcement.
|
|
||||||
pub message: AnnouncementMessage,
|
|
||||||
/// Signature over the announcement.
|
/// Signature over the announcement.
|
||||||
pub signature: crypto::Signature,
|
pub signature: crypto::Signature,
|
||||||
|
/// Unsigned node announcement.
|
||||||
|
pub message: AnnouncementMessage,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Announcement {
|
impl Announcement {
|
||||||
|
|
@ -576,16 +590,18 @@ impl ZeroBytes {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[allow(clippy::unwrap_used)]
|
#[allow(clippy::unwrap_used)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use std::str::FromStr;
|
||||||
use crate::prelude::*;
|
|
||||||
use crate::wire::Encode;
|
|
||||||
|
|
||||||
use crate::crypto::test::signer::MockSigner;
|
|
||||||
use crate::test::arbitrary;
|
|
||||||
use fastrand;
|
use fastrand;
|
||||||
use qcheck_macros::quickcheck;
|
use qcheck_macros::quickcheck;
|
||||||
use radicle::git::raw;
|
use radicle::git::raw;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::crypto::test::signer::MockSigner;
|
||||||
|
use crate::prelude::*;
|
||||||
|
use crate::test::arbitrary;
|
||||||
|
use crate::wire::Encode;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ref_remote_limit() {
|
fn test_ref_remote_limit() {
|
||||||
let mut refs = BoundedVec::<_, REF_REMOTE_LIMIT>::new();
|
let mut refs = BoundedVec::<_, REF_REMOTE_LIMIT>::new();
|
||||||
|
|
@ -672,16 +688,18 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_node_announcement_validate() {
|
fn test_node_announcement_validate() {
|
||||||
let ann = NodeAnnouncement {
|
let ann = NodeAnnouncement {
|
||||||
|
version: 1,
|
||||||
features: node::Features::SEED,
|
features: node::Features::SEED,
|
||||||
timestamp: Timestamp::try_from(42491841u64).unwrap(),
|
timestamp: Timestamp::try_from(42491841u64).unwrap(),
|
||||||
alias: Alias::new("alice"),
|
alias: Alias::new("alice"),
|
||||||
addresses: BoundedVec::new(),
|
addresses: BoundedVec::new(),
|
||||||
nonce: 0,
|
nonce: 0,
|
||||||
|
agent: UserAgent::from_str("/heartwood:1.0.0/").unwrap(),
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(ann.work(), 0);
|
assert_eq!(ann.work(), 1);
|
||||||
assert_eq!(ann.clone().solve(1).unwrap().work(), 4);
|
assert_eq!(ann.clone().solve(1).unwrap().work(), 1);
|
||||||
assert_eq!(ann.clone().solve(8).unwrap().work(), 9);
|
assert_eq!(ann.clone().solve(8).unwrap().work(), 10);
|
||||||
assert_eq!(ann.solve(14).unwrap().work(), 14);
|
assert_eq!(ann.solve(14).unwrap().work(), 14);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use std::collections::HashSet;
|
||||||
|
|
||||||
use bloomy::BloomFilter;
|
use bloomy::BloomFilter;
|
||||||
use qcheck::Arbitrary;
|
use qcheck::Arbitrary;
|
||||||
|
use radicle::node::UserAgent;
|
||||||
|
|
||||||
use crate::crypto;
|
use crate::crypto;
|
||||||
use crate::identity::DocAt;
|
use crate::identity::DocAt;
|
||||||
|
|
@ -80,11 +81,13 @@ impl Arbitrary for Message {
|
||||||
.into(),
|
.into(),
|
||||||
MessageType::NodeAnnouncement => {
|
MessageType::NodeAnnouncement => {
|
||||||
let message = NodeAnnouncement {
|
let message = NodeAnnouncement {
|
||||||
|
version: u8::arbitrary(g),
|
||||||
features: u64::arbitrary(g).into(),
|
features: u64::arbitrary(g).into(),
|
||||||
timestamp: Timestamp::arbitrary(g),
|
timestamp: Timestamp::arbitrary(g),
|
||||||
alias: Alias::arbitrary(g),
|
alias: Alias::arbitrary(g),
|
||||||
addresses: Arbitrary::arbitrary(g),
|
addresses: Arbitrary::arbitrary(g),
|
||||||
nonce: u64::arbitrary(g),
|
nonce: u64::arbitrary(g),
|
||||||
|
agent: UserAgent::arbitrary(g),
|
||||||
}
|
}
|
||||||
.into();
|
.into();
|
||||||
let bytes: [u8; 64] = Arbitrary::arbitrary(g);
|
let bytes: [u8; 64] = Arbitrary::arbitrary(g);
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,7 @@ use radicle::identity::{RepoId, Visibility};
|
||||||
use radicle::node::config::ConnectAddress;
|
use radicle::node::config::ConnectAddress;
|
||||||
use radicle::node::policy::store as policy;
|
use radicle::node::policy::store as policy;
|
||||||
use radicle::node::seed::Store as _;
|
use radicle::node::seed::Store as _;
|
||||||
use radicle::node::Database;
|
use radicle::node::{Alias, Database, UserAgent, POLICIES_DB_FILE};
|
||||||
use radicle::node::{Alias, POLICIES_DB_FILE};
|
|
||||||
use radicle::node::{ConnectOptions, Handle as _};
|
use radicle::node::{ConnectOptions, Handle as _};
|
||||||
use radicle::profile;
|
use radicle::profile;
|
||||||
use radicle::profile::{env, Home, Profile};
|
use radicle::profile::{env, Home, Profile};
|
||||||
|
|
@ -135,6 +134,7 @@ impl Environment {
|
||||||
&public_key,
|
&public_key,
|
||||||
config.node.features(),
|
config.node.features(),
|
||||||
Alias::new(alias),
|
Alias::new(alias),
|
||||||
|
&UserAgent::default(),
|
||||||
now.into(),
|
now.into(),
|
||||||
config.node.external_addresses.iter(),
|
config.node.external_addresses.iter(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use radicle::crypto::test::signer::MockSigner;
|
use radicle::crypto::test::signer::MockSigner;
|
||||||
use radicle::node;
|
use radicle::node;
|
||||||
|
use radicle::node::UserAgent;
|
||||||
use radicle::test::fixtures::gen;
|
use radicle::test::fixtures::gen;
|
||||||
|
|
||||||
use crate::test::arbitrary;
|
use crate::test::arbitrary;
|
||||||
use crate::{
|
use crate::{
|
||||||
prelude::{LocalDuration, LocalTime, Message},
|
prelude::{LocalDuration, LocalTime, Message},
|
||||||
service::message::{InventoryAnnouncement, NodeAnnouncement},
|
service::message::{InventoryAnnouncement, NodeAnnouncement},
|
||||||
|
PROTOCOL_VERSION,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn messages(count: usize, now: LocalTime, delta: LocalDuration) -> Vec<Message> {
|
pub fn messages(count: usize, now: LocalTime, delta: LocalDuration) -> Vec<Message> {
|
||||||
|
|
@ -28,11 +32,13 @@ pub fn messages(count: usize, now: LocalTime, delta: LocalDuration) -> Vec<Messa
|
||||||
|
|
||||||
msgs.push(Message::node(
|
msgs.push(Message::node(
|
||||||
NodeAnnouncement {
|
NodeAnnouncement {
|
||||||
|
version: PROTOCOL_VERSION,
|
||||||
features: node::Features::SEED,
|
features: node::Features::SEED,
|
||||||
timestamp: time.into(),
|
timestamp: time.into(),
|
||||||
alias: node::Alias::new(gen::string(5)),
|
alias: node::Alias::new(gen::string(5)),
|
||||||
addresses: None.into(),
|
addresses: None.into(),
|
||||||
nonce: 0,
|
nonce: 0,
|
||||||
|
agent: UserAgent::from_str("/radicle:test/").unwrap(),
|
||||||
}
|
}
|
||||||
.solve(0)
|
.solve(0)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ use log::*;
|
||||||
use radicle::identity::Visibility;
|
use radicle::identity::Visibility;
|
||||||
use radicle::node::address::Store as _;
|
use radicle::node::address::Store as _;
|
||||||
use radicle::node::Database;
|
use radicle::node::Database;
|
||||||
|
use radicle::node::UserAgent;
|
||||||
use radicle::node::{address, Alias, ConnectOptions};
|
use radicle::node::{address, Alias, ConnectOptions};
|
||||||
use radicle::rad;
|
use radicle::rad;
|
||||||
use radicle::storage::refs::{RefsAt, SignedRefsAt};
|
use radicle::storage::refs::{RefsAt, SignedRefsAt};
|
||||||
|
|
@ -33,8 +34,7 @@ use crate::storage::{RemoteId, WriteStorage};
|
||||||
use crate::test::storage::MockStorage;
|
use crate::test::storage::MockStorage;
|
||||||
use crate::test::{arbitrary, fixtures, simulator};
|
use crate::test::{arbitrary, fixtures, simulator};
|
||||||
use crate::wire::MessageType;
|
use crate::wire::MessageType;
|
||||||
use crate::Link;
|
use crate::{Link, LocalDuration, LocalTime, PROTOCOL_VERSION};
|
||||||
use crate::{LocalDuration, LocalTime};
|
|
||||||
|
|
||||||
/// Service instantiation used for testing.
|
/// Service instantiation used for testing.
|
||||||
pub type Service<S, G> = service::Service<Database, S, G>;
|
pub type Service<S, G> = service::Service<Database, S, G>;
|
||||||
|
|
@ -175,6 +175,7 @@ where
|
||||||
&id,
|
&id,
|
||||||
config.config.features(),
|
config.config.features(),
|
||||||
config.config.alias.clone(),
|
config.config.alias.clone(),
|
||||||
|
&UserAgent::default(),
|
||||||
config.local_time.into(),
|
config.local_time.into(),
|
||||||
config.config.external_addresses.iter(),
|
config.config.external_addresses.iter(),
|
||||||
)
|
)
|
||||||
|
|
@ -251,9 +252,11 @@ where
|
||||||
.addresses_mut()
|
.addresses_mut()
|
||||||
.insert(
|
.insert(
|
||||||
&peer.node_id(),
|
&peer.node_id(),
|
||||||
|
PROTOCOL_VERSION,
|
||||||
radicle::node::Features::default(),
|
radicle::node::Features::default(),
|
||||||
Alias::from_str(peer.name).unwrap(),
|
Alias::from_str(peer.name).unwrap(),
|
||||||
0,
|
0,
|
||||||
|
&UserAgent::default(),
|
||||||
timestamp,
|
timestamp,
|
||||||
Some(known_address),
|
Some(known_address),
|
||||||
)
|
)
|
||||||
|
|
@ -303,11 +306,13 @@ where
|
||||||
pub fn node_announcement(&self) -> Message {
|
pub fn node_announcement(&self) -> Message {
|
||||||
Message::node(
|
Message::node(
|
||||||
NodeAnnouncement {
|
NodeAnnouncement {
|
||||||
|
version: PROTOCOL_VERSION,
|
||||||
features: node::Features::SEED,
|
features: node::Features::SEED,
|
||||||
timestamp: self.timestamp(),
|
timestamp: self.timestamp(),
|
||||||
alias: Alias::from_str(self.name).unwrap(),
|
alias: Alias::from_str(self.name).unwrap(),
|
||||||
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,
|
||||||
|
agent: UserAgent::from_str("/radicle:test/").unwrap(),
|
||||||
}
|
}
|
||||||
.solve(0)
|
.solve(0)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ mod varint;
|
||||||
pub use frame::StreamId;
|
pub use frame::StreamId;
|
||||||
pub use message::{AddressType, MessageType};
|
pub use message::{AddressType, MessageType};
|
||||||
pub use protocol::{Control, Wire, WireReader, WireSession, WireWriter};
|
pub use protocol::{Control, Wire, WireReader, WireSession, WireWriter};
|
||||||
|
use radicle::node::UserAgent;
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
@ -54,6 +55,8 @@ pub enum Error {
|
||||||
InvalidRefName(#[from] fmt::Error),
|
InvalidRefName(#[from] fmt::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
InvalidAlias(#[from] node::AliasError),
|
InvalidAlias(#[from] node::AliasError),
|
||||||
|
#[error("invalid user agent string: {0:?}")]
|
||||||
|
InvalidUserAgent(String),
|
||||||
#[error("invalid control message with type `{0}`")]
|
#[error("invalid control message with type `{0}`")]
|
||||||
InvalidControlMessage(u8),
|
InvalidControlMessage(u8),
|
||||||
#[error("invalid protocol version header `{0:x?}`")]
|
#[error("invalid protocol version header `{0:x?}`")]
|
||||||
|
|
@ -62,8 +65,8 @@ pub enum Error {
|
||||||
InvalidOnionAddr(#[from] tor::OnionAddrDecodeError),
|
InvalidOnionAddr(#[from] tor::OnionAddrDecodeError),
|
||||||
#[error("invalid timestamp: {0}")]
|
#[error("invalid timestamp: {0}")]
|
||||||
InvalidTimestamp(u64),
|
InvalidTimestamp(u64),
|
||||||
#[error("unknown protocol version `{0}`")]
|
#[error("wrong protocol version `{0}`")]
|
||||||
UnknownProtocolVersion(u8),
|
WrongProtocolVersion(u8),
|
||||||
#[error("unknown address type `{0}`")]
|
#[error("unknown address type `{0}`")]
|
||||||
UnknownAddressType(u8),
|
UnknownAddressType(u8),
|
||||||
#[error("unknown message type `{0}`")]
|
#[error("unknown message type `{0}`")]
|
||||||
|
|
@ -251,6 +254,12 @@ impl Encode for cyphernet::addr::tor::OnionAddrV3 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Encode for UserAgent {
|
||||||
|
fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
|
||||||
|
self.as_ref().encode(writer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Encode for Alias {
|
impl Encode for Alias {
|
||||||
fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
|
fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
|
||||||
self.as_ref().encode(writer)
|
self.as_ref().encode(writer)
|
||||||
|
|
@ -321,6 +330,13 @@ impl Decode for git::RefString {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Decode for UserAgent {
|
||||||
|
fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
|
||||||
|
String::decode(reader)
|
||||||
|
.and_then(|s| UserAgent::from_str(&s).map_err(Error::InvalidUserAgent))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Decode for Alias {
|
impl Decode for Alias {
|
||||||
fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
|
fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
|
||||||
String::decode(reader).and_then(|s| Alias::from_str(&s).map_err(Error::from))
|
String::decode(reader).and_then(|s| Alias::from_str(&s).map_err(Error::from))
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,8 @@
|
||||||
#![warn(clippy::missing_docs_in_private_items)]
|
#![warn(clippy::missing_docs_in_private_items)]
|
||||||
use std::{fmt, io};
|
use std::{fmt, io};
|
||||||
|
|
||||||
use crate::{wire, wire::varint, wire::varint::VarInt, wire::Message, Link};
|
use crate::{wire, wire::varint, wire::varint::VarInt, wire::Message, Link, PROTOCOL_VERSION};
|
||||||
|
|
||||||
/// Protocol version.
|
|
||||||
pub const PROTOCOL_VERSION: u8 = 1;
|
|
||||||
/// Protocol version strings all start with the magic sequence `rad`, followed
|
/// Protocol version strings all start with the magic sequence `rad`, followed
|
||||||
/// by a version number.
|
/// by a version number.
|
||||||
pub const PROTOCOL_VERSION_STRING: Version = Version([b'r', b'a', b'd', PROTOCOL_VERSION]);
|
pub const PROTOCOL_VERSION_STRING: Version = Version([b'r', b'a', b'd', PROTOCOL_VERSION]);
|
||||||
|
|
@ -315,8 +313,8 @@ impl wire::Encode for Control {
|
||||||
impl wire::Decode for Frame {
|
impl wire::Decode for Frame {
|
||||||
fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
|
fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
|
||||||
let version = Version::decode(reader)?;
|
let version = Version::decode(reader)?;
|
||||||
if version.number() > PROTOCOL_VERSION {
|
if version.number() != PROTOCOL_VERSION {
|
||||||
return Err(wire::Error::UnknownProtocolVersion(version.number()));
|
return Err(wire::Error::WrongProtocolVersion(version.number()));
|
||||||
}
|
}
|
||||||
let stream = StreamId::decode(reader)?;
|
let stream = StreamId::decode(reader)?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -274,8 +274,8 @@ impl wire::Encode for Message {
|
||||||
signature,
|
signature,
|
||||||
}) => {
|
}) => {
|
||||||
n += node.encode(writer)?;
|
n += node.encode(writer)?;
|
||||||
n += message.encode(writer)?;
|
|
||||||
n += signature.encode(writer)?;
|
n += signature.encode(writer)?;
|
||||||
|
n += message.encode(writer)?;
|
||||||
}
|
}
|
||||||
Self::Info(info) => {
|
Self::Info(info) => {
|
||||||
n += info.encode(writer)?;
|
n += info.encode(writer)?;
|
||||||
|
|
@ -317,8 +317,8 @@ impl wire::Decode for Message {
|
||||||
}
|
}
|
||||||
Ok(MessageType::NodeAnnouncement) => {
|
Ok(MessageType::NodeAnnouncement) => {
|
||||||
let node = NodeId::decode(reader)?;
|
let node = NodeId::decode(reader)?;
|
||||||
let message = NodeAnnouncement::decode(reader)?.into();
|
|
||||||
let signature = Signature::decode(reader)?;
|
let signature = Signature::decode(reader)?;
|
||||||
|
let message = NodeAnnouncement::decode(reader)?.into();
|
||||||
|
|
||||||
Ok(Announcement {
|
Ok(Announcement {
|
||||||
node,
|
node,
|
||||||
|
|
@ -329,8 +329,8 @@ impl wire::Decode for Message {
|
||||||
}
|
}
|
||||||
Ok(MessageType::InventoryAnnouncement) => {
|
Ok(MessageType::InventoryAnnouncement) => {
|
||||||
let node = NodeId::decode(reader)?;
|
let node = NodeId::decode(reader)?;
|
||||||
let message = InventoryAnnouncement::decode(reader)?.into();
|
|
||||||
let signature = Signature::decode(reader)?;
|
let signature = Signature::decode(reader)?;
|
||||||
|
let message = InventoryAnnouncement::decode(reader)?.into();
|
||||||
|
|
||||||
Ok(Announcement {
|
Ok(Announcement {
|
||||||
node,
|
node,
|
||||||
|
|
@ -341,8 +341,8 @@ impl wire::Decode for Message {
|
||||||
}
|
}
|
||||||
Ok(MessageType::RefsAnnouncement) => {
|
Ok(MessageType::RefsAnnouncement) => {
|
||||||
let node = NodeId::decode(reader)?;
|
let node = NodeId::decode(reader)?;
|
||||||
let message = RefsAnnouncement::decode(reader)?.into();
|
|
||||||
let signature = Signature::decode(reader)?;
|
let signature = Signature::decode(reader)?;
|
||||||
|
let message = RefsAnnouncement::decode(reader)?.into();
|
||||||
|
|
||||||
Ok(Announcement {
|
Ok(Announcement {
|
||||||
node,
|
node,
|
||||||
|
|
@ -458,6 +458,7 @@ impl wire::Decode for ZeroBytes {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use qcheck_macros::quickcheck;
|
use qcheck_macros::quickcheck;
|
||||||
|
use radicle::node::UserAgent;
|
||||||
use radicle::storage::refs::RefsAt;
|
use radicle::storage::refs::RefsAt;
|
||||||
use radicle_crypto::test::signer::MockSigner;
|
use radicle_crypto::test::signer::MockSigner;
|
||||||
|
|
||||||
|
|
@ -502,11 +503,13 @@ mod tests {
|
||||||
let addrs: [Address; ADDRESS_LIMIT] = arbitrary::gen(1);
|
let addrs: [Address; ADDRESS_LIMIT] = arbitrary::gen(1);
|
||||||
let alias = ['@'; radicle::node::MAX_ALIAS_LENGTH];
|
let alias = ['@'; radicle::node::MAX_ALIAS_LENGTH];
|
||||||
let ann = AnnouncementMessage::Node(NodeAnnouncement {
|
let ann = AnnouncementMessage::Node(NodeAnnouncement {
|
||||||
|
version: 1,
|
||||||
features: Default::default(),
|
features: Default::default(),
|
||||||
alias: radicle::node::Alias::new(String::from_iter(alias)),
|
alias: radicle::node::Alias::new(String::from_iter(alias)),
|
||||||
addresses: BoundedVec::collect_from(&mut addrs.into_iter()),
|
addresses: BoundedVec::collect_from(&mut addrs.into_iter()),
|
||||||
timestamp: arbitrary::gen(1),
|
timestamp: arbitrary::gen(1),
|
||||||
nonce: u64::MAX,
|
nonce: u64::MAX,
|
||||||
|
agent: UserAgent::default(),
|
||||||
});
|
});
|
||||||
let ann = ann.signed(&signer);
|
let ann = ann.signed(&signer);
|
||||||
let msg = Message::Announcement(ann);
|
let msg = Message::Announcement(ann);
|
||||||
|
|
@ -555,10 +558,10 @@ mod tests {
|
||||||
|
|
||||||
#[quickcheck]
|
#[quickcheck]
|
||||||
fn prop_message_encode_decode(message: Message) {
|
fn prop_message_encode_decode(message: Message) {
|
||||||
assert_eq!(
|
let encoded = &wire::serialize(&message);
|
||||||
wire::deserialize::<Message>(&wire::serialize(&message)).unwrap(),
|
let decoded = wire::deserialize::<Message>(encoded).unwrap();
|
||||||
message
|
|
||||||
);
|
assert_eq!(message, decoded);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ use crate::service;
|
||||||
use crate::service::io::Io;
|
use crate::service::io::Io;
|
||||||
use crate::service::FETCH_TIMEOUT;
|
use crate::service::FETCH_TIMEOUT;
|
||||||
use crate::service::{session, DisconnectReason, Metrics, Service};
|
use crate::service::{session, DisconnectReason, Metrics, Service};
|
||||||
use crate::wire;
|
|
||||||
use crate::wire::frame;
|
use crate::wire::frame;
|
||||||
use crate::wire::frame::{Frame, FrameData, StreamId};
|
use crate::wire::frame::{Frame, FrameData, StreamId};
|
||||||
use crate::wire::Encode;
|
use crate::wire::Encode;
|
||||||
|
|
@ -839,15 +838,6 @@ where
|
||||||
// Buffer is empty, or message isn't complete.
|
// Buffer is empty, or message isn't complete.
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(wire::Error::UnknownProtocolVersion(v)) => {
|
|
||||||
// It's ok for a peer to send frames from a newer protocol version
|
|
||||||
// that we don't understand. We just ignore them.
|
|
||||||
log::debug!(
|
|
||||||
target: "wire",
|
|
||||||
"Ignoring frame with newer protocol version ({v})",
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!(target: "wire", "Invalid gossip message from {nid}: {e}");
|
log::error!(target: "wire", "Invalid gossip message from {nid}: {e}");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,8 @@ pub use features::Features;
|
||||||
pub use seed::SyncedAt;
|
pub use seed::SyncedAt;
|
||||||
pub use timestamp::Timestamp;
|
pub use timestamp::Timestamp;
|
||||||
|
|
||||||
|
/// Peer-to-peer protocol version.
|
||||||
|
pub const PROTOCOL_VERSION: u8 = 1;
|
||||||
/// Default name for control socket file.
|
/// Default name for control socket file.
|
||||||
pub const DEFAULT_SOCKET_NAME: &str = "control.sock";
|
pub const DEFAULT_SOCKET_NAME: &str = "control.sock";
|
||||||
/// Default radicle protocol port.
|
/// Default radicle protocol port.
|
||||||
|
|
@ -217,6 +219,77 @@ impl PartialOrd for SyncStatus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Node user agent.
|
||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct UserAgent(String);
|
||||||
|
|
||||||
|
impl UserAgent {
|
||||||
|
/// Return a reference to the user agent string.
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
self.0.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for UserAgent {
|
||||||
|
fn default() -> Self {
|
||||||
|
UserAgent(String::from("/radicle/"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for UserAgent {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for UserAgent {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
let reserved = ['/', ':'];
|
||||||
|
|
||||||
|
if input.len() > 64 {
|
||||||
|
return Err(input.to_owned());
|
||||||
|
}
|
||||||
|
let Some(s) = input.strip_prefix('/') else {
|
||||||
|
return Err(input.to_owned());
|
||||||
|
};
|
||||||
|
let Some(s) = s.strip_suffix('/') else {
|
||||||
|
return Err(input.to_owned());
|
||||||
|
};
|
||||||
|
if s.is_empty() {
|
||||||
|
return Err(input.to_owned());
|
||||||
|
}
|
||||||
|
if s.split('/').all(|segment| {
|
||||||
|
if let Some((client, version)) = segment.split_once(':') {
|
||||||
|
if client.is_empty() || version.is_empty() {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
let client = client
|
||||||
|
.chars()
|
||||||
|
.all(|c| c.is_ascii_graphic() && !reserved.contains(&c));
|
||||||
|
let version = version
|
||||||
|
.chars()
|
||||||
|
.all(|c| c.is_ascii_graphic() || !reserved.contains(&c));
|
||||||
|
client && version
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Ok(Self(input.to_owned()))
|
||||||
|
} else {
|
||||||
|
Err(input.to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for UserAgent {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
self.0.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Node alias.
|
/// Node alias.
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(try_from = "String", into = "String")]
|
#[serde(try_from = "String", into = "String")]
|
||||||
|
|
@ -1282,6 +1355,27 @@ mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::assert_matches;
|
use crate::assert_matches;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_user_agent() {
|
||||||
|
assert!(UserAgent::from_str("/radicle:1.0.0/").is_ok());
|
||||||
|
assert!(UserAgent::from_str("/radicle:1.0.0/heartwood:0.9/").is_ok());
|
||||||
|
assert!(UserAgent::from_str("/radicle:1.0.0/heartwood:0.9/rust:1.77/").is_ok());
|
||||||
|
assert!(UserAgent::from_str("/radicle:1.0.0-rc.1/").is_ok());
|
||||||
|
assert!(UserAgent::from_str("/radicle:1.0.0-rc.1/").is_ok());
|
||||||
|
assert!(UserAgent::from_str("/radicle:@a.b.c/").is_ok());
|
||||||
|
assert!(UserAgent::from_str("/radicle/").is_ok());
|
||||||
|
assert!(UserAgent::from_str("/rad/icle/").is_ok());
|
||||||
|
assert!(UserAgent::from_str("/rad:ic/le/").is_ok());
|
||||||
|
|
||||||
|
assert!(UserAgent::from_str("/:/").is_err());
|
||||||
|
assert!(UserAgent::from_str("//").is_err());
|
||||||
|
assert!(UserAgent::from_str("").is_err());
|
||||||
|
assert!(UserAgent::from_str("radicle:1.0.0/").is_err());
|
||||||
|
assert!(UserAgent::from_str("/radicle:1.0.0").is_err());
|
||||||
|
assert!(UserAgent::from_str("/radi cle:1.0/").is_err());
|
||||||
|
assert!(UserAgent::from_str("/radi\ncle:1.0/").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_alias() {
|
fn test_alias() {
|
||||||
assert!(Alias::from_str("cloudhead").is_ok());
|
assert!(Alias::from_str("cloudhead").is_ok());
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use localtime::LocalTime;
|
||||||
use nonempty::NonEmpty;
|
use nonempty::NonEmpty;
|
||||||
|
|
||||||
use crate::collections::RandomMap;
|
use crate::collections::RandomMap;
|
||||||
use crate::node::{Address, Alias, Penalty};
|
use crate::node::{Address, Alias, Penalty, UserAgent};
|
||||||
use crate::prelude::Timestamp;
|
use crate::prelude::Timestamp;
|
||||||
use crate::{node, profile};
|
use crate::{node, profile};
|
||||||
|
|
||||||
|
|
@ -120,6 +120,8 @@ impl<K: hash::Hash + Eq, V> DerefMut for AddressBook<K, V> {
|
||||||
/// Node public data.
|
/// Node public data.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Node {
|
pub struct Node {
|
||||||
|
/// Protocol version.
|
||||||
|
pub version: u8,
|
||||||
/// Advertized alias.
|
/// Advertized alias.
|
||||||
pub alias: Alias,
|
pub alias: Alias,
|
||||||
/// Advertized features.
|
/// Advertized features.
|
||||||
|
|
@ -130,6 +132,8 @@ pub struct Node {
|
||||||
pub pow: u32,
|
pub pow: u32,
|
||||||
/// When this data was published.
|
/// When this data was published.
|
||||||
pub timestamp: Timestamp,
|
pub timestamp: Timestamp,
|
||||||
|
/// User agent string.
|
||||||
|
pub agent: UserAgent,
|
||||||
/// Node connection penalty.
|
/// Node connection penalty.
|
||||||
pub penalty: Penalty,
|
pub penalty: Penalty,
|
||||||
/// Whether the node is banned.
|
/// Whether the node is banned.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
use std::num::TryFromIntError;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use localtime::LocalTime;
|
use localtime::LocalTime;
|
||||||
|
|
@ -7,6 +8,7 @@ use thiserror::Error;
|
||||||
|
|
||||||
use crate::node;
|
use crate::node;
|
||||||
use crate::node::address::{AddressType, KnownAddress, Node, Source};
|
use crate::node::address::{AddressType, KnownAddress, Node, Source};
|
||||||
|
use crate::node::UserAgent;
|
||||||
use crate::node::{Address, Alias, AliasError, AliasStore, Database, NodeId, Penalty, Severity};
|
use crate::node::{Address, Alias, AliasError, AliasStore, Database, NodeId, Penalty, Severity};
|
||||||
use crate::prelude::Timestamp;
|
use crate::prelude::Timestamp;
|
||||||
use crate::sql::transaction;
|
use crate::sql::transaction;
|
||||||
|
|
@ -18,6 +20,8 @@ pub enum Error {
|
||||||
Internal(#[from] sql::Error),
|
Internal(#[from] sql::Error),
|
||||||
#[error("alias error: {0}")]
|
#[error("alias error: {0}")]
|
||||||
InvalidAlias(#[from] AliasError),
|
InvalidAlias(#[from] AliasError),
|
||||||
|
#[error("integer conversion error: {0}")]
|
||||||
|
TryFromInt(#[from] TryFromIntError),
|
||||||
/// No rows returned in query result.
|
/// No rows returned in query result.
|
||||||
#[error("no rows returned")]
|
#[error("no rows returned")]
|
||||||
NoRows,
|
NoRows,
|
||||||
|
|
@ -28,6 +32,8 @@ pub enum Error {
|
||||||
pub struct AddressEntry {
|
pub struct AddressEntry {
|
||||||
/// Node ID.
|
/// Node ID.
|
||||||
pub node: NodeId,
|
pub node: NodeId,
|
||||||
|
/// Node protocol version.
|
||||||
|
pub version: u8,
|
||||||
/// Node penalty.
|
/// Node penalty.
|
||||||
pub penalty: Penalty,
|
pub penalty: Penalty,
|
||||||
/// Node address.
|
/// Node address.
|
||||||
|
|
@ -48,9 +54,11 @@ pub trait Store {
|
||||||
fn insert(
|
fn insert(
|
||||||
&mut self,
|
&mut self,
|
||||||
node: &NodeId,
|
node: &NodeId,
|
||||||
|
version: u8,
|
||||||
features: node::Features,
|
features: node::Features,
|
||||||
alias: Alias,
|
alias: Alias,
|
||||||
pow: u32,
|
pow: u32,
|
||||||
|
agent: &UserAgent,
|
||||||
timestamp: Timestamp,
|
timestamp: Timestamp,
|
||||||
addrs: impl IntoIterator<Item = KnownAddress>,
|
addrs: impl IntoIterator<Item = KnownAddress>,
|
||||||
) -> Result<bool, Error>;
|
) -> Result<bool, Error>;
|
||||||
|
|
@ -89,24 +97,30 @@ pub trait Store {
|
||||||
impl Store for Database {
|
impl Store for Database {
|
||||||
fn get(&self, node: &NodeId) -> Result<Option<Node>, Error> {
|
fn get(&self, node: &NodeId) -> Result<Option<Node>, Error> {
|
||||||
let mut stmt = self.db.prepare(
|
let mut stmt = self.db.prepare(
|
||||||
"SELECT features, alias, pow, penalty, banned, timestamp FROM nodes WHERE id = ?",
|
"SELECT version, features, alias, pow, penalty, banned, agent, timestamp
|
||||||
|
FROM nodes
|
||||||
|
WHERE id = ?",
|
||||||
)?;
|
)?;
|
||||||
stmt.bind((1, node))?;
|
stmt.bind((1, node))?;
|
||||||
|
|
||||||
if let Some(Ok(row)) = stmt.into_iter().next() {
|
if let Some(Ok(row)) = stmt.into_iter().next() {
|
||||||
|
let version = row.read::<i64, _>("version").try_into()?;
|
||||||
let features = row.read::<node::Features, _>("features");
|
let features = row.read::<node::Features, _>("features");
|
||||||
let alias = Alias::from_str(row.read::<&str, _>("alias"))?;
|
let alias = Alias::from_str(row.read::<&str, _>("alias"))?;
|
||||||
let timestamp = row.read::<Timestamp, _>("timestamp");
|
let timestamp = row.read::<Timestamp, _>("timestamp");
|
||||||
let pow = row.read::<i64, _>("pow") as u32;
|
let pow = row.read::<i64, _>("pow") as u32;
|
||||||
|
let agent = row.read::<UserAgent, _>("agent");
|
||||||
let penalty = row.read::<i64, _>("penalty").min(u8::MAX as i64);
|
let penalty = row.read::<i64, _>("penalty").min(u8::MAX as i64);
|
||||||
let penalty = Penalty(penalty as u8);
|
let penalty = Penalty(penalty as u8);
|
||||||
let banned = row.read::<i64, _>("banned").is_positive();
|
let banned = row.read::<i64, _>("banned").is_positive();
|
||||||
let addrs = self.addresses_of(node)?;
|
let addrs = self.addresses_of(node)?;
|
||||||
|
|
||||||
Ok(Some(Node {
|
Ok(Some(Node {
|
||||||
|
version,
|
||||||
features,
|
features,
|
||||||
alias,
|
alias,
|
||||||
pow,
|
pow,
|
||||||
|
agent,
|
||||||
timestamp,
|
timestamp,
|
||||||
penalty,
|
penalty,
|
||||||
addrs,
|
addrs,
|
||||||
|
|
@ -207,26 +221,30 @@ impl Store for Database {
|
||||||
fn insert(
|
fn insert(
|
||||||
&mut self,
|
&mut self,
|
||||||
node: &NodeId,
|
node: &NodeId,
|
||||||
|
version: u8,
|
||||||
features: node::Features,
|
features: node::Features,
|
||||||
alias: Alias,
|
alias: Alias,
|
||||||
pow: u32,
|
pow: u32,
|
||||||
|
agent: &UserAgent,
|
||||||
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, pow, timestamp)
|
"INSERT INTO nodes (id, version, features, alias, pow, agent, timestamp)
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5)
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
|
||||||
ON CONFLICT DO UPDATE
|
ON CONFLICT DO UPDATE
|
||||||
SET features = ?2, alias = ?3, pow = ?4, timestamp = ?5
|
SET version = ?2, features = ?3, alias = ?4, pow = ?5, agent = ?6, timestamp = ?7
|
||||||
WHERE timestamp < ?5",
|
WHERE timestamp < ?7",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
stmt.bind((1, node))?;
|
stmt.bind((1, node))?;
|
||||||
stmt.bind((2, features))?;
|
stmt.bind((2, version as i64))?;
|
||||||
stmt.bind((3, sql::Value::String(alias.into())))?;
|
stmt.bind((3, features))?;
|
||||||
stmt.bind((4, pow as i64))?;
|
stmt.bind((4, sql::Value::String(alias.into())))?;
|
||||||
stmt.bind((5, ×tamp))?;
|
stmt.bind((5, pow as i64))?;
|
||||||
|
stmt.bind((6, agent.as_str()))?;
|
||||||
|
stmt.bind((7, ×tamp))?;
|
||||||
stmt.next()?;
|
stmt.next()?;
|
||||||
|
|
||||||
for addr in addrs {
|
for addr in addrs {
|
||||||
|
|
@ -261,7 +279,7 @@ impl Store for Database {
|
||||||
let mut stmt = self
|
let mut stmt = self
|
||||||
.db
|
.db
|
||||||
.prepare(
|
.prepare(
|
||||||
"SELECT a.node, a.type, a.value, a.source, a.last_success, a.last_attempt, a.banned, n.penalty
|
"SELECT a.node, a.type, a.value, a.source, a.last_success, a.last_attempt, a.banned, n.version, n.penalty
|
||||||
FROM addresses AS a
|
FROM addresses AS a
|
||||||
JOIN nodes AS n ON a.node = n.id
|
JOIN nodes AS n ON a.node = n.id
|
||||||
ORDER BY n.penalty ASC, n.id ASC",
|
ORDER BY n.penalty ASC, n.id ASC",
|
||||||
|
|
@ -278,12 +296,14 @@ impl Store for Database {
|
||||||
let last_attempt = row.read::<Option<i64>, _>("last_attempt");
|
let last_attempt = row.read::<Option<i64>, _>("last_attempt");
|
||||||
let last_success = last_success.map(|t| LocalTime::from_millis(t as u128));
|
let last_success = last_success.map(|t| LocalTime::from_millis(t as u128));
|
||||||
let last_attempt = last_attempt.map(|t| LocalTime::from_millis(t as u128));
|
let last_attempt = last_attempt.map(|t| LocalTime::from_millis(t as u128));
|
||||||
|
let version = row.read::<i64, _>("version").try_into()?;
|
||||||
let banned = row.read::<i64, _>("banned").is_positive();
|
let banned = row.read::<i64, _>("banned").is_positive();
|
||||||
let penalty = row.read::<i64, _>("penalty");
|
let penalty = row.read::<i64, _>("penalty");
|
||||||
let penalty = Penalty(penalty as u8); // Clamped at `u8::MAX`.
|
let penalty = Penalty(penalty as u8); // Clamped at `u8::MAX`.
|
||||||
|
|
||||||
entries.push(AddressEntry {
|
entries.push(AddressEntry {
|
||||||
node,
|
node,
|
||||||
|
version,
|
||||||
penalty,
|
penalty,
|
||||||
address: KnownAddress {
|
address: KnownAddress {
|
||||||
addr,
|
addr,
|
||||||
|
|
@ -510,15 +530,34 @@ mod test {
|
||||||
let mut cache = Database::memory().unwrap();
|
let mut cache = Database::memory().unwrap();
|
||||||
let features = node::Features::SEED;
|
let features = node::Features::SEED;
|
||||||
let timestamp = Timestamp::from(LocalTime::now());
|
let timestamp = Timestamp::from(LocalTime::now());
|
||||||
|
let ua = UserAgent::default();
|
||||||
|
|
||||||
cache
|
cache
|
||||||
.insert(&alice, features, Alias::new("alice"), 16, timestamp, [])
|
.insert(
|
||||||
|
&alice,
|
||||||
|
1,
|
||||||
|
features,
|
||||||
|
Alias::new("alice"),
|
||||||
|
16,
|
||||||
|
&ua,
|
||||||
|
timestamp,
|
||||||
|
[],
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let node = cache.get(&alice).unwrap().unwrap();
|
let node = cache.get(&alice).unwrap().unwrap();
|
||||||
assert_eq!(node.alias.as_ref(), "alice");
|
assert_eq!(node.alias.as_ref(), "alice");
|
||||||
|
|
||||||
cache
|
cache
|
||||||
.insert(&alice, features, Alias::new("bob"), 16, timestamp + 1, [])
|
.insert(
|
||||||
|
&alice,
|
||||||
|
1,
|
||||||
|
features,
|
||||||
|
Alias::new("bob"),
|
||||||
|
16,
|
||||||
|
&ua,
|
||||||
|
timestamp + 1,
|
||||||
|
[],
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let node = cache.get(&alice).unwrap().unwrap();
|
let node = cache.get(&alice).unwrap().unwrap();
|
||||||
assert_eq!(node.alias.as_ref(), "bob");
|
assert_eq!(node.alias.as_ref(), "bob");
|
||||||
|
|
@ -528,8 +567,10 @@ mod test {
|
||||||
fn test_insert_and_get() {
|
fn test_insert_and_get() {
|
||||||
let alice = arbitrary::gen::<NodeId>(1);
|
let alice = arbitrary::gen::<NodeId>(1);
|
||||||
let mut cache = Database::memory().unwrap();
|
let mut cache = Database::memory().unwrap();
|
||||||
|
let version = 2;
|
||||||
let features = node::Features::SEED;
|
let features = node::Features::SEED;
|
||||||
let timestamp = LocalTime::now().into();
|
let timestamp = LocalTime::now().into();
|
||||||
|
let ua = UserAgent::default();
|
||||||
|
|
||||||
let ka = KnownAddress {
|
let ka = KnownAddress {
|
||||||
addr: net::SocketAddr::from(([4, 4, 4, 4], 8776)).into(),
|
addr: net::SocketAddr::from(([4, 4, 4, 4], 8776)).into(),
|
||||||
|
|
@ -541,9 +582,11 @@ mod test {
|
||||||
let inserted = cache
|
let inserted = cache
|
||||||
.insert(
|
.insert(
|
||||||
&alice,
|
&alice,
|
||||||
|
version,
|
||||||
features,
|
features,
|
||||||
Alias::new("alice"),
|
Alias::new("alice"),
|
||||||
16,
|
16,
|
||||||
|
&ua,
|
||||||
timestamp,
|
timestamp,
|
||||||
[ka.clone()],
|
[ka.clone()],
|
||||||
)
|
)
|
||||||
|
|
@ -552,6 +595,7 @@ mod test {
|
||||||
|
|
||||||
let node = cache.get(&alice).unwrap().unwrap();
|
let node = cache.get(&alice).unwrap().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(node.version, version);
|
||||||
assert_eq!(node.features, features);
|
assert_eq!(node.features, features);
|
||||||
assert_eq!(node.pow, 16);
|
assert_eq!(node.pow, 16);
|
||||||
assert_eq!(node.timestamp, timestamp);
|
assert_eq!(node.timestamp, timestamp);
|
||||||
|
|
@ -566,6 +610,7 @@ mod test {
|
||||||
let features = node::Features::SEED;
|
let features = node::Features::SEED;
|
||||||
let timestamp = LocalTime::now().into();
|
let timestamp = LocalTime::now().into();
|
||||||
let alias = Alias::new("alice");
|
let alias = Alias::new("alice");
|
||||||
|
let ua = UserAgent::default();
|
||||||
|
|
||||||
let ka = KnownAddress {
|
let ka = KnownAddress {
|
||||||
addr: net::SocketAddr::from(([4, 4, 4, 4], 8776)).into(),
|
addr: net::SocketAddr::from(([4, 4, 4, 4], 8776)).into(),
|
||||||
|
|
@ -575,12 +620,21 @@ mod test {
|
||||||
banned: false,
|
banned: false,
|
||||||
};
|
};
|
||||||
let inserted = cache
|
let inserted = cache
|
||||||
.insert(&alice, features, alias.clone(), 0, timestamp, [ka.clone()])
|
.insert(
|
||||||
|
&alice,
|
||||||
|
1,
|
||||||
|
features,
|
||||||
|
alias.clone(),
|
||||||
|
0,
|
||||||
|
&ua,
|
||||||
|
timestamp,
|
||||||
|
[ka.clone()],
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(inserted);
|
assert!(inserted);
|
||||||
|
|
||||||
let inserted = cache
|
let inserted = cache
|
||||||
.insert(&alice, features, alias, 0, timestamp, [ka])
|
.insert(&alice, 1, features, alias, 0, &ua, timestamp, [ka])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!inserted);
|
assert!(!inserted);
|
||||||
|
|
||||||
|
|
@ -593,6 +647,8 @@ mod test {
|
||||||
let mut cache = Database::memory().unwrap();
|
let mut cache = Database::memory().unwrap();
|
||||||
let timestamp = LocalTime::now().into();
|
let timestamp = LocalTime::now().into();
|
||||||
let features = node::Features::SEED;
|
let features = node::Features::SEED;
|
||||||
|
let ua1 = UserAgent::default();
|
||||||
|
let ua2 = UserAgent::default();
|
||||||
let alias1 = Alias::new("alice");
|
let alias1 = Alias::new("alice");
|
||||||
let alias2 = Alias::new("~alice~");
|
let alias2 = Alias::new("~alice~");
|
||||||
let ka = KnownAddress {
|
let ka = KnownAddress {
|
||||||
|
|
@ -604,17 +660,35 @@ mod test {
|
||||||
};
|
};
|
||||||
|
|
||||||
let updated = cache
|
let updated = cache
|
||||||
.insert(&alice, features, alias1, 0, timestamp, [ka.clone()])
|
.insert(
|
||||||
|
&alice,
|
||||||
|
1,
|
||||||
|
features,
|
||||||
|
alias1,
|
||||||
|
0,
|
||||||
|
&ua1,
|
||||||
|
timestamp,
|
||||||
|
[ka.clone()],
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(updated);
|
assert!(updated);
|
||||||
|
|
||||||
let updated = cache
|
let updated = cache
|
||||||
.insert(&alice, features, alias2.clone(), 0, timestamp, [])
|
.insert(&alice, 1, features, alias2.clone(), 0, &ua1, 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, alias2.clone(), 0, timestamp - 1, [])
|
.insert(
|
||||||
|
&alice,
|
||||||
|
1,
|
||||||
|
features,
|
||||||
|
alias2.clone(),
|
||||||
|
0,
|
||||||
|
&ua1,
|
||||||
|
timestamp - 1,
|
||||||
|
[],
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!updated, "Can't update using a smaller timestamp");
|
assert!(!updated, "Can't update using a smaller timestamp");
|
||||||
|
|
||||||
|
|
@ -624,12 +698,30 @@ mod test {
|
||||||
assert_eq!(node.pow, 0);
|
assert_eq!(node.pow, 0);
|
||||||
|
|
||||||
let updated = cache
|
let updated = cache
|
||||||
.insert(&alice, features, alias2.clone(), 0, timestamp + 1, [])
|
.insert(
|
||||||
|
&alice,
|
||||||
|
1,
|
||||||
|
features,
|
||||||
|
alias2.clone(),
|
||||||
|
0,
|
||||||
|
&ua2,
|
||||||
|
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, alias2, 1, timestamp + 2, [])
|
.insert(
|
||||||
|
&alice,
|
||||||
|
1,
|
||||||
|
node::Features::NONE,
|
||||||
|
alias2,
|
||||||
|
1,
|
||||||
|
&ua2,
|
||||||
|
timestamp + 2,
|
||||||
|
[],
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(updated);
|
assert!(updated);
|
||||||
|
|
||||||
|
|
@ -639,6 +731,7 @@ mod test {
|
||||||
assert_eq!(node.timestamp, timestamp + 2);
|
assert_eq!(node.timestamp, timestamp + 2);
|
||||||
assert_eq!(node.pow, 1);
|
assert_eq!(node.pow, 1);
|
||||||
assert_eq!(node.addrs, vec![ka]);
|
assert_eq!(node.addrs, vec![ka]);
|
||||||
|
assert_eq!(node.agent, ua2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -647,6 +740,7 @@ mod test {
|
||||||
let bob = arbitrary::gen::<NodeId>(1);
|
let bob = arbitrary::gen::<NodeId>(1);
|
||||||
let mut cache = Database::memory().unwrap();
|
let mut cache = Database::memory().unwrap();
|
||||||
let timestamp = LocalTime::now().into();
|
let timestamp = LocalTime::now().into();
|
||||||
|
let ua = UserAgent::default();
|
||||||
let features = node::Features::SEED;
|
let features = node::Features::SEED;
|
||||||
let alice_alias = Alias::new("alice");
|
let alice_alias = Alias::new("alice");
|
||||||
let bob_alias = Alias::new("bob");
|
let bob_alias = Alias::new("bob");
|
||||||
|
|
@ -666,15 +760,26 @@ mod test {
|
||||||
cache
|
cache
|
||||||
.insert(
|
.insert(
|
||||||
&alice,
|
&alice,
|
||||||
|
1,
|
||||||
features,
|
features,
|
||||||
alice_alias.clone(),
|
alice_alias.clone(),
|
||||||
0,
|
0,
|
||||||
|
&ua,
|
||||||
timestamp,
|
timestamp,
|
||||||
[ka.clone()],
|
[ka.clone()],
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
cache
|
cache
|
||||||
.insert(&bob, features, bob_alias.clone(), 0, timestamp, [ka])
|
.insert(
|
||||||
|
&bob,
|
||||||
|
1,
|
||||||
|
features,
|
||||||
|
bob_alias.clone(),
|
||||||
|
0,
|
||||||
|
&ua,
|
||||||
|
timestamp,
|
||||||
|
[ka],
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
assert_eq!(cache.len().unwrap(), 6);
|
assert_eq!(cache.len().unwrap(), 6);
|
||||||
|
|
@ -695,6 +800,7 @@ mod test {
|
||||||
let mut cache = Database::memory().unwrap();
|
let mut cache = Database::memory().unwrap();
|
||||||
let mut expected = Vec::new();
|
let mut expected = Vec::new();
|
||||||
let timestamp = LocalTime::now().into();
|
let timestamp = LocalTime::now().into();
|
||||||
|
let ua = UserAgent::default();
|
||||||
let features = node::Features::SEED;
|
let features = node::Features::SEED;
|
||||||
let alias = Alias::new("alice");
|
let alias = Alias::new("alice");
|
||||||
|
|
||||||
|
|
@ -711,11 +817,12 @@ mod test {
|
||||||
};
|
};
|
||||||
expected.push(AddressEntry {
|
expected.push(AddressEntry {
|
||||||
node: id,
|
node: id,
|
||||||
|
version: 3,
|
||||||
penalty: Penalty::default(),
|
penalty: Penalty::default(),
|
||||||
address: ka.clone(),
|
address: ka.clone(),
|
||||||
});
|
});
|
||||||
cache
|
cache
|
||||||
.insert(&id, features, alias.clone(), 0, timestamp, [ka])
|
.insert(&id, 3, features, alias.clone(), 0, &ua, timestamp, [ka])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -735,9 +842,19 @@ mod test {
|
||||||
let mut cache = Database::memory().unwrap();
|
let mut cache = Database::memory().unwrap();
|
||||||
let features = node::Features::SEED;
|
let features = node::Features::SEED;
|
||||||
let timestamp = Timestamp::from(LocalTime::now());
|
let timestamp = Timestamp::from(LocalTime::now());
|
||||||
|
let ua = UserAgent::default();
|
||||||
|
|
||||||
cache
|
cache
|
||||||
.insert(&alice, features, Alias::new("alice"), 16, timestamp, [])
|
.insert(
|
||||||
|
&alice,
|
||||||
|
1,
|
||||||
|
features,
|
||||||
|
Alias::new("alice"),
|
||||||
|
16,
|
||||||
|
&ua,
|
||||||
|
timestamp,
|
||||||
|
[],
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let node = cache.get(&alice).unwrap().unwrap();
|
let node = cache.get(&alice).unwrap().unwrap();
|
||||||
assert_eq!(node.penalty, Penalty::default());
|
assert_eq!(node.penalty, Penalty::default());
|
||||||
|
|
@ -762,6 +879,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_disconnected_ban() {
|
fn test_disconnected_ban() {
|
||||||
let alice = arbitrary::gen::<NodeId>(1);
|
let alice = arbitrary::gen::<NodeId>(1);
|
||||||
|
let ua = UserAgent::default();
|
||||||
let ip1: net::Ipv4Addr = [8, 8, 8, 8].into();
|
let ip1: net::Ipv4Addr = [8, 8, 8, 8].into();
|
||||||
let ip2: net::Ipv4Addr = [9, 9, 9, 9].into();
|
let ip2: net::Ipv4Addr = [9, 9, 9, 9].into();
|
||||||
let ka1 = arbitrary::gen::<KnownAddress>(1);
|
let ka1 = arbitrary::gen::<KnownAddress>(1);
|
||||||
|
|
@ -780,9 +898,11 @@ mod test {
|
||||||
|
|
||||||
db.insert(
|
db.insert(
|
||||||
&alice,
|
&alice,
|
||||||
|
1,
|
||||||
features,
|
features,
|
||||||
Alias::new("alice"),
|
Alias::new("alice"),
|
||||||
16,
|
16,
|
||||||
|
&ua,
|
||||||
timestamp,
|
timestamp,
|
||||||
[ka1.clone(), ka2.clone()],
|
[ka1.clone(), ka2.clone()],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ use crate::node;
|
||||||
use crate::node::policy::SeedingPolicy;
|
use crate::node::policy::SeedingPolicy;
|
||||||
use crate::node::{Address, Alias, NodeId};
|
use crate::node::{Address, Alias, NodeId};
|
||||||
|
|
||||||
|
/// Peer-to-peer protocol version.
|
||||||
|
pub type ProtocolVersion = u8;
|
||||||
|
|
||||||
/// Default number of workers to spawn.
|
/// Default number of workers to spawn.
|
||||||
pub const DEFAULT_WORKERS: usize = 8;
|
pub const DEFAULT_WORKERS: usize = 8;
|
||||||
|
|
||||||
|
|
@ -63,14 +66,14 @@ pub enum Network {
|
||||||
|
|
||||||
impl Network {
|
impl Network {
|
||||||
/// Bootstrap nodes for this network.
|
/// Bootstrap nodes for this network.
|
||||||
pub fn bootstrap(&self) -> Vec<(Alias, ConnectAddress)> {
|
pub fn bootstrap(&self) -> Vec<(Alias, ProtocolVersion, ConnectAddress)> {
|
||||||
match self {
|
match self {
|
||||||
Self::Main => [
|
Self::Main => [
|
||||||
("seed.radicle.garden", seeds::RADICLE_COMMUNITY_NODE.clone()),
|
("seed.radicle.garden", seeds::RADICLE_COMMUNITY_NODE.clone()),
|
||||||
("seed.radicle.xyz", seeds::RADICLE_TEAM_NODE.clone()),
|
("seed.radicle.xyz", seeds::RADICLE_TEAM_NODE.clone()),
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(a, s)| (Alias::new(a), s))
|
.map(|(a, s)| (Alias::new(a), 1, s))
|
||||||
.collect(),
|
.collect(),
|
||||||
|
|
||||||
Self::Test => vec![],
|
Self::Test => vec![],
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,9 @@ use std::{fmt, time};
|
||||||
use sqlite as sql;
|
use sqlite as sql;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::node::{address, Address, Alias, Features, KnownAddress, NodeId, Timestamp};
|
use crate::node::{
|
||||||
|
address, Address, Alias, Features, KnownAddress, NodeId, Timestamp, UserAgent, PROTOCOL_VERSION,
|
||||||
|
};
|
||||||
use crate::sql::transaction;
|
use crate::sql::transaction;
|
||||||
|
|
||||||
/// How long to wait for the database lock to be released before failing a read.
|
/// How long to wait for the database lock to be released before failing a read.
|
||||||
|
|
@ -31,6 +33,7 @@ const MIGRATIONS: &[&str] = &[
|
||||||
include_str!("db/migrations/3.sql"),
|
include_str!("db/migrations/3.sql"),
|
||||||
include_str!("db/migrations/4.sql"),
|
include_str!("db/migrations/4.sql"),
|
||||||
include_str!("db/migrations/5.sql"),
|
include_str!("db/migrations/5.sql"),
|
||||||
|
include_str!("db/migrations/6.sql"),
|
||||||
];
|
];
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
|
|
@ -131,15 +134,18 @@ impl Database {
|
||||||
node: &NodeId,
|
node: &NodeId,
|
||||||
features: Features,
|
features: Features,
|
||||||
alias: Alias,
|
alias: Alias,
|
||||||
|
agent: &UserAgent,
|
||||||
timestamp: Timestamp,
|
timestamp: Timestamp,
|
||||||
addrs: impl IntoIterator<Item = &'a Address>,
|
addrs: impl IntoIterator<Item = &'a Address>,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
address::Store::insert(
|
address::Store::insert(
|
||||||
&mut self,
|
&mut self,
|
||||||
node,
|
node,
|
||||||
|
PROTOCOL_VERSION,
|
||||||
features,
|
features,
|
||||||
alias,
|
alias,
|
||||||
0,
|
0,
|
||||||
|
agent,
|
||||||
timestamp,
|
timestamp,
|
||||||
addrs
|
addrs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
-- Add the version and user-agent columns.
|
||||||
|
alter table "nodes" add column "version" integer not null default 1;
|
||||||
|
alter table "nodes" add column "agent" text not null default "/radicle/";
|
||||||
|
|
@ -27,7 +27,7 @@ use crate::node::policy::config::store::Read;
|
||||||
use crate::node::{
|
use crate::node::{
|
||||||
notifications, policy,
|
notifications, policy,
|
||||||
policy::{Policy, Scope, SeedingPolicy},
|
policy::{Policy, Scope, SeedingPolicy},
|
||||||
Alias, AliasStore, Handle as _, Node,
|
Alias, AliasStore, Handle as _, Node, UserAgent,
|
||||||
};
|
};
|
||||||
use crate::prelude::{Did, NodeId, RepoId};
|
use crate::prelude::{Did, NodeId, RepoId};
|
||||||
use crate::storage::git::transport;
|
use crate::storage::git::transport;
|
||||||
|
|
@ -319,6 +319,7 @@ impl Profile {
|
||||||
&public_key,
|
&public_key,
|
||||||
config.node.features(),
|
config.node.features(),
|
||||||
config.node.alias.clone(),
|
config.node.alias.clone(),
|
||||||
|
&UserAgent::default(),
|
||||||
LocalTime::now().into(),
|
LocalTime::now().into(),
|
||||||
config.node.external_addresses.iter(),
|
config.node.external_addresses.iter(),
|
||||||
)?;
|
)?;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use sqlite::Value;
|
||||||
|
|
||||||
use crate::identity::RepoId;
|
use crate::identity::RepoId;
|
||||||
use crate::node;
|
use crate::node;
|
||||||
use crate::node::Address;
|
use crate::node::{Address, UserAgent};
|
||||||
|
|
||||||
/// Run an SQL query inside a transaction.
|
/// Run an SQL query inside a transaction.
|
||||||
/// Commits the transaction on success, and rolls back on error.
|
/// Commits the transaction on success, and rolls back on error.
|
||||||
|
|
@ -93,3 +93,20 @@ impl sql::BindableWithIndex for &Address {
|
||||||
self.to_string().bind(stmt, i)
|
self.to_string().bind(stmt, i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&Value> for UserAgent {
|
||||||
|
type Error = sql::Error;
|
||||||
|
|
||||||
|
fn try_from(value: &Value) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
Value::String(ua) => UserAgent::from_str(ua).map_err(|e| sql::Error {
|
||||||
|
code: None,
|
||||||
|
message: Some(e.to_string()),
|
||||||
|
}),
|
||||||
|
_ => Err(sql::Error {
|
||||||
|
code: None,
|
||||||
|
message: Some("sql: invalid type for user-agent".to_owned()),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ use crate::identity::{
|
||||||
Did,
|
Did,
|
||||||
};
|
};
|
||||||
use crate::node::address::{AddressType, Source};
|
use crate::node::address::{AddressType, Source};
|
||||||
use crate::node::{Address, Alias, KnownAddress, Timestamp};
|
use crate::node::{Address, Alias, KnownAddress, Timestamp, UserAgent};
|
||||||
use crate::storage;
|
use crate::storage;
|
||||||
use crate::storage::refs::{Refs, RefsAt, SignedRefs};
|
use crate::storage::refs::{Refs, RefsAt, SignedRefs};
|
||||||
use crate::test::storage::{MockRepository, MockStorage};
|
use crate::test::storage::{MockRepository, MockStorage};
|
||||||
|
|
@ -324,3 +324,12 @@ impl Arbitrary for Timestamp {
|
||||||
Self::try_from(u64::arbitrary(g).min(*Self::MAX)).unwrap()
|
Self::try_from(u64::arbitrary(g).min(*Self::MAX)).unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Arbitrary for UserAgent {
|
||||||
|
fn arbitrary(g: &mut qcheck::Gen) -> Self {
|
||||||
|
UserAgent::from_str(
|
||||||
|
format!("/radicle:1.{}.{}/", u8::arbitrary(g), u8::arbitrary(g)).as_str(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue