diff --git a/src/avalanche/delegation.h b/src/avalanche/delegation.h --- a/src/avalanche/delegation.h +++ b/src/avalanche/delegation.h @@ -49,6 +49,15 @@ SER_READ(obj, obj.dgid = obj.computeDelegationId()); } + /** + * Verify the delegation: check all the signatures at + * each level. + * + * @param[out] state Verification state of the delegation. + * @param[in] proof Avalanche proof corresponding to this delegation. + * @param[out] auth pubkey for the last level of the delegation. + * @return true if the verification is successful, else false + */ bool verify(DelegationState &state, const Proof &proof, CPubKey &auth) const; }; diff --git a/src/avalanche/processor.h b/src/avalanche/processor.h --- a/src/avalanche/processor.h +++ b/src/avalanche/processor.h @@ -262,10 +262,14 @@ std::unique_ptr chainNotificationsHandler; /** - * Flag indicating that the proof must be registered at first new block - * after IBD + * Flag indicating that this node's proof must be registered at first + * new block after IBD */ bool mustRegisterProof = false; + /** + * Avalanche nodes met via peer-discovery waiting for IBD to end. + */ + std::map> nodesQueue; public: Processor(interfaces::Chain &chain, CConnman *connmanIn, @@ -288,6 +292,15 @@ bool addNode(NodeId nodeid, const Proof &proof, const Delegation &delegation); bool forNode(NodeId nodeid, std::function func) const; + /** + * Queue a node and its proof for registration after IBD + * @returns true if node was queued successfully, else false + */ + bool queueNode(NodeId nodeId, Proof proof, Delegation delegation); + /** Get all queued nodes */ + const std::map> &getNodesQueue(); + bool removeNodeFromQueue(NodeId nodeId); + void clearNodesQueue(); CPubKey getSessionPubKey() const; bool sendHello(CNode *pfrom) const; diff --git a/src/avalanche/processor.cpp b/src/avalanche/processor.cpp --- a/src/avalanche/processor.cpp +++ b/src/avalanche/processor.cpp @@ -151,12 +151,29 @@ void updatedBlockTip() override { LOCK(m_processor->cs_peerManager); - if (m_processor->mustRegisterProof && - !::ChainstateActive().IsInitialBlockDownload()) { - m_processor->peerManager->getPeerId(m_processor->peerData->proof); - m_processor->mustRegisterProof = false; + if (!::ChainstateActive().IsInitialBlockDownload()) { + // Register our own proof + if (m_processor->mustRegisterProof) { + m_processor->peerManager->getPeerId( + m_processor->peerData->proof); + m_processor->mustRegisterProof = false; + } + // Register proofs for nodes met while doing IBD + const auto &nodes = m_processor->getNodesQueue(); + for (auto [nodeId, proofAndDelegation] : nodes) { + Proof proof = std::get<0>(proofAndDelegation); + Delegation delegation = std::get<1>(proofAndDelegation); + // Add the node to the avalanche peers. + bool success = g_avalanche->addNode(nodeId, proof, delegation); + if (success) { + LogPrint(BCLog::NET, "added avalanche node=%d\n", nodeId); + } else { + LogPrint(BCLog::NET, "failed to add avalanche node=%d\n", + nodeId); + } + } + m_processor->clearNodesQueue(); } - m_processor->peerManager->updatedBlockTip(); } }; @@ -396,6 +413,25 @@ return peerManager->forNode(nodeid, std::move(func)); } +bool Processor::queueNode(NodeId nodeId, Proof proof, Delegation delegation) { + const auto [it, success] = nodesQueue.insert({nodeId, {proof, delegation}}); + return success; +} + +bool Processor::removeNodeFromQueue(NodeId nodeId) { + size_t n = nodesQueue.erase(nodeId); + return n == 1 ? true : false; +} + +void Processor::clearNodesQueue() { + nodesQueue.clear(); +} + +const std::map> & +Processor::getNodesQueue() { + return nodesQueue; +} + CPubKey Processor::getSessionPubKey() const { return sessionKey.GetPubKey(); } diff --git a/src/net.h b/src/net.h --- a/src/net.h +++ b/src/net.h @@ -1000,6 +1000,7 @@ AvalancheState() {} avalanche::Delegation delegation; + SchnorrSig sig; }; // m_avalanche_state == nullptr if we're not using avalanche with this peer diff --git a/src/net_processing.cpp b/src/net_processing.cpp --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -2063,6 +2064,29 @@ } } + // Process an avalanche proof item. For now, we assume that peers always + // request a single avalanche proof, and it must be the one this node + // advertised in the avahello message. + if (it != pfrom.vRecvGetData.end() && it->type == MSG_AVALANCHE_PROOF) { + const CInv &inv = *it++; + + if (!g_avalanche || + !gArgs.GetBoolArg("-enableavalanche", AVALANCHE_DEFAULT_ENABLED) || + !gArgs.IsArgSet("-avaproof")) { + vNotFound.push_back(inv); + } else { + const avalanche::ProofId proofid{inv.hash}; + const avalanche::Proof proof = g_avalanche->getProof(); + if (proofid == proof.getId()) { + connman.PushMessage(&pfrom, + msgMaker.Make(NetMsgType::AVAPROOF, proof)); + } else { + // The requested proof is not ours. + vNotFound.push_back(inv); + } + } + } + // Only process one BLOCK item per call, since they're uncommon and can be // expensive to process. if (it != pfrom.vRecvGetData.end() && !pfrom.fPauseSend) { @@ -3954,17 +3978,39 @@ pfrom.m_avalanche_state = std::make_unique(); } + // Store the delegation and signature for later verification. CHashVerifier verifier(&vRecv); - avalanche::Delegation &delegation = pfrom.m_avalanche_state->delegation; - verifier >> delegation; + verifier >> pfrom.m_avalanche_state->delegation; + SchnorrSig &sig = pfrom.m_avalanche_state->sig; + verifier >> sig; + + // Ask for the proof. + std::vector vGetData; + vGetData.emplace_back( + CInv(MSG_AVALANCHE_PROOF, + pfrom.m_avalanche_state->delegation.getProofId())); + m_connman.PushMessage(&pfrom, + msgMaker.Make(NetMsgType::GETDATA, vGetData)); + } + + if (msg_type == NetMsgType::AVAPROOF && g_avalanche) { + if (!pfrom.m_avalanche_state) { + Misbehaving(pfrom, 20, "avaproof-before-avahello"); + return; + } + // Read the proof. avalanche::Proof proof; - // TODO: read proof from message + vRecv >> proof; + if (proof.getStakes().size() > AVALANCHE_MAX_PROOF_STAKES) { Misbehaving(pfrom, 100, "too-large-avalanche-proof"); return; } + // Verify the delegation is internally consistent. + const avalanche::Delegation &delegation = + pfrom.m_avalanche_state->delegation; avalanche::DelegationState state; CPubKey pubkey; if (!delegation.verify(state, proof, pubkey)) { @@ -3972,8 +4018,36 @@ return; } - SchnorrSig sig; - verifier >> sig; + // Use the delegated pubkey to verify the signature received + // in the avahello message. + const uint256 hash = g_avalanche->buildRemoteSighash(&pfrom); + + if (!pubkey.VerifySchnorr(hash, pfrom.m_avalanche_state->sig)) { + Misbehaving(pfrom, 100, "invalid-avalanche-handshake"); + return; + } + + if (::ChainstateActive().IsInitialBlockDownload()) { + // We add the nodes to a queue that is processed when receiving the + // first block after IBD. + if (g_avalanche->queueNode(pfrom.GetId(), proof, delegation)) { + LogPrint(BCLog::NET, + "queued avalanche node for proof verification=%d\n", + pfrom.GetId()); + } else { + // We already have a proof queued from this node + g_avalanche->removeNodeFromQueue(pfrom.GetId()); + Misbehaving(pfrom, 100, "duplicate-avalanche-proof"); + } + } else { + // Add the node to the avalanche peers. + if (g_avalanche->addNode(pfrom.GetId(), proof, delegation)) { + LogPrint(BCLog::NET, "added avalanche node=%d\n", + pfrom.GetId()); + } else { + Misbehaving(pfrom, 100, "failed-avalanche-addnode"); + } + } } // Ignore avalanche requests while importing diff --git a/src/protocol.h b/src/protocol.h --- a/src/protocol.h +++ b/src/protocol.h @@ -310,6 +310,12 @@ * Sent in response to a "avapoll" message. */ extern const char *AVARESPONSE; +/** + * Contains an avalanche::Proof. + * Sent in response to a "getdata" message with inventory type + * MSG_AVALANCHE_PROOF. + */ +extern const char *AVAPROOF; /** * Indicate if the message is used to transmit the content of a block. @@ -495,6 +501,7 @@ MSG_FILTERED_BLOCK = 3, //! Defined in BIP152 MSG_CMPCT_BLOCK = 4, + MSG_AVALANCHE_PROOF = 5, }; /** diff --git a/src/protocol.cpp b/src/protocol.cpp --- a/src/protocol.cpp +++ b/src/protocol.cpp @@ -54,6 +54,7 @@ const char *AVAHELLO = "avahello"; const char *AVAPOLL = "avapoll"; const char *AVARESPONSE = "avaresponse"; +const char *AVAPROOF = "avaproof"; bool IsBlockLike(const std::string &strCommand) { return strCommand == NetMsgType::BLOCK || 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 @@ -15,7 +15,10 @@ AvalancheResponse, AvalancheVote, CInv, + MSG_AVALANCHE_PROOF, msg_avapoll, + msg_avahello, + msg_getdata, msg_tcpavaresponse, NODE_AVALANCHE, NODE_NETWORK, @@ -39,6 +42,7 @@ BLOCK_PENDING = -3 QUORUM_NODE_COUNT = 16 +DUMMY_PROOFID = 1337 class TestNode(P2PInterface): @@ -48,6 +52,7 @@ self.avahello = None self.avaresponses = [] self.avapolls = [] + self.avaproof = None super().__init__() def peer_connect(self, *args, **kwargs): @@ -116,6 +121,30 @@ with mininode_lock: return self.avahello + def send_avahello(self): + msg = msg_avahello() + msg.hello.delegation.proofid = DUMMY_PROOFID + self.send_message(msg) + + def send_getdata(self, inv: List[CInv]): + msg = msg_getdata() + msg.inv = inv + self.send_message(msg) + + 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 get_stakes(coinbases: List[Dict], priv_key: str) -> List[Dict]: @@ -396,12 +425,26 @@ quorum = get_quorum() poll_node = quorum[0] + # Check the handshake sequence + self.log.info("Receive AVAHELLO, request the corresponding proof") avahello = poll_node.wait_for_avahello().hello avakey.set(bytes.fromhex(node.getavalanchekey())) assert avakey.verify_schnorr( avahello.sig, avahello.get_sighash(poll_node)) + poll_node.send_getdata([CInv(MSG_AVALANCHE_PROOF, + avahello.delegation.proofid)]) + poll_node.wait_for_avaproof() + # TODO: deserialize proof and check it in more details + + self.log.info( + "Send an AVAHELLO to the node, check that it ask for our prooof") + poll_node.send_avahello() + poll_node.wait_for_getdata([DUMMY_PROOFID]) + # TODO: reply with a new AVAPROOF message and verify that poll node is + # added to node's avalanche peers + # Check the maximum number of stakes policy blocks = node.generatetoaddress(AVALANCHE_MAX_PROOF_STAKES + 1, addrkey0.address) 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 @@ -63,6 +63,7 @@ MSG_BLOCK = 2 MSG_FILTERED_BLOCK = 3 MSG_CMPCT_BLOCK = 4 +MSG_AVALANCHE_PROOF = 5 MSG_TYPE_MASK = 0xffffffff >> 2 FILTER_TYPE_BASIC = 0 @@ -302,7 +303,8 @@ MSG_TX: "TX", MSG_BLOCK: "Block", MSG_FILTERED_BLOCK: "filtered Block", - MSG_CMPCT_BLOCK: "CompactBlock" + MSG_CMPCT_BLOCK: "CompactBlock", + MSG_AVALANCHE_PROOF: "avalanche proof", } def __init__(self, t=0, h=0): @@ -852,6 +854,25 @@ self.round, repr(self.invs)) +# TODO: implement the Proof with all its attributes +class AvalancheProof: + __slots__ = ("blob") + + def __init__(self, blob: bytes = b""): + self.blob: bytes = blob + + def deserialize(self, f): + self.blob = f.read() + + def serialize(self): + r = b"" + r += self.blob + return r + + def __repr__(self): + return "AvalancheProof({})".format(self.serialize().hex()) + + class AvalancheVote(): __slots__ = ("error", "hash") @@ -1819,6 +1840,25 @@ return "msg_avapoll(poll={})".format(repr(self.poll)) +class msg_avaproof(): + __slots__ = ("proof",) + msgtype = b"avaproof" + + def __init__(self): + self.proof = AvalancheProof() + + def deserialize(self, f): + self.proof.deserialize(f) + + def serialize(self): + r = b"" + r += self.proof.serialize() + return r + + def __repr__(self): + return "msg_avaproof(proof={})".format(repr(self.proof)) + + class msg_avaresponse(): __slots__ = ("response",) msgtype = b"avaresponse" diff --git a/test/functional/test_framework/mininode.py b/test/functional/test_framework/mininode.py --- a/test/functional/test_framework/mininode.py +++ b/test/functional/test_framework/mininode.py @@ -30,6 +30,7 @@ msg_addr, msg_addrv2, msg_avapoll, + msg_avaproof, msg_tcpavaresponse, msg_avahello, msg_block, @@ -74,6 +75,7 @@ b"addr": msg_addr, b"addrv2": msg_addrv2, b"avapoll": msg_avapoll, + b"avaproof": msg_avaproof, b"avaresponse": msg_tcpavaresponse, b"avahello": msg_avahello, b"block": msg_block, @@ -385,6 +387,8 @@ def on_avapoll(self, message): pass + def on_avaproof(self, message): pass + def on_avaresponse(self, message): pass def on_avahello(self, message): pass