diff --git a/src/net_processing.cpp b/src/net_processing.cpp --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -296,6 +296,12 @@ */ static constexpr size_t MAX_ADDR_PROCESSING_TOKEN_BUCKET{MAX_ADDR_TO_SEND}; +/** + * Maximum number of proof inventories that can be added to the inventory to + * send vector due to getavaproofs requests. + */ +static constexpr size_t MAX_GETAVAPROOFS_INVS{1000}; + inline size_t GetMaxAddrToSend() { return gArgs.GetArg("-maxaddrtosend", MAX_ADDR_TO_SEND); } @@ -3384,7 +3390,8 @@ msg_type == NetMsgType::AVAPOLL || msg_type == NetMsgType::AVARESPONSE || msg_type == NetMsgType::AVAPROOF || - msg_type == NetMsgType::GETAVAADDR; + msg_type == NetMsgType::GETAVAADDR || + msg_type == NetMsgType::GETAVAPROOFS; } /** @@ -5295,6 +5302,39 @@ return; } + if (msg_type == NetMsgType::GETAVAPROOFS) { + if (pfrom.m_proof_relay == nullptr) { + return; + } + + g_avalanche->withPeerManager([&](const avalanche::PeerManager &pm) { + // TODO: this can benefit from some improvements: + // - The peers can be ordered by score the that the high value + // proof are added first. + // - If the number of proofs is very large wrt to the limit, a + // cache be helpful. + pm.forEachPeer([&](const avalanche::Peer &peer) { + LOCK(pfrom.m_proof_relay->cs_proof_inventory); + + // Don't grow the inventory vector past MAX_GETAVAPROOFS_INVS. + // This is so that if a peer send repeated getavaproofs messages + // memory will not grow unbound. + if (pfrom.m_proof_relay->setInventoryProofToSend.size() > + MAX_GETAVAPROOFS_INVS) { + return; + } + + const auto &proofid = peer.getProofId(); + if (!pfrom.m_proof_relay->filterProofKnown.contains(proofid)) { + pfrom.m_proof_relay->setInventoryProofToSend.insert( + proofid); + } + }); + }); + + return; + } + if (msg_type == NetMsgType::GETADDR) { // This asymmetric behavior for inbound and outbound connections was // introduced to prevent a fingerprinting attack: an attacker can send diff --git a/src/protocol.h b/src/protocol.h --- a/src/protocol.h +++ b/src/protocol.h @@ -303,6 +303,12 @@ */ extern const char *GETAVAADDR; +/** + * The getavaproofs message requests an inv message that provides the valid + * proofids with the highest score from our peer. + */ +extern const char *GETAVAPROOFS; + /** * Indicate if the message is used to transmit the content of a block. * These messages can be significantly larger than usual messages and therefore diff --git a/src/protocol.cpp b/src/protocol.cpp --- a/src/protocol.cpp +++ b/src/protocol.cpp @@ -52,6 +52,7 @@ const char *AVARESPONSE = "avaresponse"; const char *AVAPROOF = "avaproof"; const char *GETAVAADDR = "getavaaddr"; +const char *GETAVAPROOFS = "getavaproofs"; bool IsBlockLike(const std::string &strCommand) { return strCommand == NetMsgType::BLOCK || diff --git a/test/functional/abc_p2p_proof_inventory.py b/test/functional/abc_p2p_proof_inventory.py --- a/test/functional/abc_p2p_proof_inventory.py +++ b/test/functional/abc_p2p_proof_inventory.py @@ -10,6 +10,7 @@ from test_framework.address import ADDRESS_ECREG_UNSPENDABLE from test_framework.avatools import ( + AvaP2PInterface, avalanche_proof_from_hex, gen_proof, get_proof_ids, @@ -34,7 +35,7 @@ UNCONDITIONAL_RELAY_DELAY = 2 * 60 -class ProofInvStoreP2PInterface(P2PInterface): +class ProofInvStoreP2PInterface(AvaP2PInterface): def __init__(self): super().__init__() self.proof_invs_counter = 0 @@ -187,14 +188,53 @@ node0.sendavalancheproof(proof.serialize().hex()) self.sync_proofs() + def test_respond_getavaproofs(self): + self.log.info("Check the node responds to getavaproofs messages") + node = self.nodes[0] + + sending_peer = node.add_p2p_connection(AvaP2PInterface()) + for _ in range(50): + _, proof = gen_proof(node) + sending_peer.send_avaproof(proof) + wait_for_proof(node, f"{proof.proofid:0{64}x}") + num_proofs = len(get_proof_ids(node)) + + receiving_peer = node.add_p2p_connection(ProofInvStoreP2PInterface()) + + # The avahello message counts for 1 proof inventory sent + receiving_peer.wait_for_avahello() + + assert_equal(receiving_peer.proof_invs_counter, 0) + + receiving_peer.send_getavaproofs() + receiving_peer.wait_until( + lambda: receiving_peer.proof_invs_counter == num_proofs - 1) + + self.log.info( + "Check the node won't send the same inv again even after receiving a getavaproofs message") + + # Let the node get more proofs + for _ in range(20): + _, proof = gen_proof(node) + sending_peer.send_avaproof(proof) + wait_for_proof(node, f"{proof.proofid:0{64}x}") + num_proofs = len(get_proof_ids(node)) + + receiving_peer.send_getavaproofs() + # The peer should only receive the last 20 proofs, so (num_proofs - 1) + # total. + receiving_peer.wait_until( + lambda: receiving_peer.proof_invs_counter == num_proofs - 1) + def test_unbroadcast(self): self.log.info("Test broadcasting proofs") node = self.nodes[0] - # Disconnect the other nodes, or they will request the proof and + # Disconnect the other nodes/peers, or they will request the proof and # invalidate the test - [node.stop_node() for node in self.nodes[1:]] + [n.stop_node() for n in self.nodes[1:]] + node.disconnect_p2ps() def add_peers(count): peers = [] @@ -289,6 +329,7 @@ self.test_receive_proof() self.test_proof_relay() self.test_manually_sent_proof() + self.test_respond_getavaproofs() # Run these tests last because they need to disconnect the nodes self.test_unbroadcast() diff --git a/test/functional/test_framework/avatools.py b/test/functional/test_framework/avatools.py --- a/test/functional/test_framework/avatools.py +++ b/test/functional/test_framework/avatools.py @@ -26,6 +26,7 @@ msg_avahello, msg_avapoll, msg_avaproof, + msg_getavaproofs, msg_tcpavaresponse, ) from .p2p import P2PInterface, p2p_lock @@ -272,6 +273,9 @@ msg.proof = proof self.send_message(msg) + def send_getavaproofs(self): + self.send_message(msg_getavaproofs()) + def get_ava_p2p_interface( node: TestNode, diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -2118,6 +2118,23 @@ return "msg_getavaaddr()" +class msg_getavaproofs: + __slots__ = () + msgtype = b"getavaproofs" + + def __init__(self): + pass + + def deserialize(self, f): + pass + + def serialize(self): + return b"" + + def __repr__(self): + return "msg_getavaproofs()" + + class TestFrameworkMessages(unittest.TestCase): def test_legacy_avalanche_proof_serialization_round_trip(self): """Verify that a LegacyAvalancheProof object is unchanged after a diff --git a/test/functional/test_framework/p2p.py b/test/functional/test_framework/p2p.py --- a/test/functional/test_framework/p2p.py +++ b/test/functional/test_framework/p2p.py @@ -52,6 +52,7 @@ msg_filterload, msg_getaddr, msg_getavaaddr, + msg_getavaproofs, msg_getblocks, msg_getblocktxn, msg_getdata, @@ -108,6 +109,7 @@ b"filterload": msg_filterload, b"getaddr": msg_getaddr, b"getavaaddr": msg_getavaaddr, + b"getavaproofs": msg_getavaproofs, b"getblocks": msg_getblocks, b"getblocktxn": msg_getblocktxn, b"getdata": msg_getdata, @@ -460,6 +462,8 @@ def on_getavaaddr(self, message): pass + def on_getavaproofs(self, message): pass + def on_getblocks(self, message): pass def on_getblocktxn(self, message): pass