From 495c6d554e64ce60eda2a461da3e9093775eb40e Mon Sep 17 00:00:00 2001 From: Shallom Micah Bawa Date: Fri, 27 Mar 2026 00:47:06 +0100 Subject: [PATCH] rpc: implement getaddednodeinfo Wire through the full RPC stack to expose information about manually added peers (via addnode add), matching Bitcoin Core's getaddednodeinfo behavior. Layers touched: - UserRequest/NodeResponse enums and NodeInterface method - Handler in perform_user_request with filter_map logic - FlorestaRPC trait + JsonRPCClient impl - Server dispatcher + RpcImpl method - CLI command with doc binding - RPC doc, integration test, and test framework client method --- bin/floresta-cli/src/main.rs | 15 +++++ crates/floresta-node/src/json_rpc/network.rs | 10 +++ crates/floresta-node/src/json_rpc/server.rs | 9 +++ crates/floresta-rpc/src/rpc.rs | 10 +++ crates/floresta-rpc/src/rpc_types.rs | 20 ++++++ .../src/p2p_wire/node/user_req.rs | 54 ++++++++++++++++ .../src/p2p_wire/node_interface.rs | 44 +++++++++++++ doc/rpc/getaddednodeinfo.md | 52 ++++++++++++++++ tests/floresta-cli/getaddednodeinfo.py | 62 +++++++++++++++++++ tests/test_framework/rpc/base.py | 8 +++ tests/test_runner.py | 1 + 11 files changed, 285 insertions(+) create mode 100644 doc/rpc/getaddednodeinfo.md create mode 100644 tests/floresta-cli/getaddednodeinfo.py diff --git a/bin/floresta-cli/src/main.rs b/bin/floresta-cli/src/main.rs index 2a77e6bdd..24fb0dbc8 100644 --- a/bin/floresta-cli/src/main.rs +++ b/bin/floresta-cli/src/main.rs @@ -96,6 +96,9 @@ fn do_request(cmd: &Cli, client: Client) -> anyhow::Result { serde_json::to_string_pretty(&client.get_block(hash, verbosity)?)? } Methods::GetPeerInfo => serde_json::to_string_pretty(&client.get_peer_info()?)?, + Methods::GetAddedNodeInfo { node } => { + serde_json::to_string_pretty(&client.get_added_node_info(node)?)? + } Methods::Stop => serde_json::to_string_pretty(&client.stop()?)?, Methods::AddNode { node, @@ -293,6 +296,18 @@ pub enum Methods { #[command(name = "getpeerinfo")] GetPeerInfo, + #[doc = include_str!("../../../doc/rpc/getaddednodeinfo.md")] + #[command( + name = "getaddednodeinfo", + about = "Returns information about the given added node, or all added nodes", + long_about = Some(include_str!("../../../doc/rpc/getaddednodeinfo.md")), + disable_help_subcommand = true + )] + GetAddedNodeInfo { + /// If provided, return information about this specific node + node: Option, + }, + /// Returns the value associated with a UTXO, if it's still not spent. /// This function only works properly if we have the compact block filters /// feature enabled diff --git a/crates/floresta-node/src/json_rpc/network.rs b/crates/floresta-node/src/json_rpc/network.rs index 371ab793f..fa02bf38f 100644 --- a/crates/floresta-node/src/json_rpc/network.rs +++ b/crates/floresta-node/src/json_rpc/network.rs @@ -118,4 +118,14 @@ impl RpcImpl { .await .map_err(|_| JsonRpcError::Node("Failed to get peer information".to_string())) } + + pub(crate) async fn get_added_node_info( + &self, + node: Option, + ) -> Result> { + self.node + .get_added_node_info(node) + .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..b3e6314f1 100644 --- a/crates/floresta-node/src/json_rpc/server.rs +++ b/crates/floresta-node/src/json_rpc/server.rs @@ -356,6 +356,15 @@ async fn handle_json_rpc_request( .await .map(|v| serde_json::to_value(v).unwrap()), + "getaddednodeinfo" => { + let node = get_optional_field(¶ms, 0, "node", get_string)?; + + state + .get_added_node_info(node) + .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-rpc/src/rpc.rs b/crates/floresta-rpc/src/rpc.rs index 66df3d723..b406cf279 100644 --- a/crates/floresta-rpc/src/rpc.rs +++ b/crates/floresta-rpc/src/rpc.rs @@ -92,6 +92,9 @@ pub trait FlorestaRPC { /// the peer's IP address, the peer's version, the peer's user agent, the transport protocol /// and the peer's current height. fn get_peer_info(&self) -> Result>; + #[doc = include_str!("../../../doc/rpc/getaddednodeinfo.md")] + /// Returns information about manually added peers. + fn get_added_node_info(&self, node: Option) -> Result>; /// Returns a block, given a block hash /// /// This method returns a block, given a block hash. If the verbosity flag is 0, the block @@ -293,6 +296,13 @@ impl FlorestaRPC for T { self.call("getpeerinfo", &[]) } + fn get_added_node_info(&self, node: Option) -> Result> { + match node { + Some(node) => self.call("getaddednodeinfo", &[Value::String(node)]), + None => self.call("getaddednodeinfo", &[]), + } + } + fn get_best_block_hash(&self) -> Result { self.call("getbestblockhash", &[]) } diff --git a/crates/floresta-rpc/src/rpc_types.rs b/crates/floresta-rpc/src/rpc_types.rs index 1ebce51d7..9026ce70d 100644 --- a/crates/floresta-rpc/src/rpc_types.rs +++ b/crates/floresta-rpc/src/rpc_types.rs @@ -188,6 +188,26 @@ pub struct PeerInfo { pub transport_protocol: String, } +/// Information about a manually added peer, as returned by `getaddednodeinfo`. +#[derive(Debug, Deserialize, Serialize)] +pub struct AddedNodeInfo { + /// The node address in `ip:port` format, as provided to `addnode`. + pub addednode: String, + /// Whether the node is currently connected. + pub connected: bool, + /// A list of addresses with connection direction info. Empty when not connected. + pub addresses: Vec, +} + +/// An address entry within [`AddedNodeInfo`]. +#[derive(Debug, Deserialize, Serialize)] +pub struct AddedNodeAddressInfo { + /// The address in `ip:port` format. + pub address: String, + /// Connection direction: `"outbound"` when connected. + pub connected: String, +} + #[derive(Debug, Deserialize, Serialize)] #[serde(untagged)] pub enum GetBlockRes { 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..970126783 100644 --- a/crates/floresta-wire/src/p2p_wire/node/user_req.rs +++ b/crates/floresta-wire/src/p2p_wire/node/user_req.rs @@ -13,9 +13,12 @@ use tracing::warn; use super::try_and_log; use super::NodeRequest; use super::UtreexoNode; +use crate::address_man::LocalAddress; use crate::block_proof::Bitmap; use crate::node::running_ctx::RunningNode; use crate::node_context::NodeContext; +use crate::node_interface::AddedNodeAddress; +use crate::node_interface::AddedNodeInfoItem; use crate::node_interface::NodeInterface; use crate::node_interface::NodeResponse; use crate::node_interface::UserRequest; @@ -47,6 +50,52 @@ where try_and_log!(responder.send(NodeResponse::GetPeerInfo(peers))); } + /// Handles getaddednodeinfo requests, returning info about manually addedd peers. + fn handle_get_added_node_info( + &self, + node: Option, + responder: oneshot::Sender, + ) { + let items: Vec = self + .added_peers + .iter() + .filter_map(|added| { + let added_ip = LocalAddress::from(added.address.clone()).get_net_address(); + let addr_str = format!("{}:{}", added_ip, added.port); + + // If a specific node was requested, skip non-matching entries + if let Some(ref filter) = node { + if &addr_str != filter && &added_ip.to_string() != filter { + return None; + } + } + + // Check if this added peer is currently connected + let is_connected = self + .peers + .values() + .any(|peer| peer.address == added_ip && peer.port == added.port); + + let addresses = if is_connected { + vec![AddedNodeAddress { + address: addr_str.clone(), + connected: "outbound".to_string(), + }] + } else { + vec![] + }; + + Some(AddedNodeInfoItem { + addednode: addr_str, + connected: is_connected, + addresses, + }) + }) + .collect(); + + try_and_log!(responder.send(NodeResponse::GetAddedNodeInfo(items))); + } + /// Actually perform the user request /// /// These are requests made by some consumer of `floresta-wire` using the [`NodeInterface`], and may @@ -90,6 +139,11 @@ where return; } + UserRequest::GetAddedNodeInfo(node) => { + self.handle_get_added_node_info(node, responder); + return; + } + UserRequest::Add((addr, port, v2transport)) => { let node_response = match self.handle_addnode_add_peer(addr, port, v2transport) { Ok(_) => { diff --git a/crates/floresta-wire/src/p2p_wire/node_interface.rs b/crates/floresta-wire/src/p2p_wire/node_interface.rs index 568b63feb..607d80a96 100644 --- a/crates/floresta-wire/src/p2p_wire/node_interface.rs +++ b/crates/floresta-wire/src/p2p_wire/node_interface.rs @@ -93,6 +93,9 @@ pub enum UserRequest { /// Adds a transaction to mempool and advertises it SendTransaction(Transaction), + + /// Return information about manually added peers, optionally filtered by address. + GetAddedNodeInfo(Option), } #[derive(Debug, Clone, Serialize)] @@ -113,6 +116,26 @@ pub struct PeerInfo { pub transport_protocol: TransportProtocol, } +#[derive(Debug, Clone, Serialize)] +/// Information about a manually added peer, as returned by `getaddednodeinfo`. +pub struct AddedNodeInfoItem { + /// The address of the added node in `ip:port` format. + pub addednode: String, + /// Whether the node is currently connected. + pub connected: bool, + /// A list of addresses associated with this added node, with connection direction info. + pub addresses: Vec, +} + +#[derive(Debug, Clone, Serialize)] +/// An address entry within `AddedNodeInfoItem`. +pub struct AddedNodeAddress { + /// The address in `ip:port` format. + pub address: String, + /// The connection direction: `"outbound"` if connected, `"false"` if not. + pub connected: String, +} + #[derive(Debug)] /// A response that can be sent back to the user. /// @@ -151,6 +174,9 @@ pub enum NodeResponse { /// Transaction broadcast TransactionBroadcastResult(Result), + + /// A response containing a list of manually added peer information. + GetAddedNodeInfo(Vec), } #[derive(Debug, Clone)] @@ -308,6 +334,24 @@ impl NodeInterface { extract_variant!(GetPeerInfo, val); } + /// Gets information about all manually added peers, optionally filtered by address. + /// + /// This function will return a list of `AddedNodeInfoItem` structs, each of which contains + /// information about a manually added peer. + /// + /// If an address is provided, the function will only return information about the peer with + /// that address. + pub async fn get_added_node_info( + &self, + node: Option, + ) -> Result, oneshot::error::RecvError> { + let val = self + .send_request(UserRequest::GetAddedNodeInfo(node)) + .await?; + + extract_variant!(GetAddedNodeInfo, val); + } + /// Pings all connected peers to check if they are alive. pub async fn ping(&self) -> Result { let val = self.send_request(UserRequest::Ping).await?; diff --git a/doc/rpc/getaddednodeinfo.md b/doc/rpc/getaddednodeinfo.md new file mode 100644 index 000000000..e70488fa1 --- /dev/null +++ b/doc/rpc/getaddednodeinfo.md @@ -0,0 +1,52 @@ +# `getaddednodeinfo` + +Returns information about the given added node, or all added nodes. + +## Usage + +### Synopsis + +```bash +floresta-cli getaddednodeinfo [node] +``` + +### Examples + +```bash +floresta-cli getaddednodeinfo +floresta-cli getaddednodeinfo 192.168.0.1:8333 +``` + +## Arguments + +`node` - (string, optional) If provided, return information about this specific node. Can be an `ip:port` or just an `ip`. + +## Returns + +### Ok response + +```json +[ + { + "addednode": "192.168.0.1:8333", + "connected": true, + "addresses": [ + { + "address": "192.168.0.1:8333", + "connected": "outbound" + } + ] + } +] +``` + +- `addednode` - (string) The node address in `ip:port` format, as provided to `addnode`. +- `connected` - (boolean) Whether the node is currently connected. +- `addresses` - (json array) A list of addresses with connection direction info. Empty when not connected. + - `address` - (string) The address in `ip:port` format. + - `connected` - (string) Connection direction: `"outbound"` when connected. + +## Notes + +- Only nodes added via `addnode add` are listed. Nodes connected with `addnode onetry` are not included. +- The `addresses` array is empty when `connected` is `false`. diff --git a/tests/floresta-cli/getaddednodeinfo.py b/tests/floresta-cli/getaddednodeinfo.py new file mode 100644 index 000000000..fa71b659b --- /dev/null +++ b/tests/floresta-cli/getaddednodeinfo.py @@ -0,0 +1,62 @@ +# SPDX-License-Identifier: MIT OR Apache-2.0 + +""" +floresta_cli_getaddednodeinfo.py + +This functional test cli utility to interact with a Floresta node with `getaddednodeinfo` +""" + +from test_framework import FlorestaTestFramework +from test_framework.node import NodeType + + +class GetAddedNodeInfoTest(FlorestaTestFramework): + """ + Test `getaddednodeinfo` by adding a bitcoind peer via `addnode add`, + then verifying the result of `getaddednodeinfo` contains the peer. + """ + + def set_test_params(self): + """ + Setup florestad and bitcoind nodes + """ + 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): + """ + Test getaddednodeinfo returns correct info before and after adding a peer + """ + self.run_node(self.florestad) + self.run_node(self.bitcoind) + + # Before adding any node, the list should be empty + result = self.florestad.rpc.get_addednodeinfo() + self.assertIsSome(result) + self.assertEqual(len(result), 0) + + # Add bitcoind as a persistent peer + self.connect_nodes(self.florestad, self.bitcoind, "add", v2transport=False) + self.wait_for_peers_connections(self.florestad, self.bitcoind, True) + + # getaddednodeinfo should now return one entry + result = self.florestad.rpc.get_addednodeinfo() + self.assertIsSome(result) + self.assertEqual(len(result), 1) + self.assertTrue(result[0]["connected"]) + self.assertEqual(len(result[0]["addresses"]), 1) + self.assertEqual(result[0]["addresses"][0]["connected"], "outbound") + + # Query by specific node address + node_addr = result[0]["addednode"] + filtered = self.florestad.rpc.get_addednodeinfo(node_addr) + self.assertIsSome(filtered) + self.assertEqual(len(filtered), 1) + self.assertEqual(filtered[0]["addednode"], node_addr) + + +if __name__ == "__main__": + GetAddedNodeInfoTest().main() diff --git a/tests/test_framework/rpc/base.py b/tests/test_framework/rpc/base.py index f0d8baf2d..d8a630f10 100644 --- a/tests/test_framework/rpc/base.py +++ b/tests/test_framework/rpc/base.py @@ -327,3 +327,11 @@ def disconnectnode(self, node_address: str, node_id: Optional[int] = None): "disconnectnode", params=[node_address, node_id] ) return self.perform_request("disconnectnode", params=[node_address]) + + def get_addednodeinfo(self, node: Optional[str] = None): + """ + Returns information about the given added node, or all added nodes + """ + if node is not None: + return self.perform_request("getaddednodeinfo", params=[node]) + return self.perform_request("getaddednodeinfo") 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"),