diff --git a/src/avalanche/peermanager.h b/src/avalanche/peermanager.h --- a/src/avalanche/peermanager.h +++ b/src/avalanche/peermanager.h @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -127,10 +128,18 @@ NodeSet nodes; + /** Pool of proofs using orphan UTXOs as stake */ + std::map orphanProofs; + + /** Filter of recently rejected proofs */ + std::unique_ptr recentProofRejects; + static constexpr int SELECT_PEER_MAX_RETRY = 3; static constexpr int SELECT_NODE_MAX_RETRY = 3; public: + PeerManager(); + /** * Node API. */ @@ -188,8 +197,14 @@ std::vector getPeers() const; std::vector getNodeIdsForPeer(PeerId peerId) const; + const Proof *getProof(const ProofId proofId) const; + + bool addProof(Proof &&proof); + private: PeerSet::iterator fetchOrCreatePeer(const Proof &proof); + PeerSet::iterator fetchOrCreatePeer(const Proof &proof, + ProofValidationState &state); bool addNodeToPeer(const PeerSet::iterator &it); bool removeNodeFromPeer(const PeerSet::iterator &it, uint32_t count = 1); }; diff --git a/src/avalanche/peermanager.cpp b/src/avalanche/peermanager.cpp --- a/src/avalanche/peermanager.cpp +++ b/src/avalanche/peermanager.cpp @@ -13,6 +13,10 @@ namespace avalanche { +PeerManager::PeerManager() { + recentProofRejects.reset(new CRollingBloomFilter(120000, 0.000001)); +} + bool PeerManager::addNode(NodeId nodeid, const Proof &proof, const Delegation &delegation) { auto it = fetchOrCreatePeer(proof); @@ -187,6 +191,13 @@ PeerManager::PeerSet::iterator PeerManager::fetchOrCreatePeer(const Proof &proof) { + ProofValidationState state; + return fetchOrCreatePeer(proof, state); +} + +PeerManager::PeerSet::iterator +PeerManager::fetchOrCreatePeer(const Proof &proof, + ProofValidationState &state) { { // Check if we already know of that peer. auto &pview = peers.get(); @@ -201,7 +212,6 @@ LOCK(cs_main); const CCoinsViewCache &coins = ::ChainstateActive().CoinsTip(); - ProofValidationState state; if (!proof.verify(state, coins)) { return peers.end(); } @@ -456,4 +466,43 @@ return nodeids; } +const Proof *PeerManager::getProof(const ProofId proofId) const { + auto &peerView = peers.get(); + auto peerIt = peerView.find(proofId); + if (peerIt != peerView.end()) { + return &peerIt->proof; + } + + auto proofIt = orphanProofs.find(proofId); + if (proofIt != orphanProofs.end()) { + return &proofIt->second; + } + return nullptr; +} + +bool PeerManager::addProof(Proof &&proof) { + // skip proof verification if we already did it previously + if (recentProofRejects->contains(proof.getId()) || + getProof(proof.getId())) { + return false; + } + + ProofValidationState state; + // Store good proof in a new peer + if (fetchOrCreatePeer(proof, state) != peers.end()) { + return true; + } + + // Keep track of orphan proofs in case they become valid later + if (state.GetResult() == ProofValidationResult::MISSING_UTXO) { + return orphanProofs + .insert(std::make_pair(proof.getId(), std::move(proof))) + .second; + } + + // Keep track of bad proofs to avoid requesting or relaying them + recentProofRejects->insert(proof.getId()); + return false; +} + } // namespace avalanche diff --git a/src/avalanche/processor.h b/src/avalanche/processor.h --- a/src/avalanche/processor.h +++ b/src/avalanche/processor.h @@ -303,6 +303,14 @@ std::vector getPeers() const; std::vector getNodeIdsForPeer(PeerId peerId) const; + /** + * Add a proof and return true if it is verifies successfully. + */ + bool addProof(Proof &&proof); + + /** Return a pointer to a proof or nullptr if we don't have it */ + const Proof *getProof(const ProofId proofId) const; + bool startEventLoop(CScheduler &scheduler); bool stopEventLoop(); diff --git a/src/avalanche/processor.cpp b/src/avalanche/processor.cpp --- a/src/avalanche/processor.cpp +++ b/src/avalanche/processor.cpp @@ -621,4 +621,14 @@ return peerManager->getNodeIdsForPeer(peerId); } +const Proof *Processor::getProof(const ProofId proofId) const { + LOCK(cs_peerManager); + return peerManager->getProof(proofId); +} + +bool Processor::addProof(Proof &&proof) { + LOCK(cs_peerManager); + return peerManager->addProof(std::move(proof)); +} + } // namespace avalanche diff --git a/src/net_processing.cpp b/src/net_processing.cpp --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -1742,6 +1742,15 @@ return recentRejects->contains(txid) || mempool.exists(txid); } + case MSG_AVA_PROOF: { + if (!gArgs.GetBoolArg("-enableavalanche", + AVALANCHE_DEFAULT_ENABLED)) { + // We are not interested, just say we already got it + return true; + } + const avalanche::ProofId proofid(inv.hash); + return g_avalanche->getProof(proofid) != nullptr; + } case MSG_BLOCK: return LookupBlockIndex(BlockHash(inv.hash)) != nullptr; } @@ -2658,7 +2667,8 @@ bool IsAvalancheMessageType(const std::string &msg_type) { return msg_type == NetMsgType::AVAHELLO || msg_type == NetMsgType::AVAPOLL || - msg_type == NetMsgType::AVARESPONSE; + msg_type == NetMsgType::AVARESPONSE || + msg_type == NetMsgType::AVAPROOF; } void PeerManager::ProcessMessage(const Config &config, CNode &pfrom, @@ -3995,6 +4005,20 @@ verifier >> sig; } + if (msg_type == NetMsgType::AVAPROOF) { + // Read the proof. + avalanche::Proof proof; + vRecv >> proof; + + if (!g_avalanche->addProof(std::move(proof)) && + !g_avalanche->getProof(proof.getId())) { + // We didn't add it, and it is not because we already have it. + // It must be bad. + Misbehaving(pfrom, 100, "invalid-proof"); + } + return; + } + if (msg_type == NetMsgType::AVAPOLL) { auto now = std::chrono::steady_clock::now(); int64_t cooldown = diff --git a/test/functional/abc_p2p_avalanche.py b/test/functional/abc_p2p_avalanche.py --- a/test/functional/abc_p2p_avalanche.py +++ b/test/functional/abc_p2p_avalanche.py @@ -3,6 +3,7 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the resolution of forks via avalanche.""" +from io import BytesIO import random from test_framework.avatools import get_stakes @@ -12,10 +13,12 @@ ) from test_framework.mininode import P2PInterface, mininode_lock from test_framework.messages import ( + AvalancheProof, AvalancheResponse, AvalancheVote, CInv, msg_avapoll, + msg_avaproof, msg_tcpavaresponse, NODE_AVALANCHE, NODE_NETWORK, @@ -113,6 +116,25 @@ with mininode_lock: return self.avahello + def on_avaproof(self, message): + with mininode_lock: + assert(self.avaproof is None) + self.avaproof = message + + def wait_for_avaproof(self, timeout=10): + wait_until( + lambda: self.avaproof is not None, + timeout=timeout, + lock=mininode_lock) + + with mininode_lock: + return self.avaproof + + def send_avaproof(self, proof: AvalancheProof): + msg = msg_avaproof() + msg.proof = proof + self.send_message(msg) + class AvalancheTest(BitcoinTestFramework): def set_test_params(self): @@ -343,7 +365,7 @@ wait_until(has_parked_new_tip, timeout=15) assert_equal(node.getbestblockhash(), fork_tip) - # Restart the node + # Restart the node with a proof to test peer discovery self.restart_node(0, self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", @@ -359,6 +381,30 @@ assert avakey.verify_schnorr( avahello.sig, avahello.get_sighash(poll_node)) + self.log.info("Test avaproof messages") + # create a new proof with a different UTXO + new_stakes = get_stakes(node, [blockhashes[1]], addrkey0.key) + proof = node.buildavalancheproof( + proof_sequence, proof_expiration, pubkey.get_bytes().hex(), + new_stakes) + good_proof = AvalancheProof() + good_proof.deserialize(BytesIO(bytes.fromhex(proof))) + poll_node.send_avaproof(good_proof) + + # We should now have a peer + wait_until(lambda: len(node.getavalanchepeerinfo()) == 1) + avapeerinfo = node.getavalanchepeerinfo() + assert_equal(avapeerinfo[0]["proof"], proof) + # But we are still missing node discovery + assert_equal(len(avapeerinfo[0]["nodes"]), 0) + + # Make sure sending a bad proof gets us banned + bad_proof = good_proof + bad_proof.stakes = [] + with node.assert_debug_log( + ['Misbehaving', 'peer=0 (0 -> 100) BAN THRESHOLD EXCEEDED: invalid-proof']): + poll_node.send_avaproof(bad_proof) + if __name__ == '__main__': AvalancheTest().main() diff --git a/test/functional/p2p_invalid_messages.py b/test/functional/p2p_invalid_messages.py --- a/test/functional/p2p_invalid_messages.py +++ b/test/functional/p2p_invalid_messages.py @@ -13,6 +13,7 @@ msg_avahello, msg_avapoll, msg_avaresponse, + msg_avaproof, msg_getdata, msg_headers, msg_inv, @@ -372,6 +373,10 @@ ['Misbehaving', 'peer=9 (40 -> 60): unsolicited-avaresponse']): msg = msg_avaresponse() conn.send_and_ping(msg) + with self.nodes[0].assert_debug_log( + ['Misbehaving', 'peer=9 (60 -> 80): unsolicited-avaproof']): + msg = msg_avaproof() + conn.send_and_ping(msg) self.nodes[0].disconnect_p2ps() def _tweak_msg_data_size(self, message, wrong_size):