node: implement node announcement sybil resistance

We implement a small proof-of-work on the node announcement message
to prevent the routing table from being filled by fake entries.

Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
Alexis Sellier 2022-10-30 10:21:32 +01:00
parent 3c900f106f
commit 7b7e83fbc7
No known key found for this signature in database
6 changed files with 172 additions and 12 deletions

66
Cargo.lock generated
View File

@ -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"

View File

@ -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" }

View File

@ -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 {

View File

@ -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());
}
}

View File

@ -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);

View File

@ -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(),
)
}