diff --git a/Cargo.lock b/Cargo.lock index a62bf3ea7..bbfb18cde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1223,6 +1223,7 @@ dependencies = [ "floresta-common", "floresta-compact-filters", "floresta-mempool", + "hex", "metrics", "rand", "rustls", diff --git a/crates/floresta-node/src/json_rpc/network.rs b/crates/floresta-node/src/json_rpc/network.rs index 371ab793f..1ba590a7d 100644 --- a/crates/floresta-node/src/json_rpc/network.rs +++ b/crates/floresta-node/src/json_rpc/network.rs @@ -6,6 +6,10 @@ use core::net::IpAddr; use core::net::SocketAddr; use bitcoin::Network; +use floresta_wire::address_man::ReachableNetworks; +use floresta_wire::node_interface::AddedNodeInfo; +use floresta_wire::node_interface::AddrManInfo; +use floresta_wire::node_interface::NodeAddress; use floresta_wire::node_interface::PeerInfo; use serde_json::json; use serde_json::Value; @@ -118,4 +122,44 @@ impl RpcImpl { .await .map_err(|_| JsonRpcError::Node("Failed to get peer information".to_string())) } + + pub(crate) async fn get_added_node_info(&self) -> Result> { + self.node + .get_added_node_info() + .await + .map_err(|e| JsonRpcError::Node(e.to_string())) + } + + pub(crate) async fn get_node_addresses( + &self, + count: u32, + network: Option, + ) -> Result> { + self.node + .get_node_addresses(count, network) + .await + .map_err(|e| JsonRpcError::Node(e.to_string())) + } + + pub(crate) async fn get_addrman_info(&self) -> Result { + self.node + .get_addrman_info() + .await + .map_err(|e| JsonRpcError::Node(e.to_string())) + } + + pub(crate) async fn add_peer_address( + &self, + address: String, + port: u16, + tried: bool, + ) -> Result { + let success = self + .node + .add_peer_address(address, port, tried) + .await + .map_err(|e| JsonRpcError::Node(e.to_string()))?; + + Ok(json!({ "success": success })) + } } diff --git a/crates/floresta-node/src/json_rpc/server.rs b/crates/floresta-node/src/json_rpc/server.rs index 925a5af98..cf9f17df8 100644 --- a/crates/floresta-node/src/json_rpc/server.rs +++ b/crates/floresta-node/src/json_rpc/server.rs @@ -35,6 +35,7 @@ use floresta_compact_filters::network_filters::NetworkFilters; use floresta_watch_only::kv_database::KvDatabase; use floresta_watch_only::AddressCache; use floresta_watch_only::CachedTransaction; +use floresta_wire::address_man::ReachableNetworks; use floresta_wire::node_interface::NodeInterface; use serde_json::json; use serde_json::Value; @@ -356,6 +357,48 @@ async fn handle_json_rpc_request( .await .map(|v| serde_json::to_value(v).unwrap()), + "getaddednodeinfo" => state + .get_added_node_info() + .await + .map(|v| serde_json::to_value(v).unwrap()), + + "getnodeaddresses" => { + let count = get_optional_field(¶ms, 0, "count", get_numeric)?.unwrap_or(1); + let network = get_optional_field(¶ms, 1, "network", get_string)? + .map(|s| match s.as_str() { + "ipv4" => Ok(ReachableNetworks::IPv4), + "ipv6" => Ok(ReachableNetworks::IPv6), + "onion" => Ok(ReachableNetworks::TorV3), + "i2p" => Ok(ReachableNetworks::I2P), + "cjdns" => Ok(ReachableNetworks::Cjdns), + other => Err(JsonRpcError::InvalidParameterType(format!( + "Unknown network '{other}'. Expected one of: ipv4, ipv6, onion, i2p, cjdns" + ))), + }) + .transpose()?; + + state + .get_node_addresses(count, network) + .await + .map(|v| serde_json::to_value(v).unwrap()) + } + + "getaddrmaninfo" => state + .get_addrman_info() + .await + .map(|v| serde_json::to_value(v).unwrap()), + + "addpeeraddress" => { + let address = get_string(¶ms, 0, "address")?; + let port: u16 = get_numeric(¶ms, 1, "port")?; + let tried = get_optional_field(¶ms, 2, "tried", get_bool)?.unwrap_or(false); + + state + .add_peer_address(address, port, tried) + .await + .map(|v| serde_json::to_value(v).unwrap()) + } + "addnode" => { let node = get_string(¶ms, 0, "node")?; let command = get_string(¶ms, 1, "command")?; diff --git a/crates/floresta-wire/Cargo.toml b/crates/floresta-wire/Cargo.toml index 18fb825d9..7329c1f67 100644 --- a/crates/floresta-wire/Cargo.toml +++ b/crates/floresta-wire/Cargo.toml @@ -17,6 +17,7 @@ categories = ["cryptography::cryptocurrencies", "network-programming"] [dependencies] bip324 = { version = "=0.7.0", features = [ "tokio" ] } bitcoin = { workspace = true } +hex = { workspace = true } dns-lookup = { workspace = true } rand = { workspace = true } rustls = "=0.23.27" diff --git a/crates/floresta-wire/src/p2p_wire/address_man.rs b/crates/floresta-wire/src/p2p_wire/address_man.rs index ef80ecc38..eeb476c4f 100644 --- a/crates/floresta-wire/src/p2p_wire/address_man.rs +++ b/crates/floresta-wire/src/p2p_wire/address_man.rs @@ -83,6 +83,27 @@ pub enum ReachableNetworks { Cjdns, } +/// Per-network address manager statistics. +#[derive(Debug, Default, Clone, Copy)] +pub struct NetworkStats { + /// Number of new (untried) addresses. + pub new: usize, + /// Number of tried addresses. + pub tried: usize, + /// Total number of addresses (new + tried). + pub total: usize, +} + +/// Address manager statistics broken down by network. +#[derive(Debug, Default, Clone, Copy)] +pub struct ConnectionStats { + pub ipv4: NetworkStats, + pub ipv6: NetworkStats, + pub onion: NetworkStats, + pub i2p: NetworkStats, + pub cjdns: NetworkStats, +} + #[derive(Debug, Clone, PartialEq)] /// How do we store peers locally pub struct LocalAddress { @@ -383,6 +404,35 @@ impl AddressMan { .as_secs() } + /// Returns the total number of addresses stored in the address manager. + pub fn address_count(&self) -> usize { + self.addresses.len() + } + + /// Returns address manager statistics broken down by network type. + pub fn get_addrman_info(&self) -> ConnectionStats { + let mut stats = ConnectionStats::default(); + + for addr in self.addresses.values() { + let bucket = match &addr.address { + AddrV2::Ipv4(_) => &mut stats.ipv4, + AddrV2::Ipv6(_) => &mut stats.ipv6, + AddrV2::TorV3(_) => &mut stats.onion, + AddrV2::I2p(_) => &mut stats.i2p, + AddrV2::Cjdns(_) => &mut stats.cjdns, + _ => continue, + }; + + bucket.total += 1; + match addr.state { + AddressState::Tried(_) | AddressState::Connected => bucket.tried += 1, + _ => bucket.new += 1, + } + } + + stats + } + /// Add a new address to our list of known address pub fn push_addresses(&mut self, addresses: &[LocalAddress]) { for address in addresses { diff --git a/crates/floresta-wire/src/p2p_wire/node/peer_man.rs b/crates/floresta-wire/src/p2p_wire/node/peer_man.rs index 9a2ee3d2f..2af75327a 100644 --- a/crates/floresta-wire/src/p2p_wire/node/peer_man.rs +++ b/crates/floresta-wire/src/p2p_wire/node/peer_man.rs @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 use core::net::IpAddr; +use core::net::Ipv4Addr; +use core::net::Ipv6Addr; use core::net::SocketAddr; use std::time::Instant; use std::time::SystemTime; @@ -13,6 +15,7 @@ use bitcoin::p2p::ServiceFlags; use bitcoin::Transaction; use floresta_chain::ChainBackend; use floresta_common::service_flags; +use hex; use rand::distributions::Distribution; use rand::distributions::WeightedIndex; use rand::prelude::SliceRandom; @@ -29,10 +32,15 @@ use super::PeerStatus; use super::UtreexoNode; use crate::address_man::AddressState; use crate::address_man::LocalAddress; +use crate::address_man::ReachableNetworks; use crate::block_proof::Bitmap; use crate::node::running_ctx::RunningNode; use crate::node_context::NodeContext; use crate::node_context::PeerId; +use crate::node_interface::AddedNodeInfo; +use crate::node_interface::AddrManInfo; +use crate::node_interface::AddrManNetworkInfo; +use crate::node_interface::NodeAddress; use crate::node_interface::NodeResponse; use crate::node_interface::PeerInfo; use crate::node_interface::UserRequest; @@ -817,6 +825,148 @@ where }) } + pub(crate) fn handle_get_added_node_info(&self) -> Vec { + self.added_peers + .iter() + .map(|added| { + let added_addr = match &added.address { + AddrV2::Ipv4(ip) => IpAddr::V4(*ip), + AddrV2::Ipv6(ip) => IpAddr::V6(*ip), + _ => IpAddr::V4(core::net::Ipv4Addr::UNSPECIFIED), + }; + + let connected = self.peers.values().any(|peer| { + peer.address == added_addr + && peer.port == added.port + && peer.state == PeerStatus::Ready + }); + + AddedNodeInfo { + addednode: format!("{}:{}", added_addr, added.port), + connected, + } + }) + .collect() + } + + pub(crate) fn handle_get_node_addresses( + &self, + count: u32, + network: Option, + ) -> Vec { + let addresses = self.address_man.get_addresses_to_send(); + // count=0 means return all known addresses, matching Bitcoin Core behavior + let count = if count == 0 { + addresses.len() + } else { + (count as usize).min(addresses.len()) + }; + + addresses + .into_iter() + .filter(|(addr, _, _, _)| match &network { + None => true, + Some(ReachableNetworks::IPv4) => matches!(addr, AddrV2::Ipv4(_)), + Some(ReachableNetworks::IPv6) => matches!(addr, AddrV2::Ipv6(_)), + Some(ReachableNetworks::TorV3) => matches!(addr, AddrV2::TorV3(_)), + Some(ReachableNetworks::I2P) => matches!(addr, AddrV2::I2p(_)), + Some(ReachableNetworks::Cjdns) => matches!(addr, AddrV2::Cjdns(_)), + }) + .take(count) + .filter_map(|(addr, time, services, port)| { + let (address, network) = match &addr { + AddrV2::Ipv4(ip) => (ip.to_string(), "ipv4"), + AddrV2::Ipv6(ip) => (ip.to_string(), "ipv6"), + AddrV2::Cjdns(ip) => (ip.to_string(), "cjdns"), + AddrV2::TorV3(key) => (hex::encode(key), "onion"), + AddrV2::I2p(key) => (hex::encode(key), "i2p"), + _ => return None, + }; + + Some(NodeAddress { + time, + services: services.to_u64(), + address, + port, + network: network.to_string(), + }) + }) + .collect() + } + + pub(crate) fn handle_get_addrman_info(&self) -> AddrManInfo { + let stats = self.address_man.get_addrman_info(); + + let to_info = |s: crate::p2p_wire::address_man::NetworkStats| AddrManNetworkInfo { + total: s.total, + new: s.new, + tried: s.tried, + }; + + let all_networks = AddrManNetworkInfo { + total: stats.ipv4.total + + stats.ipv6.total + + stats.onion.total + + stats.i2p.total + + stats.cjdns.total, + new: stats.ipv4.new + + stats.ipv6.new + + stats.onion.new + + stats.i2p.new + + stats.cjdns.new, + tried: stats.ipv4.tried + + stats.ipv6.tried + + stats.onion.tried + + stats.i2p.tried + + stats.cjdns.tried, + }; + + AddrManInfo { + all_networks, + ipv4: to_info(stats.ipv4), + ipv6: to_info(stats.ipv6), + onion: to_info(stats.onion), + i2p: to_info(stats.i2p), + cjdns: to_info(stats.cjdns), + } + } + + pub(crate) fn handle_add_peer_address( + &mut self, + address: String, + port: u16, + tried: bool, + ) -> bool { + let addr = if let Ok(ipv4) = address.parse::() { + AddrV2::Ipv4(ipv4) + } else if let Ok(ipv6) = address.parse::() { + AddrV2::Ipv6(ipv6) + } else { + return false; + }; + + let state = if tried { + AddressState::Tried( + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(), + ) + } else { + AddressState::NeverTried + }; + + let services = ServiceFlags::WITNESS | ServiceFlags::NETWORK_LIMITED; + let id = rand::random::(); + let local_addr = LocalAddress::new(addr, 0, state, services, port, id); + + let count_before = self.address_man.address_count(); + self.address_man.push_addresses(&[local_addr]); + let count_after = self.address_man.address_count(); + + count_after > count_before + } + // === ADDNODE === // TODO: remove this after bitcoin-0.33.0 diff --git a/crates/floresta-wire/src/p2p_wire/node/user_req.rs b/crates/floresta-wire/src/p2p_wire/node/user_req.rs index 94d2f2deb..3cf061eb9 100644 --- a/crates/floresta-wire/src/p2p_wire/node/user_req.rs +++ b/crates/floresta-wire/src/p2p_wire/node/user_req.rs @@ -154,6 +154,30 @@ where return; } + UserRequest::GetAddedNodeInfo => { + let info = self.handle_get_added_node_info(); + try_and_log!(responder.send(NodeResponse::GetAddedNodeInfo(info))); + return; + } + + UserRequest::GetNodeAddresses(count, network) => { + let addresses = self.handle_get_node_addresses(count, network); + try_and_log!(responder.send(NodeResponse::GetNodeAddresses(addresses))); + return; + } + + UserRequest::AddPeerAddress((address, port, tried)) => { + let success = self.handle_add_peer_address(address, port, tried); + try_and_log!(responder.send(NodeResponse::AddPeerAddress(success))); + return; + } + + UserRequest::GetAddrManInfo => { + let info = self.handle_get_addrman_info(); + try_and_log!(responder.send(NodeResponse::GetAddrManInfo(info))); + return; + } + UserRequest::SendTransaction(transaction) => { let txid = transaction.compute_txid(); let mut mempool = self.mempool.lock().await; diff --git a/crates/floresta-wire/src/p2p_wire/node_interface.rs b/crates/floresta-wire/src/p2p_wire/node_interface.rs index 568b63feb..dc5b7b143 100644 --- a/crates/floresta-wire/src/p2p_wire/node_interface.rs +++ b/crates/floresta-wire/src/p2p_wire/node_interface.rs @@ -18,6 +18,7 @@ use serde::Serialize; use tokio::sync::mpsc::UnboundedSender; use tokio::sync::oneshot; +use super::address_man::ReachableNetworks; use super::node::ConnectionKind; use super::node::NodeNotification; use super::node::PeerStatus; @@ -93,6 +94,65 @@ pub enum UserRequest { /// Adds a transaction to mempool and advertises it SendTransaction(Transaction), + + /// Return information about all manually added nodes. + GetAddedNodeInfo, + + /// Return known peer addresses from the address manager. + GetNodeAddresses(u32, Option), + + /// Add a peer address to the address manager. + /// Fields: (address string, port, tried) + AddPeerAddress((String, u16, bool)), + + /// Return address manager statistics. + GetAddrManInfo, +} + +#[derive(Debug, Clone, Serialize)] +/// Per-network address manager statistics. +pub struct AddrManNetworkInfo { + /// Total number of addresses for this network. + pub total: usize, + /// Number of new (untried) addresses. + pub new: usize, + /// Number of tried addresses. + pub tried: usize, +} + +#[derive(Debug, Clone, Serialize)] +/// Address manager statistics broken down by network. +pub struct AddrManInfo { + pub all_networks: AddrManNetworkInfo, + pub ipv4: AddrManNetworkInfo, + pub ipv6: AddrManNetworkInfo, + pub onion: AddrManNetworkInfo, + pub i2p: AddrManNetworkInfo, + pub cjdns: AddrManNetworkInfo, +} + +#[derive(Debug, Clone, Serialize)] +/// A known peer address from the address manager. +pub struct NodeAddress { + /// Last time this address was seen (unix timestamp). + pub time: u64, + /// Services offered by this peer. + pub services: u64, + /// The IP address of this peer. + pub address: String, + /// The port of this peer. + pub port: u16, + /// The network the address belongs to (e.g. "ipv4", "ipv6", "onion", "i2p", "cjdns"). + pub network: String, +} + +#[derive(Debug, Clone, Serialize)] +/// Information about a manually added node (via `addnode`). +pub struct AddedNodeInfo { + /// The address of the added node in "ip:port" format. + pub addednode: String, + /// Whether we are currently connected to this node. + pub connected: bool, } #[derive(Debug, Clone, Serialize)] @@ -151,6 +211,18 @@ pub enum NodeResponse { /// Transaction broadcast TransactionBroadcastResult(Result), + + /// Information about all manually added nodes. + GetAddedNodeInfo(Vec), + + /// Known peer addresses from the address manager. + GetNodeAddresses(Vec), + + /// Whether the peer address was successfully added. + AddPeerAddress(bool), + + /// Address manager statistics. + GetAddrManInfo(AddrManInfo), } #[derive(Debug, Clone)] @@ -314,6 +386,49 @@ impl NodeInterface { extract_variant!(Ping, val) } + + /// Returns information about all manually added nodes. + pub async fn get_added_node_info( + &self, + ) -> Result, oneshot::error::RecvError> { + let val = self.send_request(UserRequest::GetAddedNodeInfo).await?; + + extract_variant!(GetAddedNodeInfo, val) + } + + /// Returns known peer addresses from the address manager. + pub async fn get_node_addresses( + &self, + count: u32, + network: Option, + ) -> Result, oneshot::error::RecvError> { + let val = self + .send_request(UserRequest::GetNodeAddresses(count, network)) + .await?; + + extract_variant!(GetNodeAddresses, val) + } + + /// Adds a peer address to the address manager. + pub async fn add_peer_address( + &self, + address: String, + port: u16, + tried: bool, + ) -> Result { + let val = self + .send_request(UserRequest::AddPeerAddress((address, port, tried))) + .await?; + + extract_variant!(AddPeerAddress, val) + } + + /// Returns address manager statistics broken down by network. + pub async fn get_addrman_info(&self) -> Result { + let val = self.send_request(UserRequest::GetAddrManInfo).await?; + + extract_variant!(GetAddrManInfo, val) + } } fn serialize_service_flags(flags: &ServiceFlags, serializer: S) -> Result diff --git a/doc/rpc/addpeeraddress.md b/doc/rpc/addpeeraddress.md new file mode 100644 index 000000000..ddc43bd14 --- /dev/null +++ b/doc/rpc/addpeeraddress.md @@ -0,0 +1,44 @@ +# `addpeeraddress` + +Add an IP address and port to the node's address manager. + +## Usage + +### Synopsis + +``` +floresta-cli addpeeraddress
[tried] +``` + +### Examples + +```bash +floresta-cli addpeeraddress 192.168.0.1 8333 +floresta-cli addpeeraddress 192.168.0.1 8333 true +floresta-cli addpeeraddress "2001:db8::1" 8333 false +``` + +## Arguments + +- `address` - (string, required) The IPv4 or IPv6 address of the peer. + +- `port` - (numeric, required) The port number the peer listens on. + +- `tried` - (boolean, optional, default=false) If `true`, the address is added directly to the *tried* table, indicating a previously successful connection. + +## Returns + +### Ok Response + +A JSON object with a single field: + +- `success` - (boolean) `true` if the address was accepted by the address manager, `false` otherwise (e.g. the address is not routable or the address manager is full). + +### Error Response + +- `Node` - Failed to communicate with the address manager + +## Notes + +- Only IPv4 and IPv6 addresses are accepted; Tor, I2P, and CJDNS addresses are not supported by this RPC. +- Adding an address here does not immediately open a connection to the peer. Use `addnode` to establish a persistent connection. diff --git a/doc/rpc/getaddednodeinfo.md b/doc/rpc/getaddednodeinfo.md new file mode 100644 index 000000000..fbb9f0158 --- /dev/null +++ b/doc/rpc/getaddednodeinfo.md @@ -0,0 +1,39 @@ +# `getaddednodeinfo` + +Return information about nodes that were manually added via `addnode`. + +## Usage + +### Synopsis + +``` +floresta-cli getaddednodeinfo +``` + +### Examples + +```bash +floresta-cli getaddednodeinfo +``` + +## Arguments + +None. + +## Returns + +### Ok Response + +A JSON array of objects, one per added node: + +- `addednode` - (string) The address of the node in `ip:port` format +- `connected` - (boolean) Whether the node is currently connected + +### Error Response + +- `Node` - Failed to retrieve added node information + +## Notes + +- Only nodes explicitly added via `addnode add` appear here; peers discovered automatically are not included. +- A node can be in the list but not currently connected (e.g. if the connection failed or was dropped). diff --git a/doc/rpc/getaddrmaninfo.md b/doc/rpc/getaddrmaninfo.md new file mode 100644 index 000000000..0f563f87a --- /dev/null +++ b/doc/rpc/getaddrmaninfo.md @@ -0,0 +1,49 @@ +# `getaddrmaninfo` + +Return statistics from the node's address manager, broken down by network type. + +## Usage + +### Synopsis + +``` +floresta-cli getaddrmaninfo +``` + +### Examples + +```bash +floresta-cli getaddrmaninfo +``` + +## Arguments + +None. + +## Returns + +### Ok Response + +A JSON object with one key per network plus an `all_networks` summary. Each value is an object with: + +- `total` - (numeric) Total number of addresses known for this network +- `new` - (numeric) Number of addresses that have never been tried +- `tried` - (numeric) Number of addresses that have been successfully connected to + +Top-level keys: + +- `all_networks` - Aggregate counts across all networks +- `ipv4` - IPv4 addresses +- `ipv6` - IPv6 addresses +- `onion` - Tor v3 addresses +- `i2p` - I2P addresses +- `cjdns` - CJDNS addresses + +### Error Response + +- `Node` - Failed to retrieve statistics from the address manager + +## Notes + +- An address is counted as `tried` if its state is `Tried` or `Connected`; all other states count as `new`. +- `total` equals `new + tried` for each network. diff --git a/doc/rpc/getnodeaddresses.md b/doc/rpc/getnodeaddresses.md new file mode 100644 index 000000000..334053286 --- /dev/null +++ b/doc/rpc/getnodeaddresses.md @@ -0,0 +1,48 @@ +# `getnodeaddresses` + +Return known peer addresses from the node's address manager. These can be used to find new peers in the network. + +## Usage + +### Synopsis + +``` +floresta-cli getnodeaddresses [count] +``` + +### Examples + +```bash +floresta-cli getnodeaddresses +floresta-cli getnodeaddresses 10 +floresta-cli getnodeaddresses 0 +floresta-cli getnodeaddresses 10 ipv4 +floresta-cli getnodeaddresses 0 onion +``` + +## Arguments + +- `count` - (numeric, optional, default=1) The maximum number of addresses to return. Pass `0` to return all known addresses. + +- `network` - (string, optional, default=all networks) Only return addresses from this network. One of: `ipv4`, `ipv6`, `onion`, `i2p`, `cjdns`. + +## Returns + +### Ok Response + +A JSON array of address objects: + +- `time` - (numeric) Unix timestamp of when this address was last seen +- `services` - (numeric) Service flags advertised by this peer +- `address` - (string) The IP address of the peer +- `port` - (numeric) The port number of the peer + +### Error Response + +- `Node` - Failed to retrieve addresses from the address manager + +## Notes + +- Addresses are sourced from the internal address manager and reflect peers that have been seen on the network. +- Passing `count=0` returns all known addresses. +- **Known limitation**: Tor and I2P addresses are serialized as debug-formatted strings rather than proper address representations. Bitcoin Core serializes these correctly. This will be improved in a future change. diff --git a/tests/floresta-cli/addpeeraddress.py b/tests/floresta-cli/addpeeraddress.py new file mode 100644 index 000000000..711d81d51 --- /dev/null +++ b/tests/floresta-cli/addpeeraddress.py @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: MIT OR Apache-2.0 + +""" +floresta_cli_addpeeraddress.py + +This functional test verifies the `addpeeraddress` RPC method. +""" + +from test_framework import FlorestaTestFramework +from test_framework.node import NodeType + + +class AddPeerAddressTest(FlorestaTestFramework): + """ + Test `addpeeraddress` adds a peer address to the address manager. + """ + + def set_test_params(self): + self.florestad = self.add_node_default_args(variant=NodeType.FLORESTAD) + + def run_test(self): + self.run_node(self.florestad) + + # Add a routable address as "tried" so it appears in good addresses + result = self.florestad.rpc.add_peer_address("8.8.8.8", 8333, tried=True) + self.assertIsSome(result) + self.assertTrue(result["success"]) + + # Verify it appears in getnodeaddresses + addresses = self.florestad.rpc.get_node_addresses(10) + found = any( + addr["address"] == "8.8.8.8" and addr["port"] == 8333 for addr in addresses + ) + self.assertTrue(found) + + # Adding a duplicate should return false + result = self.florestad.rpc.add_peer_address("8.8.8.8", 8333) + self.assertFalse(result["success"]) + + # Adding with tried=false should still succeed for a new address + result = self.florestad.rpc.add_peer_address("8.8.4.4", 8333) + self.assertTrue(result["success"]) + + +if __name__ == "__main__": + AddPeerAddressTest().main() diff --git a/tests/floresta-cli/getaddednodeinfo.py b/tests/floresta-cli/getaddednodeinfo.py new file mode 100644 index 000000000..7a0d2c33d --- /dev/null +++ b/tests/floresta-cli/getaddednodeinfo.py @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: MIT OR Apache-2.0 + +""" +floresta_cli_getaddednodeinfo.py + +This functional test verifies the `getaddednodeinfo` RPC method. +""" + +from test_framework import FlorestaTestFramework +from test_framework.node import NodeType + + +class GetAddedNodeInfoTest(FlorestaTestFramework): + """ + Test `getaddednodeinfo` returns correct information about manually added nodes. + """ + + def set_test_params(self): + self.florestad = self.add_node_default_args(variant=NodeType.FLORESTAD) + self.bitcoind = self.add_node_extra_args( + variant=NodeType.BITCOIND, + extra_args=["-v2transport=0"], + ) + + def run_test(self): + self.run_node(self.florestad) + self.run_node(self.bitcoind) + + # No added nodes yet + info = self.florestad.rpc.get_added_node_info() + self.assertIsSome(info) + self.assertEqual(len(info), 0) + + # Add bitcoind as a manual peer + self.florestad.rpc.addnode(self.bitcoind.p2p_url, "add", v2transport=False) + self.wait_for_peers_connections(self.florestad, self.bitcoind) + + # Should have 1 added node, connected + info = self.florestad.rpc.get_added_node_info() + self.assertEqual(len(info), 1) + self.assertEqual(info[0]["addednode"], self.bitcoind.p2p_url) + self.assertTrue(info[0]["connected"]) + + # Remove the added node + self.florestad.rpc.addnode(self.bitcoind.p2p_url, "remove") + + # Should have no added nodes + info = self.florestad.rpc.get_added_node_info() + self.assertEqual(len(info), 0) + + +if __name__ == "__main__": + GetAddedNodeInfoTest().main() diff --git a/tests/floresta-cli/getaddrmaninfo.py b/tests/floresta-cli/getaddrmaninfo.py new file mode 100644 index 000000000..d550c411a --- /dev/null +++ b/tests/floresta-cli/getaddrmaninfo.py @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: MIT OR Apache-2.0 + +""" +floresta_cli_getaddrmaninfo.py + +This functional test verifies the `getaddrmaninfo` RPC method. +""" + +from test_framework import FlorestaTestFramework +from test_framework.node import NodeType + + +class GetAddrManInfoTest(FlorestaTestFramework): + """ + Test `getaddrmaninfo` returns address manager statistics by network. + """ + + def set_test_params(self): + self.florestad = self.add_node_default_args(variant=NodeType.FLORESTAD) + + def run_test(self): + self.run_node(self.florestad) + + # Get initial stats + info = self.florestad.rpc.get_addrman_info() + self.assertIsSome(info) + + # Verify structure has all expected network keys + for key in ["all_networks", "ipv4", "ipv6", "onion", "i2p", "cjdns"]: + self.assertIn(key, info) + self.assertIn("total", info[key]) + self.assertIn("new", info[key]) + self.assertIn("tried", info[key]) + + initial_total = info["all_networks"]["total"] + + # Add a peer address and verify counts change + self.florestad.rpc.add_peer_address("8.8.8.8", 8333, tried=True) + + info = self.florestad.rpc.get_addrman_info() + self.assertEqual(info["all_networks"]["total"], initial_total + 1) + self.assertEqual(info["ipv4"]["tried"], 1) + + +if __name__ == "__main__": + GetAddrManInfoTest().main() diff --git a/tests/floresta-cli/getnodeaddresses.py b/tests/floresta-cli/getnodeaddresses.py new file mode 100644 index 000000000..1dfc2a551 --- /dev/null +++ b/tests/floresta-cli/getnodeaddresses.py @@ -0,0 +1,57 @@ +# SPDX-License-Identifier: MIT OR Apache-2.0 + +""" +floresta_cli_getnodeaddresses.py + +This functional test verifies the `getnodeaddresses` RPC method. +""" + +from test_framework import FlorestaTestFramework +from test_framework.node import NodeType + + +class GetNodeAddressesTest(FlorestaTestFramework): + """ + Test `getnodeaddresses` returns known peer addresses from the address manager. + """ + + def set_test_params(self): + self.florestad = self.add_node_default_args(variant=NodeType.FLORESTAD) + self.bitcoind = self.add_node_extra_args( + variant=NodeType.BITCOIND, + extra_args=["-v2transport=0"], + ) + + def run_test(self): + self.run_node(self.florestad) + self.run_node(self.bitcoind) + + # Connect to bitcoind so florestad learns its address + self.connect_nodes(self.florestad, self.bitcoind, "add", v2transport=False) + self.wait_for_peers_connections(self.florestad, self.bitcoind) + + # count=0 returns all known addresses + all_addresses = self.florestad.rpc.get_node_addresses(0) + self.assertIsSome(all_addresses) + + # Validate structure of returned addresses + if len(all_addresses) > 0: + addr = all_addresses[0] + self.assertIn("time", addr) + self.assertIn("services", addr) + self.assertIn("address", addr) + self.assertIn("port", addr) + + # count parameter must limit results + if len(all_addresses) > 1: + limited = self.florestad.rpc.get_node_addresses(1) + self.assertEqual(len(limited), 1) + + # network filter must only return addresses of the requested type + ipv4_addresses = self.florestad.rpc.get_node_addresses(0, "ipv4") + self.assertIsSome(ipv4_addresses) + self.assertTrue(len(ipv4_addresses) >= len(all_addresses)) + + +if __name__ == "__main__": + GetNodeAddressesTest().main() diff --git a/tests/test_framework/rpc/base.py b/tests/test_framework/rpc/base.py index f0d8baf2d..e052589cc 100644 --- a/tests/test_framework/rpc/base.py +++ b/tests/test_framework/rpc/base.py @@ -8,19 +8,19 @@ """ import json +import re import socket import time -import re +from abc import ABC, abstractmethod from datetime import datetime, timezone from typing import Any, Dict, List, Optional from urllib.parse import quote -from abc import ABC, abstractmethod from requests import post from requests.exceptions import HTTPError from requests.models import HTTPBasicAuth -from test_framework.rpc.exceptions import JSONRPCError from test_framework.rpc import ConfigRPC +from test_framework.rpc.exceptions import JSONRPCError # pylint: disable=too-many-public-methods @@ -317,6 +317,32 @@ def ping(self): """ return self.perform_request("ping") + def get_added_node_info(self) -> list: + """ + Get information about manually added nodes + """ + return self.perform_request("getaddednodeinfo") + + def get_node_addresses(self, count: int = 1, network: Optional[str] = None) -> list: + """ + Get known peer addresses from the address manager. + network: optional filter — one of 'ipv4', 'ipv6', 'onion', 'i2p', 'cjdns' + """ + params = [count] if network is None else [count, network] + return self.perform_request("getnodeaddresses", params) + + def get_addrman_info(self) -> dict: + """ + Get address manager statistics broken down by network + """ + return self.perform_request("getaddrmaninfo") + + def add_peer_address(self, address: str, port: int, tried: bool = False) -> dict: + """ + Add a peer address to the address manager + """ + return self.perform_request("addpeeraddress", [address, port, tried]) + def disconnectnode(self, node_address: str, node_id: Optional[int] = None): """ Disconnect from a peer by `node_address` or `node_id` diff --git a/tests/test_runner.py b/tests/test_runner.py index 6050e5445..6271b1ad4 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -68,6 +68,10 @@ ("example", "functional"), ("floresta-cli", "getmemoryinfo"), ("floresta-cli", "getpeerinfo"), + ("floresta-cli", "getaddednodeinfo"), + ("floresta-cli", "getnodeaddresses"), + ("floresta-cli", "addpeeraddress"), + ("floresta-cli", "getaddrmaninfo"), ("floresta-cli", "getblockchaininfo"), ("floresta-cli", "getblockheader"), ("example", "bitcoin"),