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::routing::Store as _;
|
||||
use radicle::node::Handle as _;
|
||||
use radicle::node::UserAgent;
|
||||
use radicle::node::{Address, Alias, DEFAULT_TIMEOUT};
|
||||
use radicle::prelude::{NodeId, RepoId};
|
||||
use radicle::profile;
|
||||
|
|
@ -21,6 +22,7 @@ use radicle_node::service::Event;
|
|||
use radicle_node::test::environment::{Config, Environment, Node};
|
||||
#[allow(unused_imports)]
|
||||
use radicle_node::test::logger;
|
||||
use radicle_node::PROTOCOL_VERSION;
|
||||
|
||||
/// Seed used in tests.
|
||||
const RAD_SEED: &str = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
|
||||
|
|
@ -1317,9 +1319,11 @@ fn rad_clone_partial_fail() {
|
|||
.addresses_mut()
|
||||
.insert(
|
||||
&carol,
|
||||
PROTOCOL_VERSION,
|
||||
node::Features::SEED,
|
||||
Alias::new("carol"),
|
||||
0,
|
||||
&UserAgent::default(),
|
||||
localtime::LocalTime::now().into(),
|
||||
[node::KnownAddress::new(
|
||||
// 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 mut eve = environment.node(Config::test(Alias::new("eve")));
|
||||
let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
|
||||
let ua = UserAgent::default();
|
||||
let now = localtime::LocalTime::now().into();
|
||||
|
||||
fixtures::repository(working.join("acme"));
|
||||
|
|
@ -1383,9 +1388,11 @@ fn rad_clone_connect() {
|
|||
.addresses_mut()
|
||||
.insert(
|
||||
&alice.id,
|
||||
PROTOCOL_VERSION,
|
||||
node::Features::SEED,
|
||||
Alias::new("alice"),
|
||||
0,
|
||||
&ua,
|
||||
now,
|
||||
[node::KnownAddress::new(
|
||||
node::Address::from(alice.addr),
|
||||
|
|
@ -1397,9 +1404,11 @@ fn rad_clone_connect() {
|
|||
.addresses_mut()
|
||||
.insert(
|
||||
&bob.id,
|
||||
PROTOCOL_VERSION,
|
||||
node::Features::SEED,
|
||||
Alias::new("bob"),
|
||||
0,
|
||||
&ua,
|
||||
now,
|
||||
[node::KnownAddress::new(
|
||||
node::Address::from(bob.addr),
|
||||
|
|
|
|||
|
|
@ -10,12 +10,23 @@ pub mod tests;
|
|||
pub mod wire;
|
||||
pub mod worker;
|
||||
|
||||
use radicle::version::Version;
|
||||
|
||||
pub use localtime::{LocalDuration, LocalTime};
|
||||
pub use netservices::Direction as Link;
|
||||
pub use radicle::node::PROTOCOL_VERSION;
|
||||
pub use radicle::prelude::Timestamp;
|
||||
pub use radicle::{collections, crypto, git, identity, node, profile, rad, storage};
|
||||
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 use crate::bounded::BoundedVec;
|
||||
pub use crate::crypto::{PublicKey, Signature, Signer};
|
||||
|
|
|
|||
|
|
@ -7,18 +7,10 @@ use crossbeam_channel as chan;
|
|||
use radicle::logger;
|
||||
use radicle::prelude::Signer;
|
||||
use radicle::profile;
|
||||
use radicle::version::Version;
|
||||
use radicle_node::crypto::ssh::keystore::{Keystore, MemorySigner};
|
||||
use radicle_node::Runtime;
|
||||
use radicle_node::{Runtime, VERSION};
|
||||
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#"
|
||||
Usage
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ use radicle::node::address;
|
|||
use radicle::node::address::Store as _;
|
||||
use radicle::node::notifications;
|
||||
use radicle::node::Handle as _;
|
||||
use radicle::node::UserAgent;
|
||||
use radicle::profile::Home;
|
||||
use radicle::{cob, git, storage, Storage};
|
||||
|
||||
|
|
@ -127,7 +128,6 @@ impl Runtime {
|
|||
for (key, _) in &config.extra {
|
||||
log::warn!(target: "node", "Unused or deprecated configuration attribute {:?}", key);
|
||||
}
|
||||
log::info!(target: "node", "Opening node database..");
|
||||
|
||||
log::info!(target: "node", "Opening policy database..");
|
||||
let policies = home.policies_mut()?;
|
||||
|
|
@ -174,6 +174,7 @@ impl Runtime {
|
|||
.expect("Runtime::init: unable to solve proof-of-work puzzle")
|
||||
};
|
||||
|
||||
log::info!(target: "node", "Opening node database..");
|
||||
let db = home
|
||||
.database_mut()?
|
||||
.journal_mode(node::db::JournalMode::default())?
|
||||
|
|
@ -181,6 +182,7 @@ impl Runtime {
|
|||
&id,
|
||||
announcement.features,
|
||||
announcement.alias.clone(),
|
||||
&announcement.agent,
|
||||
announcement.timestamp,
|
||||
announcement.addresses.iter(),
|
||||
)?;
|
||||
|
|
@ -189,14 +191,16 @@ impl Runtime {
|
|||
if config.connect.is_empty() && stores.addresses().is_empty()? {
|
||||
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();
|
||||
|
||||
stores.addresses_mut().insert(
|
||||
&id,
|
||||
version,
|
||||
radicle::node::Features::SEED,
|
||||
alias,
|
||||
0,
|
||||
&UserAgent::default(),
|
||||
clock.into(),
|
||||
[node::KnownAddress::new(addr, address::Source::Bootstrap)],
|
||||
)?;
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ use radicle::storage::refs::SIGREFS_BRANCH;
|
|||
use radicle::storage::RepositoryError;
|
||||
use radicle_fetch::policy::SeedingPolicy;
|
||||
|
||||
use crate::crypto;
|
||||
use crate::crypto::{Signer, Verified};
|
||||
use crate::identity::{Doc, RepoId};
|
||||
use crate::node::routing;
|
||||
|
|
@ -56,6 +55,7 @@ use crate::storage::{refs::RefsAt, Namespaces, ReadStorage};
|
|||
use crate::worker::fetch;
|
||||
use crate::worker::FetchError;
|
||||
use crate::Link;
|
||||
use crate::{crypto, PROTOCOL_VERSION};
|
||||
|
||||
pub use crate::node::events::{Event, Events};
|
||||
pub use crate::node::{config::Network, Config, NodeId};
|
||||
|
|
@ -1693,9 +1693,11 @@ where
|
|||
|
||||
match self.db.addresses_mut().insert(
|
||||
announcer,
|
||||
*features,
|
||||
ann.version,
|
||||
ann.features,
|
||||
ann.alias.clone(),
|
||||
ann.work(),
|
||||
&ann.agent,
|
||||
timestamp,
|
||||
addresses
|
||||
.iter()
|
||||
|
|
@ -2380,6 +2382,7 @@ where
|
|||
// 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.
|
||||
let mut peers = entries
|
||||
.filter(|entry| entry.version == PROTOCOL_VERSION)
|
||||
.filter(|entry| !entry.address.banned)
|
||||
.filter(|entry| !entry.penalty.is_connect_threshold_reached())
|
||||
.filter(|entry| !self.sessions.contains_key(&entry.node))
|
||||
|
|
|
|||
|
|
@ -1,9 +1,20 @@
|
|||
pub mod store;
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
use crate::{PROTOCOL_VERSION, VERSION};
|
||||
use once_cell::sync::Lazy;
|
||||
use radicle::node::UserAgent;
|
||||
|
||||
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 {
|
||||
let features = config.features();
|
||||
let alias = config.alias.clone();
|
||||
|
|
@ -12,13 +23,17 @@ pub fn node(config: &Config, timestamp: Timestamp) -> NodeAnnouncement {
|
|||
.clone()
|
||||
.try_into()
|
||||
.expect("external addresses are within the limit");
|
||||
let agent = USER_AGENT.clone();
|
||||
let version = PROTOCOL_VERSION;
|
||||
|
||||
NodeAnnouncement {
|
||||
features,
|
||||
version,
|
||||
timestamp,
|
||||
alias,
|
||||
addresses,
|
||||
nonce: 0,
|
||||
agent,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use radicle::storage::refs::RefsAt;
|
|||
use crate::crypto;
|
||||
use crate::identity::RepoId;
|
||||
use crate::node;
|
||||
use crate::node::{Address, Alias};
|
||||
use crate::node::{Address, Alias, UserAgent};
|
||||
use crate::prelude::BoundedVec;
|
||||
use crate::service::filter::Filter;
|
||||
use crate::service::{Link, NodeId, Timestamp};
|
||||
|
|
@ -54,6 +54,8 @@ impl Subscribe {
|
|||
/// Node announcing itself to the network.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct NodeAnnouncement {
|
||||
/// Supported protocol version.
|
||||
pub version: u8,
|
||||
/// Advertized features.
|
||||
pub features: node::Features,
|
||||
/// Monotonic timestamp.
|
||||
|
|
@ -64,6 +66,8 @@ pub struct NodeAnnouncement {
|
|||
pub addresses: BoundedVec<Address, ADDRESS_LIMIT>,
|
||||
/// Nonce used for announcement proof-of-work.
|
||||
pub nonce: u64,
|
||||
/// User-agent string.
|
||||
pub agent: UserAgent,
|
||||
}
|
||||
|
||||
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> {
|
||||
let mut n = 0;
|
||||
|
||||
n += self.version.encode(writer)?;
|
||||
n += self.features.encode(writer)?;
|
||||
n += self.timestamp.encode(writer)?;
|
||||
n += self.alias.encode(writer)?;
|
||||
n += self.addresses.encode(writer)?;
|
||||
n += self.nonce.encode(writer)?;
|
||||
n += self.agent.encode(writer)?;
|
||||
|
||||
Ok(n)
|
||||
}
|
||||
|
|
@ -137,18 +143,26 @@ impl wire::Encode for NodeAnnouncement {
|
|||
|
||||
impl wire::Decode for NodeAnnouncement {
|
||||
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 timestamp = Timestamp::decode(reader)?;
|
||||
let alias = wire::Decode::decode(reader)?;
|
||||
let addresses = BoundedVec::<Address, ADDRESS_LIMIT>::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 {
|
||||
version,
|
||||
features,
|
||||
timestamp,
|
||||
alias,
|
||||
addresses,
|
||||
nonce,
|
||||
agent,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -330,10 +344,10 @@ impl fmt::Debug for AnnouncementMessage {
|
|||
pub struct Announcement {
|
||||
/// Node identifier.
|
||||
pub node: NodeId,
|
||||
/// Unsigned node announcement.
|
||||
pub message: AnnouncementMessage,
|
||||
/// Signature over the announcement.
|
||||
pub signature: crypto::Signature,
|
||||
/// Unsigned node announcement.
|
||||
pub message: AnnouncementMessage,
|
||||
}
|
||||
|
||||
impl Announcement {
|
||||
|
|
@ -576,16 +590,18 @@ impl ZeroBytes {
|
|||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::prelude::*;
|
||||
use crate::wire::Encode;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::crypto::test::signer::MockSigner;
|
||||
use crate::test::arbitrary;
|
||||
use fastrand;
|
||||
use qcheck_macros::quickcheck;
|
||||
use radicle::git::raw;
|
||||
|
||||
use super::*;
|
||||
use crate::crypto::test::signer::MockSigner;
|
||||
use crate::prelude::*;
|
||||
use crate::test::arbitrary;
|
||||
use crate::wire::Encode;
|
||||
|
||||
#[test]
|
||||
fn test_ref_remote_limit() {
|
||||
let mut refs = BoundedVec::<_, REF_REMOTE_LIMIT>::new();
|
||||
|
|
@ -672,16 +688,18 @@ mod tests {
|
|||
#[test]
|
||||
fn test_node_announcement_validate() {
|
||||
let ann = NodeAnnouncement {
|
||||
version: 1,
|
||||
features: node::Features::SEED,
|
||||
timestamp: Timestamp::try_from(42491841u64).unwrap(),
|
||||
alias: Alias::new("alice"),
|
||||
addresses: BoundedVec::new(),
|
||||
nonce: 0,
|
||||
agent: UserAgent::from_str("/heartwood:1.0.0/").unwrap(),
|
||||
};
|
||||
|
||||
assert_eq!(ann.work(), 0);
|
||||
assert_eq!(ann.clone().solve(1).unwrap().work(), 4);
|
||||
assert_eq!(ann.clone().solve(8).unwrap().work(), 9);
|
||||
assert_eq!(ann.work(), 1);
|
||||
assert_eq!(ann.clone().solve(1).unwrap().work(), 1);
|
||||
assert_eq!(ann.clone().solve(8).unwrap().work(), 10);
|
||||
assert_eq!(ann.solve(14).unwrap().work(), 14);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use std::collections::HashSet;
|
|||
|
||||
use bloomy::BloomFilter;
|
||||
use qcheck::Arbitrary;
|
||||
use radicle::node::UserAgent;
|
||||
|
||||
use crate::crypto;
|
||||
use crate::identity::DocAt;
|
||||
|
|
@ -80,11 +81,13 @@ impl Arbitrary for Message {
|
|||
.into(),
|
||||
MessageType::NodeAnnouncement => {
|
||||
let message = NodeAnnouncement {
|
||||
version: u8::arbitrary(g),
|
||||
features: u64::arbitrary(g).into(),
|
||||
timestamp: Timestamp::arbitrary(g),
|
||||
alias: Alias::arbitrary(g),
|
||||
addresses: Arbitrary::arbitrary(g),
|
||||
nonce: u64::arbitrary(g),
|
||||
agent: UserAgent::arbitrary(g),
|
||||
}
|
||||
.into();
|
||||
let bytes: [u8; 64] = Arbitrary::arbitrary(g);
|
||||
|
|
|
|||
|
|
@ -20,8 +20,7 @@ use radicle::identity::{RepoId, Visibility};
|
|||
use radicle::node::config::ConnectAddress;
|
||||
use radicle::node::policy::store as policy;
|
||||
use radicle::node::seed::Store as _;
|
||||
use radicle::node::Database;
|
||||
use radicle::node::{Alias, POLICIES_DB_FILE};
|
||||
use radicle::node::{Alias, Database, UserAgent, POLICIES_DB_FILE};
|
||||
use radicle::node::{ConnectOptions, Handle as _};
|
||||
use radicle::profile;
|
||||
use radicle::profile::{env, Home, Profile};
|
||||
|
|
@ -135,6 +134,7 @@ impl Environment {
|
|||
&public_key,
|
||||
config.node.features(),
|
||||
Alias::new(alias),
|
||||
&UserAgent::default(),
|
||||
now.into(),
|
||||
config.node.external_addresses.iter(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use radicle::crypto::test::signer::MockSigner;
|
||||
use radicle::node;
|
||||
use radicle::node::UserAgent;
|
||||
use radicle::test::fixtures::gen;
|
||||
|
||||
use crate::test::arbitrary;
|
||||
use crate::{
|
||||
prelude::{LocalDuration, LocalTime, Message},
|
||||
service::message::{InventoryAnnouncement, NodeAnnouncement},
|
||||
PROTOCOL_VERSION,
|
||||
};
|
||||
|
||||
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(
|
||||
NodeAnnouncement {
|
||||
version: PROTOCOL_VERSION,
|
||||
features: node::Features::SEED,
|
||||
timestamp: time.into(),
|
||||
alias: node::Alias::new(gen::string(5)),
|
||||
addresses: None.into(),
|
||||
nonce: 0,
|
||||
agent: UserAgent::from_str("/radicle:test/").unwrap(),
|
||||
}
|
||||
.solve(0)
|
||||
.unwrap(),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use log::*;
|
|||
use radicle::identity::Visibility;
|
||||
use radicle::node::address::Store as _;
|
||||
use radicle::node::Database;
|
||||
use radicle::node::UserAgent;
|
||||
use radicle::node::{address, Alias, ConnectOptions};
|
||||
use radicle::rad;
|
||||
use radicle::storage::refs::{RefsAt, SignedRefsAt};
|
||||
|
|
@ -33,8 +34,7 @@ use crate::storage::{RemoteId, WriteStorage};
|
|||
use crate::test::storage::MockStorage;
|
||||
use crate::test::{arbitrary, fixtures, simulator};
|
||||
use crate::wire::MessageType;
|
||||
use crate::Link;
|
||||
use crate::{LocalDuration, LocalTime};
|
||||
use crate::{Link, LocalDuration, LocalTime, PROTOCOL_VERSION};
|
||||
|
||||
/// Service instantiation used for testing.
|
||||
pub type Service<S, G> = service::Service<Database, S, G>;
|
||||
|
|
@ -175,6 +175,7 @@ where
|
|||
&id,
|
||||
config.config.features(),
|
||||
config.config.alias.clone(),
|
||||
&UserAgent::default(),
|
||||
config.local_time.into(),
|
||||
config.config.external_addresses.iter(),
|
||||
)
|
||||
|
|
@ -251,9 +252,11 @@ where
|
|||
.addresses_mut()
|
||||
.insert(
|
||||
&peer.node_id(),
|
||||
PROTOCOL_VERSION,
|
||||
radicle::node::Features::default(),
|
||||
Alias::from_str(peer.name).unwrap(),
|
||||
0,
|
||||
&UserAgent::default(),
|
||||
timestamp,
|
||||
Some(known_address),
|
||||
)
|
||||
|
|
@ -303,11 +306,13 @@ where
|
|||
pub fn node_announcement(&self) -> Message {
|
||||
Message::node(
|
||||
NodeAnnouncement {
|
||||
version: PROTOCOL_VERSION,
|
||||
features: node::Features::SEED,
|
||||
timestamp: self.timestamp(),
|
||||
alias: Alias::from_str(self.name).unwrap(),
|
||||
addresses: Some(net::SocketAddr::from((self.ip, node::DEFAULT_PORT)).into()).into(),
|
||||
nonce: 0,
|
||||
agent: UserAgent::from_str("/radicle:test/").unwrap(),
|
||||
}
|
||||
.solve(0)
|
||||
.unwrap(),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ mod varint;
|
|||
pub use frame::StreamId;
|
||||
pub use message::{AddressType, MessageType};
|
||||
pub use protocol::{Control, Wire, WireReader, WireSession, WireWriter};
|
||||
use radicle::node::UserAgent;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::TryFrom;
|
||||
|
|
@ -54,6 +55,8 @@ pub enum Error {
|
|||
InvalidRefName(#[from] fmt::Error),
|
||||
#[error(transparent)]
|
||||
InvalidAlias(#[from] node::AliasError),
|
||||
#[error("invalid user agent string: {0:?}")]
|
||||
InvalidUserAgent(String),
|
||||
#[error("invalid control message with type `{0}`")]
|
||||
InvalidControlMessage(u8),
|
||||
#[error("invalid protocol version header `{0:x?}`")]
|
||||
|
|
@ -62,8 +65,8 @@ pub enum Error {
|
|||
InvalidOnionAddr(#[from] tor::OnionAddrDecodeError),
|
||||
#[error("invalid timestamp: {0}")]
|
||||
InvalidTimestamp(u64),
|
||||
#[error("unknown protocol version `{0}`")]
|
||||
UnknownProtocolVersion(u8),
|
||||
#[error("wrong protocol version `{0}`")]
|
||||
WrongProtocolVersion(u8),
|
||||
#[error("unknown address type `{0}`")]
|
||||
UnknownAddressType(u8),
|
||||
#[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 {
|
||||
fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
|
||||
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 {
|
||||
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))
|
||||
|
|
|
|||
|
|
@ -2,10 +2,8 @@
|
|||
#![warn(clippy::missing_docs_in_private_items)]
|
||||
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
|
||||
/// by a version number.
|
||||
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 {
|
||||
fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
|
||||
let version = Version::decode(reader)?;
|
||||
if version.number() > PROTOCOL_VERSION {
|
||||
return Err(wire::Error::UnknownProtocolVersion(version.number()));
|
||||
if version.number() != PROTOCOL_VERSION {
|
||||
return Err(wire::Error::WrongProtocolVersion(version.number()));
|
||||
}
|
||||
let stream = StreamId::decode(reader)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -274,8 +274,8 @@ impl wire::Encode for Message {
|
|||
signature,
|
||||
}) => {
|
||||
n += node.encode(writer)?;
|
||||
n += message.encode(writer)?;
|
||||
n += signature.encode(writer)?;
|
||||
n += message.encode(writer)?;
|
||||
}
|
||||
Self::Info(info) => {
|
||||
n += info.encode(writer)?;
|
||||
|
|
@ -317,8 +317,8 @@ impl wire::Decode for Message {
|
|||
}
|
||||
Ok(MessageType::NodeAnnouncement) => {
|
||||
let node = NodeId::decode(reader)?;
|
||||
let message = NodeAnnouncement::decode(reader)?.into();
|
||||
let signature = Signature::decode(reader)?;
|
||||
let message = NodeAnnouncement::decode(reader)?.into();
|
||||
|
||||
Ok(Announcement {
|
||||
node,
|
||||
|
|
@ -329,8 +329,8 @@ impl wire::Decode for Message {
|
|||
}
|
||||
Ok(MessageType::InventoryAnnouncement) => {
|
||||
let node = NodeId::decode(reader)?;
|
||||
let message = InventoryAnnouncement::decode(reader)?.into();
|
||||
let signature = Signature::decode(reader)?;
|
||||
let message = InventoryAnnouncement::decode(reader)?.into();
|
||||
|
||||
Ok(Announcement {
|
||||
node,
|
||||
|
|
@ -341,8 +341,8 @@ impl wire::Decode for Message {
|
|||
}
|
||||
Ok(MessageType::RefsAnnouncement) => {
|
||||
let node = NodeId::decode(reader)?;
|
||||
let message = RefsAnnouncement::decode(reader)?.into();
|
||||
let signature = Signature::decode(reader)?;
|
||||
let message = RefsAnnouncement::decode(reader)?.into();
|
||||
|
||||
Ok(Announcement {
|
||||
node,
|
||||
|
|
@ -458,6 +458,7 @@ impl wire::Decode for ZeroBytes {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use qcheck_macros::quickcheck;
|
||||
use radicle::node::UserAgent;
|
||||
use radicle::storage::refs::RefsAt;
|
||||
use radicle_crypto::test::signer::MockSigner;
|
||||
|
||||
|
|
@ -502,11 +503,13 @@ mod tests {
|
|||
let addrs: [Address; ADDRESS_LIMIT] = arbitrary::gen(1);
|
||||
let alias = ['@'; radicle::node::MAX_ALIAS_LENGTH];
|
||||
let ann = AnnouncementMessage::Node(NodeAnnouncement {
|
||||
version: 1,
|
||||
features: Default::default(),
|
||||
alias: radicle::node::Alias::new(String::from_iter(alias)),
|
||||
addresses: BoundedVec::collect_from(&mut addrs.into_iter()),
|
||||
timestamp: arbitrary::gen(1),
|
||||
nonce: u64::MAX,
|
||||
agent: UserAgent::default(),
|
||||
});
|
||||
let ann = ann.signed(&signer);
|
||||
let msg = Message::Announcement(ann);
|
||||
|
|
@ -555,10 +558,10 @@ mod tests {
|
|||
|
||||
#[quickcheck]
|
||||
fn prop_message_encode_decode(message: Message) {
|
||||
assert_eq!(
|
||||
wire::deserialize::<Message>(&wire::serialize(&message)).unwrap(),
|
||||
message
|
||||
);
|
||||
let encoded = &wire::serialize(&message);
|
||||
let decoded = wire::deserialize::<Message>(encoded).unwrap();
|
||||
|
||||
assert_eq!(message, decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ use crate::service;
|
|||
use crate::service::io::Io;
|
||||
use crate::service::FETCH_TIMEOUT;
|
||||
use crate::service::{session, DisconnectReason, Metrics, Service};
|
||||
use crate::wire;
|
||||
use crate::wire::frame;
|
||||
use crate::wire::frame::{Frame, FrameData, StreamId};
|
||||
use crate::wire::Encode;
|
||||
|
|
@ -839,15 +838,6 @@ where
|
|||
// Buffer is empty, or message isn't complete.
|
||||
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) => {
|
||||
log::error!(target: "wire", "Invalid gossip message from {nid}: {e}");
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ pub use features::Features;
|
|||
pub use seed::SyncedAt;
|
||||
pub use timestamp::Timestamp;
|
||||
|
||||
/// Peer-to-peer protocol version.
|
||||
pub const PROTOCOL_VERSION: u8 = 1;
|
||||
/// Default name for control socket file.
|
||||
pub const DEFAULT_SOCKET_NAME: &str = "control.sock";
|
||||
/// 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.
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
|
|
@ -1282,6 +1355,27 @@ mod test {
|
|||
use super::*;
|
||||
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]
|
||||
fn test_alias() {
|
||||
assert!(Alias::from_str("cloudhead").is_ok());
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use localtime::LocalTime;
|
|||
use nonempty::NonEmpty;
|
||||
|
||||
use crate::collections::RandomMap;
|
||||
use crate::node::{Address, Alias, Penalty};
|
||||
use crate::node::{Address, Alias, Penalty, UserAgent};
|
||||
use crate::prelude::Timestamp;
|
||||
use crate::{node, profile};
|
||||
|
||||
|
|
@ -120,6 +120,8 @@ impl<K: hash::Hash + Eq, V> DerefMut for AddressBook<K, V> {
|
|||
/// Node public data.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Node {
|
||||
/// Protocol version.
|
||||
pub version: u8,
|
||||
/// Advertized alias.
|
||||
pub alias: Alias,
|
||||
/// Advertized features.
|
||||
|
|
@ -130,6 +132,8 @@ pub struct Node {
|
|||
pub pow: u32,
|
||||
/// When this data was published.
|
||||
pub timestamp: Timestamp,
|
||||
/// User agent string.
|
||||
pub agent: UserAgent,
|
||||
/// Node connection penalty.
|
||||
pub penalty: Penalty,
|
||||
/// Whether the node is banned.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use std::net::IpAddr;
|
||||
use std::num::TryFromIntError;
|
||||
use std::str::FromStr;
|
||||
|
||||
use localtime::LocalTime;
|
||||
|
|
@ -7,6 +8,7 @@ use thiserror::Error;
|
|||
|
||||
use crate::node;
|
||||
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::prelude::Timestamp;
|
||||
use crate::sql::transaction;
|
||||
|
|
@ -18,6 +20,8 @@ pub enum Error {
|
|||
Internal(#[from] sql::Error),
|
||||
#[error("alias error: {0}")]
|
||||
InvalidAlias(#[from] AliasError),
|
||||
#[error("integer conversion error: {0}")]
|
||||
TryFromInt(#[from] TryFromIntError),
|
||||
/// No rows returned in query result.
|
||||
#[error("no rows returned")]
|
||||
NoRows,
|
||||
|
|
@ -28,6 +32,8 @@ pub enum Error {
|
|||
pub struct AddressEntry {
|
||||
/// Node ID.
|
||||
pub node: NodeId,
|
||||
/// Node protocol version.
|
||||
pub version: u8,
|
||||
/// Node penalty.
|
||||
pub penalty: Penalty,
|
||||
/// Node address.
|
||||
|
|
@ -48,9 +54,11 @@ pub trait Store {
|
|||
fn insert(
|
||||
&mut self,
|
||||
node: &NodeId,
|
||||
version: u8,
|
||||
features: node::Features,
|
||||
alias: Alias,
|
||||
pow: u32,
|
||||
agent: &UserAgent,
|
||||
timestamp: Timestamp,
|
||||
addrs: impl IntoIterator<Item = KnownAddress>,
|
||||
) -> Result<bool, Error>;
|
||||
|
|
@ -89,24 +97,30 @@ pub trait Store {
|
|||
impl Store for Database {
|
||||
fn get(&self, node: &NodeId) -> Result<Option<Node>, Error> {
|
||||
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))?;
|
||||
|
||||
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 alias = Alias::from_str(row.read::<&str, _>("alias"))?;
|
||||
let timestamp = row.read::<Timestamp, _>("timestamp");
|
||||
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 = Penalty(penalty as u8);
|
||||
let banned = row.read::<i64, _>("banned").is_positive();
|
||||
let addrs = self.addresses_of(node)?;
|
||||
|
||||
Ok(Some(Node {
|
||||
version,
|
||||
features,
|
||||
alias,
|
||||
pow,
|
||||
agent,
|
||||
timestamp,
|
||||
penalty,
|
||||
addrs,
|
||||
|
|
@ -207,26 +221,30 @@ impl Store for Database {
|
|||
fn insert(
|
||||
&mut self,
|
||||
node: &NodeId,
|
||||
version: u8,
|
||||
features: node::Features,
|
||||
alias: Alias,
|
||||
pow: u32,
|
||||
agent: &UserAgent,
|
||||
timestamp: Timestamp,
|
||||
addrs: impl IntoIterator<Item = KnownAddress>,
|
||||
) -> Result<bool, Error> {
|
||||
transaction(&self.db, move |db| {
|
||||
let mut stmt = db.prepare(
|
||||
"INSERT INTO nodes (id, features, alias, pow, timestamp)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)
|
||||
"INSERT INTO nodes (id, version, features, alias, pow, agent, timestamp)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
|
||||
ON CONFLICT DO UPDATE
|
||||
SET features = ?2, alias = ?3, pow = ?4, timestamp = ?5
|
||||
WHERE timestamp < ?5",
|
||||
SET version = ?2, features = ?3, alias = ?4, pow = ?5, agent = ?6, timestamp = ?7
|
||||
WHERE timestamp < ?7",
|
||||
)?;
|
||||
|
||||
stmt.bind((1, node))?;
|
||||
stmt.bind((2, features))?;
|
||||
stmt.bind((3, sql::Value::String(alias.into())))?;
|
||||
stmt.bind((4, pow as i64))?;
|
||||
stmt.bind((5, ×tamp))?;
|
||||
stmt.bind((2, version as i64))?;
|
||||
stmt.bind((3, features))?;
|
||||
stmt.bind((4, sql::Value::String(alias.into())))?;
|
||||
stmt.bind((5, pow as i64))?;
|
||||
stmt.bind((6, agent.as_str()))?;
|
||||
stmt.bind((7, ×tamp))?;
|
||||
stmt.next()?;
|
||||
|
||||
for addr in addrs {
|
||||
|
|
@ -261,7 +279,7 @@ impl Store for Database {
|
|||
let mut stmt = self
|
||||
.db
|
||||
.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
|
||||
JOIN nodes AS n ON a.node = n.id
|
||||
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_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 version = row.read::<i64, _>("version").try_into()?;
|
||||
let banned = row.read::<i64, _>("banned").is_positive();
|
||||
let penalty = row.read::<i64, _>("penalty");
|
||||
let penalty = Penalty(penalty as u8); // Clamped at `u8::MAX`.
|
||||
|
||||
entries.push(AddressEntry {
|
||||
node,
|
||||
version,
|
||||
penalty,
|
||||
address: KnownAddress {
|
||||
addr,
|
||||
|
|
@ -510,15 +530,34 @@ mod test {
|
|||
let mut cache = Database::memory().unwrap();
|
||||
let features = node::Features::SEED;
|
||||
let timestamp = Timestamp::from(LocalTime::now());
|
||||
let ua = UserAgent::default();
|
||||
|
||||
cache
|
||||
.insert(&alice, features, Alias::new("alice"), 16, timestamp, [])
|
||||
.insert(
|
||||
&alice,
|
||||
1,
|
||||
features,
|
||||
Alias::new("alice"),
|
||||
16,
|
||||
&ua,
|
||||
timestamp,
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
let node = cache.get(&alice).unwrap().unwrap();
|
||||
assert_eq!(node.alias.as_ref(), "alice");
|
||||
|
||||
cache
|
||||
.insert(&alice, features, Alias::new("bob"), 16, timestamp + 1, [])
|
||||
.insert(
|
||||
&alice,
|
||||
1,
|
||||
features,
|
||||
Alias::new("bob"),
|
||||
16,
|
||||
&ua,
|
||||
timestamp + 1,
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
let node = cache.get(&alice).unwrap().unwrap();
|
||||
assert_eq!(node.alias.as_ref(), "bob");
|
||||
|
|
@ -528,8 +567,10 @@ mod test {
|
|||
fn test_insert_and_get() {
|
||||
let alice = arbitrary::gen::<NodeId>(1);
|
||||
let mut cache = Database::memory().unwrap();
|
||||
let version = 2;
|
||||
let features = node::Features::SEED;
|
||||
let timestamp = LocalTime::now().into();
|
||||
let ua = UserAgent::default();
|
||||
|
||||
let ka = KnownAddress {
|
||||
addr: net::SocketAddr::from(([4, 4, 4, 4], 8776)).into(),
|
||||
|
|
@ -541,9 +582,11 @@ mod test {
|
|||
let inserted = cache
|
||||
.insert(
|
||||
&alice,
|
||||
version,
|
||||
features,
|
||||
Alias::new("alice"),
|
||||
16,
|
||||
&ua,
|
||||
timestamp,
|
||||
[ka.clone()],
|
||||
)
|
||||
|
|
@ -552,6 +595,7 @@ mod test {
|
|||
|
||||
let node = cache.get(&alice).unwrap().unwrap();
|
||||
|
||||
assert_eq!(node.version, version);
|
||||
assert_eq!(node.features, features);
|
||||
assert_eq!(node.pow, 16);
|
||||
assert_eq!(node.timestamp, timestamp);
|
||||
|
|
@ -566,6 +610,7 @@ mod test {
|
|||
let features = node::Features::SEED;
|
||||
let timestamp = LocalTime::now().into();
|
||||
let alias = Alias::new("alice");
|
||||
let ua = UserAgent::default();
|
||||
|
||||
let ka = KnownAddress {
|
||||
addr: net::SocketAddr::from(([4, 4, 4, 4], 8776)).into(),
|
||||
|
|
@ -575,12 +620,21 @@ mod test {
|
|||
banned: false,
|
||||
};
|
||||
let inserted = cache
|
||||
.insert(&alice, features, alias.clone(), 0, timestamp, [ka.clone()])
|
||||
.insert(
|
||||
&alice,
|
||||
1,
|
||||
features,
|
||||
alias.clone(),
|
||||
0,
|
||||
&ua,
|
||||
timestamp,
|
||||
[ka.clone()],
|
||||
)
|
||||
.unwrap();
|
||||
assert!(inserted);
|
||||
|
||||
let inserted = cache
|
||||
.insert(&alice, features, alias, 0, timestamp, [ka])
|
||||
.insert(&alice, 1, features, alias, 0, &ua, timestamp, [ka])
|
||||
.unwrap();
|
||||
assert!(!inserted);
|
||||
|
||||
|
|
@ -593,6 +647,8 @@ mod test {
|
|||
let mut cache = Database::memory().unwrap();
|
||||
let timestamp = LocalTime::now().into();
|
||||
let features = node::Features::SEED;
|
||||
let ua1 = UserAgent::default();
|
||||
let ua2 = UserAgent::default();
|
||||
let alias1 = Alias::new("alice");
|
||||
let alias2 = Alias::new("~alice~");
|
||||
let ka = KnownAddress {
|
||||
|
|
@ -604,17 +660,35 @@ mod test {
|
|||
};
|
||||
|
||||
let updated = cache
|
||||
.insert(&alice, features, alias1, 0, timestamp, [ka.clone()])
|
||||
.insert(
|
||||
&alice,
|
||||
1,
|
||||
features,
|
||||
alias1,
|
||||
0,
|
||||
&ua1,
|
||||
timestamp,
|
||||
[ka.clone()],
|
||||
)
|
||||
.unwrap();
|
||||
assert!(updated);
|
||||
|
||||
let updated = cache
|
||||
.insert(&alice, features, alias2.clone(), 0, timestamp, [])
|
||||
.insert(&alice, 1, features, alias2.clone(), 0, &ua1, timestamp, [])
|
||||
.unwrap();
|
||||
assert!(!updated, "Can't update using the same timestamp");
|
||||
|
||||
let updated = cache
|
||||
.insert(&alice, features, alias2.clone(), 0, timestamp - 1, [])
|
||||
.insert(
|
||||
&alice,
|
||||
1,
|
||||
features,
|
||||
alias2.clone(),
|
||||
0,
|
||||
&ua1,
|
||||
timestamp - 1,
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!updated, "Can't update using a smaller timestamp");
|
||||
|
||||
|
|
@ -624,12 +698,30 @@ mod test {
|
|||
assert_eq!(node.pow, 0);
|
||||
|
||||
let updated = cache
|
||||
.insert(&alice, features, alias2.clone(), 0, timestamp + 1, [])
|
||||
.insert(
|
||||
&alice,
|
||||
1,
|
||||
features,
|
||||
alias2.clone(),
|
||||
0,
|
||||
&ua2,
|
||||
timestamp + 1,
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
assert!(updated, "Can update with a larger timestamp");
|
||||
|
||||
let updated = cache
|
||||
.insert(&alice, node::Features::NONE, alias2, 1, timestamp + 2, [])
|
||||
.insert(
|
||||
&alice,
|
||||
1,
|
||||
node::Features::NONE,
|
||||
alias2,
|
||||
1,
|
||||
&ua2,
|
||||
timestamp + 2,
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
assert!(updated);
|
||||
|
||||
|
|
@ -639,6 +731,7 @@ mod test {
|
|||
assert_eq!(node.timestamp, timestamp + 2);
|
||||
assert_eq!(node.pow, 1);
|
||||
assert_eq!(node.addrs, vec![ka]);
|
||||
assert_eq!(node.agent, ua2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -647,6 +740,7 @@ mod test {
|
|||
let bob = arbitrary::gen::<NodeId>(1);
|
||||
let mut cache = Database::memory().unwrap();
|
||||
let timestamp = LocalTime::now().into();
|
||||
let ua = UserAgent::default();
|
||||
let features = node::Features::SEED;
|
||||
let alice_alias = Alias::new("alice");
|
||||
let bob_alias = Alias::new("bob");
|
||||
|
|
@ -666,15 +760,26 @@ mod test {
|
|||
cache
|
||||
.insert(
|
||||
&alice,
|
||||
1,
|
||||
features,
|
||||
alice_alias.clone(),
|
||||
0,
|
||||
&ua,
|
||||
timestamp,
|
||||
[ka.clone()],
|
||||
)
|
||||
.unwrap();
|
||||
cache
|
||||
.insert(&bob, features, bob_alias.clone(), 0, timestamp, [ka])
|
||||
.insert(
|
||||
&bob,
|
||||
1,
|
||||
features,
|
||||
bob_alias.clone(),
|
||||
0,
|
||||
&ua,
|
||||
timestamp,
|
||||
[ka],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
assert_eq!(cache.len().unwrap(), 6);
|
||||
|
|
@ -695,6 +800,7 @@ mod test {
|
|||
let mut cache = Database::memory().unwrap();
|
||||
let mut expected = Vec::new();
|
||||
let timestamp = LocalTime::now().into();
|
||||
let ua = UserAgent::default();
|
||||
let features = node::Features::SEED;
|
||||
let alias = Alias::new("alice");
|
||||
|
||||
|
|
@ -711,11 +817,12 @@ mod test {
|
|||
};
|
||||
expected.push(AddressEntry {
|
||||
node: id,
|
||||
version: 3,
|
||||
penalty: Penalty::default(),
|
||||
address: ka.clone(),
|
||||
});
|
||||
cache
|
||||
.insert(&id, features, alias.clone(), 0, timestamp, [ka])
|
||||
.insert(&id, 3, features, alias.clone(), 0, &ua, timestamp, [ka])
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
|
|
@ -735,9 +842,19 @@ mod test {
|
|||
let mut cache = Database::memory().unwrap();
|
||||
let features = node::Features::SEED;
|
||||
let timestamp = Timestamp::from(LocalTime::now());
|
||||
let ua = UserAgent::default();
|
||||
|
||||
cache
|
||||
.insert(&alice, features, Alias::new("alice"), 16, timestamp, [])
|
||||
.insert(
|
||||
&alice,
|
||||
1,
|
||||
features,
|
||||
Alias::new("alice"),
|
||||
16,
|
||||
&ua,
|
||||
timestamp,
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
let node = cache.get(&alice).unwrap().unwrap();
|
||||
assert_eq!(node.penalty, Penalty::default());
|
||||
|
|
@ -762,6 +879,7 @@ mod test {
|
|||
#[test]
|
||||
fn test_disconnected_ban() {
|
||||
let alice = arbitrary::gen::<NodeId>(1);
|
||||
let ua = UserAgent::default();
|
||||
let ip1: net::Ipv4Addr = [8, 8, 8, 8].into();
|
||||
let ip2: net::Ipv4Addr = [9, 9, 9, 9].into();
|
||||
let ka1 = arbitrary::gen::<KnownAddress>(1);
|
||||
|
|
@ -780,9 +898,11 @@ mod test {
|
|||
|
||||
db.insert(
|
||||
&alice,
|
||||
1,
|
||||
features,
|
||||
Alias::new("alice"),
|
||||
16,
|
||||
&ua,
|
||||
timestamp,
|
||||
[ka1.clone(), ka2.clone()],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ use crate::node;
|
|||
use crate::node::policy::SeedingPolicy;
|
||||
use crate::node::{Address, Alias, NodeId};
|
||||
|
||||
/// Peer-to-peer protocol version.
|
||||
pub type ProtocolVersion = u8;
|
||||
|
||||
/// Default number of workers to spawn.
|
||||
pub const DEFAULT_WORKERS: usize = 8;
|
||||
|
||||
|
|
@ -63,14 +66,14 @@ pub enum Network {
|
|||
|
||||
impl Network {
|
||||
/// Bootstrap nodes for this network.
|
||||
pub fn bootstrap(&self) -> Vec<(Alias, ConnectAddress)> {
|
||||
pub fn bootstrap(&self) -> Vec<(Alias, ProtocolVersion, ConnectAddress)> {
|
||||
match self {
|
||||
Self::Main => [
|
||||
("seed.radicle.garden", seeds::RADICLE_COMMUNITY_NODE.clone()),
|
||||
("seed.radicle.xyz", seeds::RADICLE_TEAM_NODE.clone()),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(a, s)| (Alias::new(a), s))
|
||||
.map(|(a, s)| (Alias::new(a), 1, s))
|
||||
.collect(),
|
||||
|
||||
Self::Test => vec![],
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ use std::{fmt, time};
|
|||
use sqlite as sql;
|
||||
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;
|
||||
|
||||
/// 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/4.sql"),
|
||||
include_str!("db/migrations/5.sql"),
|
||||
include_str!("db/migrations/6.sql"),
|
||||
];
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
|
|
@ -131,15 +134,18 @@ impl Database {
|
|||
node: &NodeId,
|
||||
features: Features,
|
||||
alias: Alias,
|
||||
agent: &UserAgent,
|
||||
timestamp: Timestamp,
|
||||
addrs: impl IntoIterator<Item = &'a Address>,
|
||||
) -> Result<Self, Error> {
|
||||
address::Store::insert(
|
||||
&mut self,
|
||||
node,
|
||||
PROTOCOL_VERSION,
|
||||
features,
|
||||
alias,
|
||||
0,
|
||||
agent,
|
||||
timestamp,
|
||||
addrs
|
||||
.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::{
|
||||
notifications, policy,
|
||||
policy::{Policy, Scope, SeedingPolicy},
|
||||
Alias, AliasStore, Handle as _, Node,
|
||||
Alias, AliasStore, Handle as _, Node, UserAgent,
|
||||
};
|
||||
use crate::prelude::{Did, NodeId, RepoId};
|
||||
use crate::storage::git::transport;
|
||||
|
|
@ -319,6 +319,7 @@ impl Profile {
|
|||
&public_key,
|
||||
config.node.features(),
|
||||
config.node.alias.clone(),
|
||||
&UserAgent::default(),
|
||||
LocalTime::now().into(),
|
||||
config.node.external_addresses.iter(),
|
||||
)?;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use sqlite::Value;
|
|||
|
||||
use crate::identity::RepoId;
|
||||
use crate::node;
|
||||
use crate::node::Address;
|
||||
use crate::node::{Address, UserAgent};
|
||||
|
||||
/// Run an SQL query inside a transaction.
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
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::refs::{Refs, RefsAt, SignedRefs};
|
||||
use crate::test::storage::{MockRepository, MockStorage};
|
||||
|
|
@ -324,3 +324,12 @@ impl Arbitrary for Timestamp {
|
|||
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