Skip to content

Conversation

mzumsande
Copy link
Contributor

@mzumsande mzumsande commented Jan 26, 2022

This implements the suggestion from #23763 to introduce an option to establish block-relay-connections manually with the -addnode RPC.
Adding these can make sense for a node operator that wants to be connected to a anonymity networks like Tor or I2P, but also wants to have additional protection against eclipse attacks: Following the best chain can be more of an issue on anonymity networks because these are smaller and it can be easier to create a lot of sybil nodes there.

In that situation, manual block-relay-only connections to peers on clearnet networks can help us staying connected to the best chain, but in contrast to normal manual connections, transactions and addresses aren't transmitted on these links - in particular not our own address or transaction from our wallet. This increases privacy and will also make it harder to perform fingerprinting attacks (connecting our identities over different networks).

Manual Block-Relay connections:

  • can be specified with -addnode RPC, both with the add and onetry command
  • can be specified with the -addnode bitcoind arg (or in bitcoin.conf) with <IP>=manual-block-relay
  • don't participate in transaction and address relay
  • don't get discouraged / punished for misbehavior (but will still get disconnected for sending TX/tx-INV as automatic block-relay-only peers do)
  • are not subject to outbound eviction logic (unlike automatic block-relay-only-peers)

@mzumsande mzumsande marked this pull request as draft January 26, 2022 20:32
@mzumsande mzumsande force-pushed the 202112_manual_blocksonly branch from 5b8c16d to f2fc01f Compare January 26, 2022 20:46
@DrahtBot
Copy link
Contributor

DrahtBot commented Jan 26, 2022

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Reviews

See the guideline for information on the review process.

Type Reviewers
Concept ACK sipa, brunoerg
Stale ACK jnewbery, willcl-ark, aureleoules, rajarshimaitra, LarryRuane

If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #27509 (Relay own transactions only via short-lived Tor or I2P connections by vasild)
  • #27467 (p2p: skip netgroup diversity follow-up by jonatack)
  • #27385 (net, refactor: extract Network and BIP155Network logic to node/network by jonatack)
  • #26621 (refactor: Continue moving application data from CNode to Peer by dergoegge)
  • #26366 (p2p, rpc, test: addnode improv + add test coverage for invalid command by brunoerg)
  • #25572 (refactor: Introduce EvictionManager and use it for the inbound eviction logic by dergoegge)
  • #25268 (refactor: Introduce EvictionManager by dergoegge)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

@mzumsande
Copy link
Contributor Author

I'd be really interested in opinions whether the use case (as outlined in OP and linked issue) is important enough to justify the added complexity of adding another connection type (and also adding more complexity to -addnode).

@mzumsande
Copy link
Contributor Author

Rebased and addressed feedback.

src/net.h Outdated
@@ -1087,7 +1114,7 @@ class CConnman
AddrMan& addrman;
std::deque<std::string> m_addr_fetches GUARDED_BY(m_addr_fetches_mutex);
Mutex m_addr_fetches_mutex;
std::vector<std::string> m_added_nodes GUARDED_BY(m_added_nodes_mutex);
std::vector<AddedNodeEntry> m_added_nodes GUARDED_BY(m_added_nodes_mutex);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not try std::map<std::string, ConnectionType> m_added_nodes, then we can avoid to define AddedNodeEntry?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'm a bit undecided about that: I think avoiding a struct is not really a good argument for switching form vector to map: We would need to switch to a pair or struct if we'd ever need to add another member, plus there is a case for introducing a struct even if we'd switch to a map, as was recently done in PruneLocks example.
Performance-wise, this doesn't seem important at all since the container cannot have more than 8 entries (MAX_ADDNODE_CONNECTIONS).

@@ -1230,7 +1230,7 @@ void PeerManagerImpl::InitializeNode(CNode *pnode)
mapNodeState.emplace_hint(mapNodeState.end(), std::piecewise_construct, std::forward_as_tuple(nodeid), std::forward_as_tuple(pnode->IsInboundConn()));
assert(m_txrequest.Count(nodeid) == 0);
}
PeerRef peer = std::make_shared<Peer>(nodeid, /*tx_relay=*/ !pnode->IsBlockOnlyConn());
PeerRef peer = std::make_shared<Peer>(nodeid, /*tx_relay=*/!pnode->IsBlockOnlyConn() && !pnode->IsManualBlockRelayConn());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not directly change IsBlockOnlyConn() directly as below, then can avoid to define new function IsManualBlockRelayConn()?

 bool IsBlockOnlyConn() const {
  •    return m_conn_type == ConnectionType::BLOCK_RELAY;
    
  •    return m_conn_type == ConnectionType::BLOCK_RELAY || m_conn_type == ConnectionType::MANUAL_BLOCK_RELAY;
    
    }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #24170 (comment) - there are several placed that use IsBlockOnlyConn() with logic specific to automatic block-relay-only connections that have logic that does not apply to manual ones. So if I did that, we'd need additional logic in these places to distinguish between manual and automatic connections.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems preferable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have changed the logic now such that IsBlockRelayConn is used when both connection types are meant, where as IsManualBlockRelayConn and IsAutomaticBlockRelayConn are used in spots where only one is meant.

Copy link
Contributor

@jnewbery jnewbery left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK. This seems like a reasonable feature request.

I've left a few comments, mostly around naming.

I wonder if in future, we could change the net/net_processing responsibilities such that net_processing is unaware of whether a connection is manual or not. The only places that net_processing uses that information are in MaybeDiscourageAndDisconnect() (before this PR), and EvictExtraOutboundPeers() (in this PR). If disconnection and eviction logic is moved out of net_processing (cc @cfields), then net_processing won't need to be aware of the difference between manual and automatic connections.

src/net.h Outdated
ConnectionType conn_type;
};

struct AddedNodeEntry {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that this is maybe a bit of a stretch for this PR, but I really don't like the addnode/addednode terminology, and would much prefer this to be named something like ManualConnectionEntry, which much more precisely describes the nature of these connections.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The downside of ManualConnectionEntry is that it is not a connection, just a name of a node we'll try to connect to (which may succeed or not). I didn't really like the name AddedNodeEntry either though, but couldn't think of anything better. Maybe ManualNodeEntry?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe ManualConnectionTarget or DesiredManualConnection? I'm not sure they're great either.

@@ -440,7 +440,7 @@ void SetupServerArgs(ArgsManager& argsman)
" If <type> is not supplied or if <type> = 1, indexes for all known types are enabled.",
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);

argsman.AddArg("-addnode=<ip>", strprintf("Add a node to connect to and attempt to keep the connection open (see the addnode RPC help for more info). This option can be specified multiple times to add multiple nodes; connections are limited to %u at a time and are counted separately from the -maxconnections limit.", MAX_ADDNODE_CONNECTIONS), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION);
argsman.AddArg("-addnode=<ip>[=manual-block-relay]", strprintf("Add a node to connect to and attempt to keep the connection open (see the addnode RPC help for more info). This option can be specified multiple times to add multiple nodes; connections are limited to %u at a time and are counted separately from the -maxconnections limit. Append =manual-block-relay to disable transaction and address relay.", MAX_ADDNODE_CONNECTIONS), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer =blocksonly to be clearer and match the existing -blocksonly option.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-blocksonly mode is different from block relay peers.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How so? Maybe it needs better docs on the difference...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't contrast.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some differences:

  • -blocksonly is a global startup option (applying to all connections) -block-relay-only is connection-specific
  • A node in -blocksonly mode participates in address relay, we never send out addresses on -block-relay-only connections (and ignore incoming ones).
  • A node in -blocksonly signals not to receive any transactions (fRelay) and will disconnect if that is breached, but it may in special cases send out transactions itself (if submitted via RPC). We'll never send a tx over a block-relay-only connection.
  • A node in -blocksonly mode still has two of their outbound connections as -block-relay-only (for which then the block-relay-only rules apply).

for (const std::string& it : m_added_nodes) {
if (strNode == it) return false;
for (const auto& it : m_added_nodes) {
if (strNode == it.m_node_address) return false;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supposed option modify case need not be handled?
If add node firstly with setting manual and then setting manual block relay.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, a modify option is not supported, especially since it is not possible to change the connection connection type of an existing connection. A user could instead use the 'remove' option and then re-add with another connection type (which would not change existing connections though).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be an option to use std::any_of here if using standard library algorithms is preferable, but personally I think it might end up a little less readable.

@@ -1258,6 +1260,7 @@ bool CConnman::AddConnection(const std::string& address, ConnectionType conn_typ
switch (conn_type) {
case ConnectionType::INBOUND:
case ConnectionType::MANUAL:
case ConnectionType::MANUAL_BLOCK_RELAY:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addconnection rpc should not support manual block relay, so seems here need not check?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's true that it doesn't support this, just as it doesn't support INBOUND or MANUAL. But leaving it out her would result in a compiler warning for the switch, see comment below:
// no default case, so the compiler can warn about missing cases

@stratospher
Copy link
Contributor

For me, the critical question is still if this opt-in feature will find enough users to justify the added complexity in net - I honestly don't know that, and I wouldn't have a problem closing this for now if people think there isn't enough demand for this use case currently.

if it helps, i found some related issues: (but i don’t know this either)

in the very unlikely scenario that everyone likes this connection and all bitcoin nodes on tor connect with clearnet only using this option - that wouldn't be good for the network right? (assuming an average tor user wouldn’t want their addresses/transactions relayed to their clearnet peers)

@mzumsande
Copy link
Contributor Author

in the very unlikely scenario that everyone likes this connection and all bitcoin nodes on tor connect with clearnet only using this option - that wouldn't be good for the network right? (assuming an average tor user wouldn’t want their addresses/transactions relayed to their clearnet peers)

We'd need enough inbound capacity on clearnet to accomodate the additional manual connections to it, and also some users relaying transactions on both networks, otherwise txns submitted through tor wouldn't make it into clearnet and vice versa. I think that there are many nodes with a lesser need for privacy (e.g. because they don't submit transactions regularly) that don't have the need for something like and would be fine having full connections to both networks.


struct AddedNodeInfo {
AddedNodeParams m_params;
CService resolvedAddress;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be the difference between resolvedAddress and m_node_address?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m_node_address is a string, which is part of AddedNodeParams, which has just the unvalidated user input.

resolvedAddress is a CService built from m_node_address, which is generated in GetAddedNodeInfo and used in ThreadOpenConnection / rpc code.

src/rpc/net.cpp Outdated
if (strCommand == "remove" && !request.params[2].isNull()) {
throw JSONRPCError(RPC_INVALID_PARAMS, "Cannot specify a connection type with 'remove'");
}
std::string connection_type_name{manual_name};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps there is a way to simplify it:

diff --git a/src/rpc/net.cpp b/src/rpc/net.cpp
index 6155af323..b9e082a63 100644
--- a/src/rpc/net.cpp
+++ b/src/rpc/net.cpp
@@ -304,22 +304,19 @@ static RPCHelpMan addnode()
         throw std::runtime_error(
             self.ToString());
     }
-    const std::string manual_name{ConnectionTypeAsString(ConnectionType::MANUAL)};
-    const std::string manual_block_relay_name{ConnectionTypeAsString(ConnectionType::MANUAL_BLOCK_RELAY)};
+
     if (strCommand == "remove" && !request.params[2].isNull()) {
         throw JSONRPCError(RPC_INVALID_PARAMS, "Cannot specify a connection type with 'remove'");
     }
-    std::string connection_type_name{manual_name};
+
+    const std::string manual_block_relay_name{ConnectionTypeAsString(ConnectionType::MANUAL_BLOCK_RELAY)};
+    ConnectionType connection_type{ConnectionType::MANUAL};
     if (!request.params[2].isNull()) {
-        connection_type_name = request.params[2].get_str();
-    }
-    ConnectionType connection_type;
-    if (connection_type_name == manual_block_relay_name) {
-        connection_type = ConnectionType::MANUAL_BLOCK_RELAY;
-    } else if (connection_type_name == manual_name) {
-        connection_type = ConnectionType::MANUAL;
-    }  else {
-        throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Unsupported connection type: " + connection_type_name);
+        const std::string connection_type_name{request.params[2].get_str()};
+        if (connection_type_name == manual_block_relay_name)
+            connection_type = ConnectionType::MANUAL_BLOCK_RELAY;
+        else
+            throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Unsupported connection type: " + connection_type_name);
     }
 
     NodeContext& node = EnsureAnyNodeContext(request.context);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thanks, that's a bit simpler and should be equivalent, so I took your suggestion!

This is in preparation for the addition of manual block-relay-only
connections.

-BEGIN VERIFY SCRIPT-
ren() { sed -i "s:\<$1\>:$2:g" $(git grep -l "\<$1\>" ./src ./test); }

ren IsBlockOnlyConn IsAutomaticBlockRelayConn
ren GetCurrentBlockRelayOnlyConns GetCurrentAutomaticBlockRelayConns

-END VERIFY SCRIPT-
This commit implements the p2p behaviour, but the new
connection type is not used yet.
@mzumsande mzumsande force-pushed the 202112_manual_blocksonly branch from b36db71 to c13c986 Compare April 26, 2023 11:52
@mzumsande
Copy link
Contributor Author

mzumsande commented Apr 26, 2023

b36db71 to c13c986: rebased (will address the oustanding review comments soon!)

mzumsande and others added 5 commits May 15, 2023 16:40
This applies to both add mode and onetry mode

Co-authored-by: dhruv <856960+dhruv@users.noreply.github.com>
Co-authored-by: brunoerg <brunoely.gc@gmail.com>
This allows to specify manual-block-relay peers per
command arg or bitcoin.conf
…ndOrBlockRelayConn

to make it clearer that manual connections are not included

-BEGIN VERIFY SCRIPT-
ren() { sed -i "s:\<$1\>:$2:g" $(git grep -l "\<$1\>" ./src ./test); }

ren IsOutboundOrBlockRelayConn IsAutomaticOutboundOrBlockRelayConn

-END VERIFY SCRIPT-
to better distinguish from MANUAL_BLOCK_RELAY

-BEGIN VERIFY SCRIPT-
ren() { sed -i "s:\<$1\>:$2:g" $(git grep -l "\<$1\>" ./src ./test); }

ren BLOCK_RELAY AUTOMATIC_BLOCK_RELAY

-END VERIFY SCRIPT-
@mzumsande mzumsande force-pushed the 202112_manual_blocksonly branch from c13c986 to 41ce312 Compare May 15, 2023 20:45
@mzumsande
Copy link
Contributor Author

c13c986 to 41ce312:
Incorporated feedback by @brunoerg

@mzumsande
Copy link
Contributor Author

Seems like other proposals such as #27213 and #27509 that rely on improving automatic behavior have more support compared to this one (which needs manual setup by the node operator). Closing for now.

@mzumsande mzumsande closed this Jun 4, 2023
@bitcoin bitcoin locked and limited conversation to collaborators Jun 3, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.