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 @@ -292,6 +292,8 @@ */ uint256 buildRemoteChallenge(CNode *pfrom) const; + const Proof getProof() const; + std::vector getPeers() const; std::vector getNodeIdsForPeer(PeerId peerId) const; diff --git a/src/avalanche/processor.cpp b/src/avalanche/processor.cpp --- a/src/avalanche/processor.cpp +++ b/src/avalanche/processor.cpp @@ -441,6 +441,11 @@ return true; } +const Proof Processor::getProof() const { + assert(peerData); + return peerData->proof; +} + bool Processor::startEventLoop(CScheduler &scheduler) { return eventLoop.startEventLoop( scheduler, [this]() { this->runEventLoop(); }, AVALANCHE_TIME_STEP); diff --git a/src/net.h b/src/net.h --- a/src/net.h +++ b/src/net.h @@ -998,6 +998,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 { + avalanche::ProofId proofid{inv.hash}; + 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,34 @@ 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; + SchnorrSig &sig = pfrom.m_avalanche_state->sig; + verifier >> sig; + + // Ask for the proof. + std::vector vGetData; + vGetData.emplace_back( + CInv(MSG_AVALANCHE_PROOF, delegation.getProofId())); + m_connman.PushMessage(&pfrom, + msgMaker.Make(NetMsgType::GETDATA, vGetData)); + } + + if (msg_type == NetMsgType::AVAPROOF && g_avalanche) { + // 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. + avalanche::Delegation &delegation = pfrom.m_avalanche_state->delegation; avalanche::DelegationState state; CPubKey pubkey; if (!delegation.verify(state, proof, pubkey)) { @@ -3972,8 +4013,25 @@ return; } - SchnorrSig sig; - verifier >> sig; + // Use the delegated pubkey to verify the signature received + // in the avahello message. + const uint256 hash = g_avalanche->buildRemoteChallenge(&pfrom); + + bool success; + success = pubkey.VerifySchnorr(hash, pfrom.m_avalanche_state->sig); + if (!success) { + Misbehaving(pfrom, 100, "invalid-avalanche-handshake"); + return; + } + + // Add the node the avalanche peers. + success = g_avalanche->addNode(pfrom.GetId(), proof, delegation); + if (success) { + LogPrint(BCLog::NET, "added avalanche node=%d\n", pfrom.GetId()); + } else { + LogPrint(BCLog::NET, "failed to add avalanche node=%d\n", + pfrom.GetId()); + } } // 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]: @@ -381,13 +410,26 @@ quorum = get_quorum() poll_node = quorum[0] - # Check the avahello is consistent + # 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