diff --git a/.gitignore b/.gitignore index fa8d85a..b6012ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,25 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# This project does not check in Cargo.lock Cargo.lock -target + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# Generated by Intellij-based IDEs. +.idea + +# Generated by MacOS +.DS_Store + +# VSCode +.vscode + +# Rust bug report +rustc-ice-* diff --git a/Cargo.toml b/Cargo.toml index ca9bf1d..be9d55d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "enr" authors = ["Age Manning "] edition = "2021" -version = "0.12.0" +version = "0.12.1" description = "Rust implementation of Ethereum Node Record (ENR) EIP778" readme = "./README.md" keywords = ["ethereum", "enr", "record", "EIP778", "node"] @@ -23,13 +23,13 @@ sha3 = "0.10" k256 = { version = "0.13", features = ["ecdsa"], optional = true } serde = { version = "1.0", features = ["derive"], optional = true } ed25519-dalek = { version = "2.1.1", optional = true, features = ["rand_core"] } -secp256k1 = { version = "0.28", optional = true, default-features = false, features = [ +secp256k1 = { version = "0.29", optional = true, default-features = false, features = [ "global-context", ] } [dev-dependencies] alloy-rlp = { version = "0.3.4", features = ["derive"] } -secp256k1 = { version = "0.28", features = ["rand-std"] } +secp256k1 = { version = "0.29", features = ["rand-std"] } serde_json = "1.0" [features] diff --git a/README.md b/README.md index d13f76b..e993444 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ let mut enr = Enr::builder().ip4(ip).tcp4(8000).build(&key).unwrap(); enr.set_tcp4(8001, &key); // set a custom key -enr.insert("custom_key", &vec![0,0,1], &key); +enr.insert("custom_key", &[0,0,1], &key); // encode to base64 let base_64_string = enr.to_base64(); @@ -128,29 +128,27 @@ let decoded_enr: DefaultEnr = base_64_string.parse().unwrap(); assert_eq!(decoded_enr.ip4(), Some("192.168.0.1".parse().unwrap())); assert_eq!(decoded_enr.id(), Some("v4".into())); assert_eq!(decoded_enr.tcp4(), Some(8001)); -assert_eq!(decoded_enr.get("custom_key"), Some(vec![0,0,1].as_slice())); +assert_eq!(decoded_enr.get("custom_key").as_ref().map(AsRef::as_ref), Some(vec![0,0,1]).as_deref()); ``` #### Encoding/Decoding ENR's of various key types ```rust -use enr::{k256::ecdsa::SigningKey, Enr, ed25519_dalek::Keypair, CombinedKey}; +use enr::{ed25519_dalek as ed25519, k256::ecdsa, CombinedKey, Enr}; use std::net::Ipv4Addr; use rand::thread_rng; -use rand::Rng; // generate a random secp256k1 key let mut rng = thread_rng(); -let key = SigningKey::random(&mut rng); -let ip = Ipv4Addr::new(192,168,0,1); +let key = ecdsa::SigningKey::random(&mut rng); +let ip = Ipv4Addr::new(192, 168, 0, 1); let enr_secp256k1 = Enr::builder().ip4(ip).tcp4(8000).build(&key).unwrap(); // encode to base64 let base64_string_secp256k1 = enr_secp256k1.to_base64(); // generate a random ed25519 key -let mut rng = rand_07::thread_rng(); -let key = Keypair::generate(&mut rng); +let key = ed25519::SigningKey::generate(&mut rng); let enr_ed25519 = Enr::builder().ip4(ip).tcp4(8000).build(&key).unwrap(); // encode to base64 @@ -158,9 +156,9 @@ let base64_string_ed25519 = enr_ed25519.to_base64(); // decode base64 strings of varying key types // decode the secp256k1 with default Enr -let decoded_enr_secp256k1: Enr = base64_string_secp256k1.parse().unwrap(); +let decoded_enr_secp256k1: Enr = base64_string_secp256k1.parse().unwrap(); // decode ed25519 ENRs -let decoded_enr_ed25519: Enr = base64_string_ed25519.parse().unwrap(); +let decoded_enr_ed25519: Enr = base64_string_ed25519.parse().unwrap(); // use the combined key to be able to decode either let decoded_enr: Enr = base64_string_secp256k1.parse().unwrap(); diff --git a/src/builder.rs b/src/builder.rs index 8d2dfbf..9d25b79 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -48,7 +48,7 @@ impl Builder { /// Adds an arbitrary key-value to the `ENRBuilder`. pub fn add_value(&mut self, key: impl AsRef<[u8]>, value: &T) -> &mut Self { - let mut out = BytesMut::new(); + let mut out = BytesMut::with_capacity(value.length()); value.encode(&mut out); self.add_value_rlp(key, out.freeze()) } @@ -114,6 +114,22 @@ impl Builder { self } + /// Adds a [EIP-7636](https://eips.ethereum.org/EIPS/eip-7636) `client` field to the `ENRBuilder`. + pub fn client_info( + &mut self, + name: String, + version: String, + build: Option, + ) -> &mut Self { + if build.is_none() { + self.add_value("client", &vec![name, version]); + } else { + self.add_value("client", &vec![name, version, build.unwrap()]); + } + + self + } + /// Generates the rlp-encoded form of the ENR specified by the builder config. fn rlp_content(&self) -> BytesMut { let mut list = Vec::::with_capacity(MAX_ENR_SIZE); @@ -128,7 +144,7 @@ impl Builder { list: true, payload_length: list.len(), }; - let mut out = BytesMut::new(); + let mut out = BytesMut::with_capacity(header.length() + list.len()); header.encode(&mut out); out.extend_from_slice(&list); out @@ -165,7 +181,7 @@ impl Builder { Header::decode(&mut value.as_ref())?; } - let mut id_bytes = BytesMut::with_capacity(3); + let mut id_bytes = BytesMut::with_capacity(self.id.length()); self.id.as_bytes().encode(&mut id_bytes); self.add_value_rlp(ID_ENR_KEY, id_bytes.freeze()); diff --git a/src/lib.rs b/src/lib.rs index 125266b..3096090 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -132,10 +132,9 @@ //! //! ```rust //! # #[cfg(feature = "ed25519")] { -//! use enr::{k256::ecdsa, Enr, ed25519_dalek as ed25519, CombinedKey}; +//! use enr::{ed25519_dalek as ed25519, k256::ecdsa, CombinedKey, Enr}; //! use std::net::Ipv4Addr; //! use rand::thread_rng; -//! use rand::Rng; //! //! // generate a random secp256k1 key //! let mut rng = thread_rng(); @@ -155,9 +154,9 @@ //! //! // decode base64 strings of varying key types //! // decode the secp256k1 with default Enr -//! let decoded_enr_secp256k1: Enr = base64_string_secp256k1.parse().unwrap(); +//! let decoded_enr_secp256k1: Enr = base64_string_secp256k1.parse().unwrap(); //! // decode ed25519 ENRs -//! let decoded_enr_ed25519: Enr = base64_string_ed25519.parse().unwrap(); +//! let decoded_enr_ed25519: Enr = base64_string_ed25519.parse().unwrap(); //! //! // use the combined key to be able to decode either //! let decoded_enr: Enr = base64_string_secp256k1.parse().unwrap(); @@ -352,6 +351,36 @@ impl Enr { None } + /// Returns [EIP-7636](https://eips.ethereum.org/EIPS/eip-7636) entry if it is defined. + #[must_use] + pub fn client_info(&self) -> Option<(String, String, Option)> { + if let Some(Ok(client_list)) = self.get_decodable::>("client") { + match client_list.len() { + 2 => { + let client_name = String::from_utf8_lossy(client_list[0].as_ref()).to_string(); + + let client_version = + String::from_utf8_lossy(client_list[1].as_ref()).to_string(); + + return Some((client_name, client_version, None)); + } + 3 => { + let client_name = String::from_utf8_lossy(client_list[0].as_ref()).to_string(); + + let client_version = + String::from_utf8_lossy(client_list[1].as_ref()).to_string(); + + let client_additional = + String::from_utf8_lossy(client_list[2].as_ref()).to_string(); + return Some((client_name, client_version, Some(client_additional))); + } + _ => {} + } + } + + None + } + /// The TCP port of ENR record if it is defined. #[must_use] pub fn tcp4(&self) -> Option { @@ -649,6 +678,23 @@ impl Enr { self.remove_key(TCP6_ENR_KEY, key) } + /// Sets the [EIP-7636](https://eips.ethereum.org/EIPS/eip-7636) `client` field in the record. + pub fn set_client_info( + &mut self, + name: String, + version: String, + build: Option, + key: &K, + ) -> Result<(), Error> { + if build.is_none() { + self.insert("client", &vec![name, version], key)?; + } else { + self.insert("client", &vec![name, version, build.unwrap()], key)?; + } + + Ok(()) + } + /// Sets the IP and UDP port in a single update with a single increment in sequence number. pub fn set_udp_socket(&mut self, socket: SocketAddr, key: &K) -> Result<(), Error> { self.set_socket(socket, key, false) @@ -872,7 +918,7 @@ impl Enr { /// Sets a new public key for the record. pub fn set_public_key(&mut self, public_key: &K::PublicKey, key: &K) -> Result<(), Error> { - self.insert(&public_key.enr_key(), &public_key.encode().as_ref(), key) + self.insert(public_key.enr_key(), &public_key.encode().as_ref(), key) .map(|_| {}) } @@ -1134,12 +1180,19 @@ impl Decodable for Enr { _ => { let other_header = Header::decode(payload)?; let value = &payload[..other_header.payload_length]; - // Preserve the valid encoding payload.advance(other_header.payload_length); - let mut out = Vec::::new(); - other_header.encode(&mut out); - out.extend_from_slice(value); - out + + // Encode the header for list values, for non-list objects, we remove the + // header for compatibility with commonly used key entries (i.e it's the + // current convention). + if other_header.list { + let mut out = Vec::::new(); + other_header.encode(&mut out); + out.extend_from_slice(value); + out + } else { + alloy_rlp::encode(value) + } } }; content.insert(key.to_vec(), Bytes::from(value)); @@ -2058,4 +2111,90 @@ mod tests { record.set_seq(30, &key).unwrap(); assert_eq!(record.seq(), 30); } + + #[test] + fn test_set_client_eip7636() { + let key = k256::ecdsa::SigningKey::random(&mut rand::thread_rng()); + let mut enr = Enr::empty(&key).unwrap(); + + enr.set_client_info( + "Test".to_string(), + "v1.0.0".to_string(), + Some("Test".to_string()), + &key, + ) + .unwrap(); + assert!(enr.verify()); + + enr.set_client_info("Test".to_string(), "v1.0.0".to_string(), None, &key) + .unwrap(); + assert!(enr.verify()); + } + + #[test] + fn test_get_eip7636() { + let example_eip = "enr:-MO4QBn4OF-y-dqULg4WOIlc8gQAt-arldNFe0_YQ4HNX28jDtg41xjDyKfCXGfZaPN97I-MCfogeK91TyqmWTpb0_AChmNsaWVudNqKTmV0aGVybWluZIYxLjkuNTOHN2ZjYjU2N4JpZIJ2NIJpcIR_AAABg2lwNpAAAAAAAAAAAAAAAAAAAAABiXNlY3AyNTZrMaECn-TTdCwfZP4XgJyq8Lxoj-SgEoIFgDLVBEUqQk4HnAqDdWRwgiMshHVkcDaCIyw"; + let enr = example_eip.parse::().unwrap(); + + let info = enr.client_info().unwrap(); + + assert_eq!(info.0, "Nethermind"); + assert_eq!(info.1, "1.9.53"); + assert_eq!(info.2.unwrap(), "7fcb567"); + + let key = k256::ecdsa::SigningKey::random(&mut rand::thread_rng()); + let mut enr = Enr::empty(&key).unwrap(); + + enr.set_client_info("Test".to_string(), "v1.0.0".to_string(), None, &key) + .unwrap(); + + let info = enr.client_info().unwrap(); + assert_eq!(info.0, "Test"); + assert_eq!(info.1, "v1.0.0"); + assert_eq!(info.2, None); + } + + #[test] + fn test_builder_eip7636() { + let key = k256::ecdsa::SigningKey::random(&mut rand::thread_rng()); + let enr = Enr::builder() + .ip4(Ipv4Addr::new(127, 0, 0, 1)) + .tcp4(30303) + .client_info( + "Test".to_string(), + "v1.0.0".to_string(), + Some("Test".to_string()), + ) + .build(&key) + .unwrap(); + + let info = enr.client_info().unwrap(); + assert_eq!(info.0, "Test"); + assert_eq!(info.1, "v1.0.0"); + assert_eq!(info.2.unwrap(), "Test"); + + let enr = Enr::builder() + .ip4(Ipv4Addr::new(127, 0, 0, 1)) + .tcp4(30303) + .client_info("Test".to_string(), "v1.0.0".to_string(), None) + .build(&key) + .unwrap(); + + let info = enr.client_info().unwrap(); + assert_eq!(info.0, "Test"); + assert_eq!(info.1, "v1.0.0"); + assert_eq!(info.2, None); + } + /// Tests a common ENR which uses RLP encoded values without the header + #[test] + fn test_common_rlp_convention() { + const COMMON_VALID_ENR: &str = concat!( + "-LW4QCAyOCtqvQjd8AgpqbaCgfjy8oN8cBBRT5jtzarkGJQWZx1eN70EM0QafVCugLa-Bv493DPNzflagqfTOsWSF78Ih2F0d", + "G5ldHOIAGAAAAAAAACEZXRoMpBqlaGpBAAAAP__________gmlkgnY0hHF1aWOCIymJc2VjcDI1NmsxoQPg_HgqXzwRIK39Oy", + "lGdC30YUFwsfXvATnGUvEZ6MtBQIhzeW5jbmV0cwCDdGNwgiMo" + ); + + // Expect this to be able to be decoded + let _decoded: DefaultEnr = COMMON_VALID_ENR.parse().unwrap(); + } }