Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
node: implement node announcement sybil resistance
Alexis Sellier committed 3 years ago
commit 7b7e83fbc75acb7e49315ace7df3278b0eba5dea
parent 3c900f106f9a1bcbe14fb2830bf66cc39c6c7ab7
6 files changed +172 -12
modified Cargo.lock
@@ -256,6 +256,16 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -374,6 +384,7 @@ checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c"
dependencies = [
 "block-buffer 0.10.3",
 "crypto-common",
+
 "subtle",
]

[[package]]
@@ -573,6 +584,15 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -679,6 +699,15 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -953,6 +982,15 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1178,6 +1216,7 @@ dependencies = [
 "quickcheck",
 "quickcheck_macros",
 "radicle",
+
 "scrypt",
 "serde",
 "serde_json",
 "sqlite",
@@ -1287,6 +1326,27 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1438,6 +1498,12 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
modified 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" }
modified 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<Id>) -> InventoryAnnouncement {
modified radicle-node/src/service/message.rs
@@ -133,6 +133,52 @@ pub struct NodeAnnouncement {
    pub alias: [u8; 32],
    /// Announced addresses.
    pub addresses: Vec<Address>,
+
    /// 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,
+
            &params,
+
            &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::<Address>::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());
+
    }
}
modified 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);
modified 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(),
        )
    }