diff --git a/Cargo.lock b/Cargo.lock index 7ab33f88..ba65e93b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -255,6 +255,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "cipher" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "colored" version = "1.9.3" @@ -374,6 +384,7 @@ checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" dependencies = [ "block-buffer 0.10.3", "crypto-common", + "subtle", ] [[package]] @@ -572,6 +583,15 @@ dependencies = [ "libc", ] +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.5", +] + [[package]] name = "home" version = "0.5.3" @@ -678,6 +698,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.12" @@ -952,6 +981,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.5", +] + [[package]] name = "percent-encoding" version = "2.2.0" @@ -1178,6 +1216,7 @@ dependencies = [ "quickcheck", "quickcheck_macros", "radicle", + "scrypt", "serde", "serde_json", "sqlite", @@ -1286,6 +1325,27 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + +[[package]] +name = "scrypt" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f9e24d2b632954ded8ab2ef9fea0a0c769ea56ea98bddbafbad22caeeadf45d" +dependencies = [ + "hmac", + "pbkdf2", + "salsa20", + "sha2 0.10.6", +] + [[package]] name = "serde" version = "1.0.145" @@ -1437,6 +1497,12 @@ dependencies = [ "sqlite3-src", ] +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "syn" version = "1.0.101" diff --git a/radicle-node/Cargo.toml b/radicle-node/Cargo.toml index d3f58219..1958a283 100644 --- a/radicle-node/Cargo.toml +++ b/radicle-node/Cargo.toml @@ -21,6 +21,7 @@ nakamoto-net-poll = { version = "0.3.0" } nonempty = { version = "0.8.0", features = ["serialize"] } sqlite = { version = "0.27.0" } sqlite3-src = { version = "0.4.0", features = ["bundled"] } # Ensures static linking +scrypt = { version = "0.10.0", default-features = false } serde = { version = "1", features = ["derive"] } serde_json = { version = "1", features = ["preserve_order"] } tempfile = { version = "3.3.0" } diff --git a/radicle-node/src/service.rs b/radicle-node/src/service.rs index 358ee6c5..3df70402 100644 --- a/radicle-node/src/service.rs +++ b/radicle-node/src/service.rs @@ -687,23 +687,24 @@ where } } } - AnnouncementMessage::Node(NodeAnnouncement { - features, - alias, - addresses, - .. - }) => { + AnnouncementMessage::Node( + ann @ NodeAnnouncement { + features, + alias, + addresses, + .. + }, + ) => { // Discard node messages we've already seen, otherwise update - // out last seen time. + // our last seen time. if !peer.node_announced(timestamp) { debug!("Ignoring stale node announcement from {announcer}"); return Ok(false); } - // If this node isn't a seed, we're not interested in adding it - // to our address book, but other nodes may be, so we relay the message anyway. - if !features.has(Features::SEED) { - return Ok(relay); + if !ann.validate() { + warn!("Dropping node announcement from {announcer}: invalid proof-of-work"); + return Ok(false); } let alias = match str::from_utf8(alias) { @@ -714,6 +715,12 @@ where } }; + // If this node isn't a seed, we're not interested in adding it + // to our address book, but other nodes may be, so we relay the message anyway. + if !features.has(Features::SEED) { + return Ok(relay); + } + match self.addresses.insert( announcer, *features, @@ -1223,7 +1230,9 @@ mod gossip { timestamp, alias, addresses, + nonce: 0, } + .solve() } pub fn inventory(timestamp: Timestamp, inventory: Vec) -> InventoryAnnouncement { diff --git a/radicle-node/src/service/message.rs b/radicle-node/src/service/message.rs index f7ca7056..af7e888c 100644 --- a/radicle-node/src/service/message.rs +++ b/radicle-node/src/service/message.rs @@ -133,6 +133,52 @@ pub struct NodeAnnouncement { pub alias: [u8; 32], /// Announced addresses. pub addresses: Vec
, + /// Nonce used for announcement proof-of-work. + pub nonce: u64, +} + +impl NodeAnnouncement { + /// Validate a node announcement message. + /// + /// Checks that the proof-of-work is valid, by generating a single byte that + /// must be zero. + /// + /// `scrypt(encode(announcement)) == 0` + /// + pub fn validate(&self) -> bool { + let (n, r, p) = Announcement::POW_PARAMS; + let params = scrypt::Params::new(n, r, p).expect("proof-of-work parameters are valid"); + let mut output = [0; 1]; + + scrypt::scrypt( + wire::serialize(self).as_ref(), + Announcement::POW_SALT, + ¶ms, + &mut output, + ) + .expect("proof-of-work output vector is a valid length"); + + output == [0] + } + + /// Solve the proof-of-work of a node announcement by iterating through different nonces. + pub fn solve(mut self) -> Self { + loop { + if let Some(nonce) = self.nonce.checked_add(1) { + self.nonce = nonce; + + if self.validate() { + break; + } + } else { + // If a very high difficulty is chosen, it's possible to iterate through all + // possible values of the nonce without solving the puzzle. However, with "normal" + // values, this is virtually impossible. + panic!("could not solve proof-of-work!"); + } + } + self + } } impl wire::Encode for NodeAnnouncement { @@ -143,6 +189,7 @@ impl wire::Encode for NodeAnnouncement { n += self.timestamp.encode(writer)?; n += self.alias.encode(writer)?; n += self.addresses.as_slice().encode(writer)?; + n += self.nonce.encode(writer)?; Ok(n) } @@ -154,12 +201,14 @@ impl wire::Decode for NodeAnnouncement { let timestamp = Timestamp::decode(reader)?; let alias = wire::Decode::decode(reader)?; let addresses = Vec::
::decode(reader)?; + let nonce = u64::decode(reader)?; Ok(Self { features, timestamp, alias, addresses, + nonce, }) } } @@ -271,6 +320,24 @@ pub struct Announcement { } impl Announcement { + /// Proof-of-work parameters for announcements. + /// + /// These parameters are fed into `scrypt`. + /// They represent the `log2(N)`, `r`, `p` parameters, respectively. + /// + /// * log2(N) – iterations count (affects memory and CPU usage), e.g. 15 + /// * r – block size (affects memory and CPU usage), e.g. 8 + /// * p – parallelism factor (threads to run in parallel - affects the memory, CPU usage), usually 1 + /// + /// `15, 8, 1` are usually the recommended parameters. + /// + #[cfg(test)] + pub const POW_PARAMS: (u8, u32, u32) = (1, 1, 1); + #[cfg(not(test))] + pub const POW_PARAMS: (u8, u32, u32) = (15, 8, 1); + /// Salt used for generating PoW. + pub const POW_SALT: &[u8] = &[b'r', b'a', b'd']; + /// Verify this announcement's signature. pub fn verify(&self) -> bool { let msg = wire::serialize(&self.message); @@ -447,4 +514,18 @@ mod tests { assert!(ann.verify()); } + + #[test] + fn test_node_announcement_validate() { + let ann = NodeAnnouncement { + features: node::Features::SEED, + timestamp: 42491841, + alias: [0; 32], + addresses: vec![], + nonce: 0, + }; + + assert!(!ann.validate()); + assert!(ann.solve().validate()); + } } diff --git a/radicle-node/src/test/arbitrary.rs b/radicle-node/src/test/arbitrary.rs index dfc37081..c140522b 100644 --- a/radicle-node/src/test/arbitrary.rs +++ b/radicle-node/src/test/arbitrary.rs @@ -69,6 +69,7 @@ impl Arbitrary for Message { timestamp: Timestamp::arbitrary(g), alias: ByteArray::<32>::arbitrary(g).into_inner(), addresses: Arbitrary::arbitrary(g), + nonce: u64::arbitrary(g), } .into(); let bytes: ByteArray<64> = Arbitrary::arbitrary(g); diff --git a/radicle-node/src/test/peer.rs b/radicle-node/src/test/peer.rs index c5689194..cfbe55f7 100644 --- a/radicle-node/src/test/peer.rs +++ b/radicle-node/src/test/peer.rs @@ -164,7 +164,9 @@ where timestamp: self.timestamp(), alias, addresses: vec![net::SocketAddr::from((self.ip, service::DEFAULT_PORT)).into()], - }, + nonce: 0, + } + .solve(), self.signer(), ) }