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:
parent
3c900f106f
commit
7b7e83fbc7
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -687,23 +687,24 @@ where
|
|||
}
|
||||
}
|
||||
}
|
||||
AnnouncementMessage::Node(NodeAnnouncement {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
¶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::<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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue