From f953bc0f9a425d9062811e761574287918231625 Mon Sep 17 00:00:00 2001 From: jaoleal Date: Fri, 20 Mar 2026 17:24:55 -0300 Subject: [PATCH 1/4] feat(rpc): implement getaddednodeinfo RPC method Add getaddednodeinfo RPC that returns information about manually added nodes, including whether each is currently connected. Cross-references added_peers with active peer connections. Includes integration test and Python RPC wrapper. --- crates/floresta-node/src/json_rpc/network.rs | 8 +++ crates/floresta-node/src/json_rpc/server.rs | 5 ++ .../src/p2p_wire/node/peer_man.rs | 25 +++++++++ .../src/p2p_wire/node/user_req.rs | 6 +++ .../src/p2p_wire/node_interface.rs | 24 +++++++++ doc/rpc/getaddednodeinfo.md | 39 ++++++++++++++ tests/floresta-cli/getaddednodeinfo.py | 53 +++++++++++++++++++ tests/test_framework/rpc/base.py | 6 +++ tests/test_runner.py | 1 + 9 files changed, 167 insertions(+) create mode 100644 doc/rpc/getaddednodeinfo.md create mode 100644 tests/floresta-cli/getaddednodeinfo.py diff --git a/crates/floresta-node/src/json_rpc/network.rs b/crates/floresta-node/src/json_rpc/network.rs index 371ab793f..84d076b62 100644 --- a/crates/floresta-node/src/json_rpc/network.rs +++ b/crates/floresta-node/src/json_rpc/network.rs @@ -6,6 +6,7 @@ use core::net::IpAddr; use core::net::SocketAddr; use bitcoin::Network; +use floresta_wire::node_interface::AddedNodeInfo; use floresta_wire::node_interface::PeerInfo; use serde_json::json; use serde_json::Value; @@ -118,4 +119,11 @@ 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())) + } } diff --git a/crates/floresta-node/src/json_rpc/server.rs b/crates/floresta-node/src/json_rpc/server.rs index 925a5af98..57ef8efc8 100644 --- a/crates/floresta-node/src/json_rpc/server.rs +++ b/crates/floresta-node/src/json_rpc/server.rs @@ -356,6 +356,11 @@ 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()), + "addnode" => { let node = get_string(¶ms, 0, "node")?; let command = get_string(¶ms, 1, "command")?; 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..012b6e503 100644 --- a/crates/floresta-wire/src/p2p_wire/node/peer_man.rs +++ b/crates/floresta-wire/src/p2p_wire/node/peer_man.rs @@ -33,6 +33,7 @@ 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::NodeResponse; use crate::node_interface::PeerInfo; use crate::node_interface::UserRequest; @@ -817,6 +818,30 @@ 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() + } + // === 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..265c0b4c5 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,12 @@ where return; } + UserRequest::GetAddedNodeInfo => { + let info = self.handle_get_added_node_info(); + try_and_log!(responder.send(NodeResponse::GetAddedNodeInfo(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..b52892227 100644 --- a/crates/floresta-wire/src/p2p_wire/node_interface.rs +++ b/crates/floresta-wire/src/p2p_wire/node_interface.rs @@ -93,6 +93,18 @@ pub enum UserRequest { /// Adds a transaction to mempool and advertises it SendTransaction(Transaction), + + /// Return information about all manually added nodes. + GetAddedNodeInfo, +} + +#[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 +163,9 @@ pub enum NodeResponse { /// Transaction broadcast TransactionBroadcastResult(Result), + + /// Information about all manually added nodes. + GetAddedNodeInfo(Vec), } #[derive(Debug, Clone)] @@ -314,6 +329,15 @@ 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) + } } fn serialize_service_flags(flags: &ServiceFlags, serializer: S) -> Result 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/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/test_framework/rpc/base.py b/tests/test_framework/rpc/base.py index f0d8baf2d..f4801b349 100644 --- a/tests/test_framework/rpc/base.py +++ b/tests/test_framework/rpc/base.py @@ -317,6 +317,12 @@ 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 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..505013949 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -68,6 +68,7 @@ ("example", "functional"), ("floresta-cli", "getmemoryinfo"), ("floresta-cli", "getpeerinfo"), + ("floresta-cli", "getaddednodeinfo"), ("floresta-cli", "getblockchaininfo"), ("floresta-cli", "getblockheader"), ("example", "bitcoin"), From 5235a9e40d001d911a608fa3bce2c6f4d3f77ff6 Mon Sep 17 00:00:00 2001 From: jaoleal Date: Wed, 25 Mar 2026 19:34:30 -0300 Subject: [PATCH 2/4] feat(rpc): implement addpeeraddress RPC method Add addpeeraddress RPC that manually adds a peer address to the address manager. Accepts address, port, and optional tried parameter. Constructs a LocalAddress with WITNESS|NETWORK_LIMITED services and pushes it to AddressMan. Also adds address_count() helper to AddressMan. Includes integration test and Python RPC wrapper. --- crates/floresta-node/src/json_rpc/network.rs | 15 ++++++ crates/floresta-node/src/json_rpc/server.rs | 11 +++++ .../floresta-wire/src/p2p_wire/address_man.rs | 5 ++ .../src/p2p_wire/node/peer_man.rs | 36 +++++++++++++++ .../src/p2p_wire/node/user_req.rs | 6 +++ .../src/p2p_wire/node_interface.rs | 21 +++++++++ doc/rpc/addpeeraddress.md | 44 ++++++++++++++++++ tests/floresta-cli/addpeeraddress.py | 46 +++++++++++++++++++ tests/test_framework/rpc/base.py | 12 +++-- tests/test_runner.py | 1 + 10 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 doc/rpc/addpeeraddress.md create mode 100644 tests/floresta-cli/addpeeraddress.py diff --git a/crates/floresta-node/src/json_rpc/network.rs b/crates/floresta-node/src/json_rpc/network.rs index 84d076b62..b446bffd2 100644 --- a/crates/floresta-node/src/json_rpc/network.rs +++ b/crates/floresta-node/src/json_rpc/network.rs @@ -126,4 +126,19 @@ impl RpcImpl { .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 57ef8efc8..f35ccf579 100644 --- a/crates/floresta-node/src/json_rpc/server.rs +++ b/crates/floresta-node/src/json_rpc/server.rs @@ -361,6 +361,17 @@ async fn handle_json_rpc_request( .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/src/p2p_wire/address_man.rs b/crates/floresta-wire/src/p2p_wire/address_man.rs index ef80ecc38..dac1d6210 100644 --- a/crates/floresta-wire/src/p2p_wire/address_man.rs +++ b/crates/floresta-wire/src/p2p_wire/address_man.rs @@ -383,6 +383,11 @@ impl AddressMan { .as_secs() } + /// Returns the total number of addresses stored in the address manager. + pub fn address_count(&self) -> usize { + self.addresses.len() + } + /// 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 012b6e503..0cf26bf3d 100644 --- a/crates/floresta-wire/src/p2p_wire/node/peer_man.rs +++ b/crates/floresta-wire/src/p2p_wire/node/peer_man.rs @@ -842,6 +842,42 @@ where .collect() } + 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 265c0b4c5..b5f930a57 100644 --- a/crates/floresta-wire/src/p2p_wire/node/user_req.rs +++ b/crates/floresta-wire/src/p2p_wire/node/user_req.rs @@ -160,6 +160,12 @@ where 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::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 b52892227..7c2efe986 100644 --- a/crates/floresta-wire/src/p2p_wire/node_interface.rs +++ b/crates/floresta-wire/src/p2p_wire/node_interface.rs @@ -96,6 +96,10 @@ pub enum UserRequest { /// Return information about all manually added nodes. GetAddedNodeInfo, + + /// Add a peer address to the address manager. + /// Fields: (address string, port, tried) + AddPeerAddress((String, u16, bool)), } #[derive(Debug, Clone, Serialize)] @@ -166,6 +170,9 @@ pub enum NodeResponse { /// Information about all manually added nodes. GetAddedNodeInfo(Vec), + + /// Whether the peer address was successfully added. + AddPeerAddress(bool), } #[derive(Debug, Clone)] @@ -338,6 +345,20 @@ impl NodeInterface { extract_variant!(GetAddedNodeInfo, 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) + } } 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/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/test_framework/rpc/base.py b/tests/test_framework/rpc/base.py index f4801b349..f7c9b3870 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 @@ -323,6 +323,12 @@ def get_added_node_info(self) -> list: """ return self.perform_request("getaddednodeinfo") + 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 505013949..a8e11061d 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -69,6 +69,7 @@ ("floresta-cli", "getmemoryinfo"), ("floresta-cli", "getpeerinfo"), ("floresta-cli", "getaddednodeinfo"), + ("floresta-cli", "addpeeraddress"), ("floresta-cli", "getblockchaininfo"), ("floresta-cli", "getblockheader"), ("example", "bitcoin"), From 7c828973e667d47e534188e2d70bf47eba154fcc Mon Sep 17 00:00:00 2001 From: jaoleal Date: Wed, 25 Mar 2026 19:34:44 -0300 Subject: [PATCH 3/4] feat(rpc): implement getnodeaddresses RPC method Add getnodeaddresses RPC that returns known peer addresses from the address manager. Accepts an optional count parameter and optional network filter (ipv4, ipv6, onion, i2p, cjdns). Includes integration test and Python RPC wrapper. --- Cargo.lock | 1 + crates/floresta-node/src/json_rpc/network.rs | 13 +++++ crates/floresta-node/src/json_rpc/server.rs | 22 +++++++ crates/floresta-wire/Cargo.toml | 1 + .../src/p2p_wire/node/peer_man.rs | 50 ++++++++++++++++ .../src/p2p_wire/node/user_req.rs | 6 ++ .../src/p2p_wire/node_interface.rs | 35 ++++++++++++ doc/rpc/getnodeaddresses.md | 48 ++++++++++++++++ tests/floresta-cli/getnodeaddresses.py | 57 +++++++++++++++++++ tests/test_framework/rpc/base.py | 8 +++ tests/test_runner.py | 1 + 11 files changed, 242 insertions(+) create mode 100644 doc/rpc/getnodeaddresses.md create mode 100644 tests/floresta-cli/getnodeaddresses.py 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 b446bffd2..ee7619faf 100644 --- a/crates/floresta-node/src/json_rpc/network.rs +++ b/crates/floresta-node/src/json_rpc/network.rs @@ -6,7 +6,9 @@ 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::NodeAddress; use floresta_wire::node_interface::PeerInfo; use serde_json::json; use serde_json::Value; @@ -127,6 +129,17 @@ impl RpcImpl { .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 add_peer_address( &self, address: String, diff --git a/crates/floresta-node/src/json_rpc/server.rs b/crates/floresta-node/src/json_rpc/server.rs index f35ccf579..f91b924f4 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; @@ -361,6 +362,27 @@ async fn handle_json_rpc_request( .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()) + } + "addpeeraddress" => { let address = get_string(¶ms, 0, "address")?; let port: u16 = get_numeric(¶ms, 1, "port")?; 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/node/peer_man.rs b/crates/floresta-wire/src/p2p_wire/node/peer_man.rs index 0cf26bf3d..a6d7b07f9 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,11 +32,13 @@ 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::NodeAddress; use crate::node_interface::NodeResponse; use crate::node_interface::PeerInfo; use crate::node_interface::UserRequest; @@ -842,6 +847,51 @@ where .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_add_peer_address( &mut self, address: String, 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 b5f930a57..3d560524b 100644 --- a/crates/floresta-wire/src/p2p_wire/node/user_req.rs +++ b/crates/floresta-wire/src/p2p_wire/node/user_req.rs @@ -160,6 +160,12 @@ where 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))); diff --git a/crates/floresta-wire/src/p2p_wire/node_interface.rs b/crates/floresta-wire/src/p2p_wire/node_interface.rs index 7c2efe986..88432359c 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; @@ -97,11 +98,29 @@ pub enum UserRequest { /// 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)), } +#[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 { @@ -171,6 +190,9 @@ pub enum NodeResponse { /// 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), } @@ -346,6 +368,19 @@ impl NodeInterface { 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, 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/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 f7c9b3870..2133d4d90 100644 --- a/tests/test_framework/rpc/base.py +++ b/tests/test_framework/rpc/base.py @@ -323,6 +323,14 @@ def get_added_node_info(self) -> list: """ 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 add_peer_address(self, address: str, port: int, tried: bool = False) -> dict: """ Add a peer address to the address manager diff --git a/tests/test_runner.py b/tests/test_runner.py index a8e11061d..d9ea038e4 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -69,6 +69,7 @@ ("floresta-cli", "getmemoryinfo"), ("floresta-cli", "getpeerinfo"), ("floresta-cli", "getaddednodeinfo"), + ("floresta-cli", "getnodeaddresses"), ("floresta-cli", "addpeeraddress"), ("floresta-cli", "getblockchaininfo"), ("floresta-cli", "getblockheader"), From 9f84eb43f91e9c7c6d1683fed37351a75d40d606 Mon Sep 17 00:00:00 2001 From: jaoleal Date: Wed, 25 Mar 2026 19:34:52 -0300 Subject: [PATCH 4/4] feat(rpc): implement getaddrmaninfo RPC method Add getaddrmaninfo RPC that returns address manager statistics broken down by network type (ipv4, ipv6, cjdns). Each network reports total, new (untried), and tried address counts. Adds get_addrman_info() aggregation method to AddressMan. Includes integration test and Python RPC wrapper. --- crates/floresta-node/src/json_rpc/network.rs | 8 +++ crates/floresta-node/src/json_rpc/server.rs | 5 ++ .../floresta-wire/src/p2p_wire/address_man.rs | 45 +++++++++++++++++ .../src/p2p_wire/node/peer_man.rs | 39 +++++++++++++++ .../src/p2p_wire/node/user_req.rs | 6 +++ .../src/p2p_wire/node_interface.rs | 35 +++++++++++++ doc/rpc/getaddrmaninfo.md | 49 +++++++++++++++++++ tests/floresta-cli/getaddrmaninfo.py | 46 +++++++++++++++++ tests/test_framework/rpc/base.py | 6 +++ tests/test_runner.py | 1 + 10 files changed, 240 insertions(+) create mode 100644 doc/rpc/getaddrmaninfo.md create mode 100644 tests/floresta-cli/getaddrmaninfo.py diff --git a/crates/floresta-node/src/json_rpc/network.rs b/crates/floresta-node/src/json_rpc/network.rs index ee7619faf..1ba590a7d 100644 --- a/crates/floresta-node/src/json_rpc/network.rs +++ b/crates/floresta-node/src/json_rpc/network.rs @@ -8,6 +8,7 @@ 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; @@ -140,6 +141,13 @@ impl RpcImpl { .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, diff --git a/crates/floresta-node/src/json_rpc/server.rs b/crates/floresta-node/src/json_rpc/server.rs index f91b924f4..cf9f17df8 100644 --- a/crates/floresta-node/src/json_rpc/server.rs +++ b/crates/floresta-node/src/json_rpc/server.rs @@ -383,6 +383,11 @@ async fn handle_json_rpc_request( .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")?; diff --git a/crates/floresta-wire/src/p2p_wire/address_man.rs b/crates/floresta-wire/src/p2p_wire/address_man.rs index dac1d6210..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 { @@ -388,6 +409,30 @@ impl AddressMan { 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 a6d7b07f9..2af75327a 100644 --- a/crates/floresta-wire/src/p2p_wire/node/peer_man.rs +++ b/crates/floresta-wire/src/p2p_wire/node/peer_man.rs @@ -38,6 +38,8 @@ 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; @@ -892,6 +894,43 @@ where .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, 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 3d560524b..3cf061eb9 100644 --- a/crates/floresta-wire/src/p2p_wire/node/user_req.rs +++ b/crates/floresta-wire/src/p2p_wire/node/user_req.rs @@ -172,6 +172,12 @@ where 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 88432359c..dc5b7b143 100644 --- a/crates/floresta-wire/src/p2p_wire/node_interface.rs +++ b/crates/floresta-wire/src/p2p_wire/node_interface.rs @@ -104,6 +104,31 @@ pub enum UserRequest { /// 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)] @@ -195,6 +220,9 @@ pub enum NodeResponse { /// Whether the peer address was successfully added. AddPeerAddress(bool), + + /// Address manager statistics. + GetAddrManInfo(AddrManInfo), } #[derive(Debug, Clone)] @@ -394,6 +422,13 @@ impl NodeInterface { 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/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/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/test_framework/rpc/base.py b/tests/test_framework/rpc/base.py index 2133d4d90..e052589cc 100644 --- a/tests/test_framework/rpc/base.py +++ b/tests/test_framework/rpc/base.py @@ -331,6 +331,12 @@ def get_node_addresses(self, count: int = 1, network: Optional[str] = None) -> l 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 diff --git a/tests/test_runner.py b/tests/test_runner.py index d9ea038e4..6271b1ad4 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -71,6 +71,7 @@ ("floresta-cli", "getaddednodeinfo"), ("floresta-cli", "getnodeaddresses"), ("floresta-cli", "addpeeraddress"), + ("floresta-cli", "getaddrmaninfo"), ("floresta-cli", "getblockchaininfo"), ("floresta-cli", "getblockheader"), ("example", "bitcoin"),