diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -483,6 +483,7 @@ pow.cpp rest.cpp rpc/abc.cpp + rpc/avalanche.cpp rpc/blockchain.cpp rpc/command.cpp rpc/jsonrpcrequest.cpp diff --git a/src/Makefile.am b/src/Makefile.am --- a/src/Makefile.am +++ b/src/Makefile.am @@ -289,6 +289,7 @@ pow.cpp \ rest.cpp \ rpc/abc.cpp \ + rpc/avalanche.cpp \ rpc/blockchain.cpp \ rpc/command.cpp \ rpc/jsonrpcrequest.cpp \ diff --git a/src/avalanche.h b/src/avalanche.h --- a/src/avalanche.h +++ b/src/avalanche.h @@ -6,6 +6,7 @@ #define BITCOIN_AVALANCHE_H #include +#include #include #include // for CInv #include @@ -331,6 +332,8 @@ Mutex cs_running; std::condition_variable cond_running; + CKey sessionKey; + public: AvalancheProcessor(CConnman *connmanIn); ~AvalancheProcessor(); @@ -350,6 +353,8 @@ bool addPeer(NodeId nodeid, int64_t score); + CPubKey getSessionPubKey() const { return sessionKey.GetPubKey(); } + bool startEventLoop(CScheduler &scheduler); bool stopEventLoop(); diff --git a/src/avalanche.cpp b/src/avalanche.cpp --- a/src/avalanche.cpp +++ b/src/avalanche.cpp @@ -132,7 +132,10 @@ : connman(connmanIn), queryTimeoutDuration( AVALANCHE_DEFAULT_QUERY_TIMEOUT_DURATION_MILLISECONDS), - round(0), stopRequest(false), running(false) {} + round(0), stopRequest(false), running(false) { + // Pick a random key for the session. + sessionKey.MakeNewKey(true); +} AvalancheProcessor::~AvalancheProcessor() { stopEventLoop(); @@ -176,11 +179,51 @@ return it->second.getConfidence(); } +namespace { +/** + * When using TCP, we need to sign all messages as the transport layer is not + * secure. + */ +class TCPAvalancheResponse { + AvalancheResponse response; + std::array sig; + +public: + TCPAvalancheResponse(AvalancheResponse responseIn, const CKey &key) + : response(std::move(responseIn)) { + CHashWriter hasher(SER_GETHASH, 0); + hasher << response; + const uint256 hash = hasher.GetHash(); + + // Now let's sign! + std::vector vchSig; + if (key.SignSchnorr(hash, vchSig)) { + // Schnorr sigs are 64 bytes in size. + assert(vchSig.size() == 64); + std::copy(vchSig.begin(), vchSig.end(), sig.begin()); + } else { + sig.fill(0); + } + } + + // serialization support + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream &s, Operation ser_action) { + READWRITE(response); + READWRITE(sig); + } +}; +} // namespace + void AvalancheProcessor::sendResponse(CNode *pfrom, AvalancheResponse response) const { connman->PushMessage( - pfrom, CNetMsgMaker(pfrom->GetSendVersion()) - .Make(NetMsgType::AVARESPONSE, std::move(response))); + pfrom, + CNetMsgMaker(pfrom->GetSendVersion()) + .Make(NetMsgType::AVARESPONSE, + TCPAvalancheResponse(std::move(response), sessionKey))); } bool AvalancheProcessor::registerVotes( diff --git a/src/pubkey.cpp b/src/pubkey.cpp --- a/src/pubkey.cpp +++ b/src/pubkey.cpp @@ -211,7 +211,7 @@ return false; } - return secp256k1_schnorr_verify(secp256k1_context_verify, &vchSig[0], + return secp256k1_schnorr_verify(secp256k1_context_verify, vchSig.data(), hash.begin(), &pubkey); } diff --git a/src/rpc/avalanche.cpp b/src/rpc/avalanche.cpp new file mode 100644 --- /dev/null +++ b/src/rpc/avalanche.cpp @@ -0,0 +1,43 @@ +// Copyright (c) 2020 The Bitcoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include +#include +#include +#include + +#include + +static UniValue getavalanchekey(const Config &config, + const JSONRPCRequest &request) { + if (request.fHelp || request.params.size() != 0) { + throw std::runtime_error( + "getavalanchekey\n" + "Returns the key used to sign avalanche messages.\n" + "\nExamples:\n" + + HelpExampleRpc("getavalanchekey", "")); + } + + if (!g_avalanche) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, + "Avalanche is not initialized"); + } + + return HexStr(g_avalanche->getSessionPubKey()); +} + +// clang-format off +static const ContextFreeRPCCommand commands[] = { + // category name actor (function) argNames + // ------------------- ------------------------ ---------------------- ---------- + { "avalanche", "getavalanchekey", getavalanchekey, {}}, +}; +// clang-format on + +void RegisterAvalancheRPCCommands(CRPCTable &t) { + for (unsigned int vcidx = 0; vcidx < ARRAYLEN(commands); vcidx++) { + t.appendCommand(commands[vcidx].name, &commands[vcidx]); + } +} diff --git a/src/rpc/register.h b/src/rpc/register.h --- a/src/rpc/register.h +++ b/src/rpc/register.h @@ -22,6 +22,8 @@ void RegisterRawTransactionRPCCommands(CRPCTable &tableRPC); /** Register ABC RPC commands */ void RegisterABCRPCCommands(CRPCTable &tableRPC); +/** Register Avalanche RPC commands */ +void RegisterAvalancheRPCCommands(CRPCTable &tableRPC); /** * Register all context-free (legacy) RPC commands, except for wallet and dump @@ -34,6 +36,7 @@ RegisterMiningRPCCommands(t); RegisterRawTransactionRPCCommands(t); RegisterABCRPCCommands(t); + RegisterAvalancheRPCCommands(t); } /** 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 @@ -5,10 +5,11 @@ """Test the resolution of forks via avalanche.""" import random -from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import assert_equal, wait_until from test_framework.mininode import P2PInterface, mininode_lock from test_framework.messages import AvalancheVote, CInv, msg_avapoll +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal, wait_until +from test_framework import schnorr BLOCK_ACCEPTED = 0 @@ -58,13 +59,22 @@ # Generate many block and poll for them. node.generate(100) + # Get the key so we can verify signatures. + avakey = bytes.fromhex(node.getavalanchekey()) + self.log.info("Poll for the chain tip...") best_block_hash = int(node.getbestblockhash(), 16) poll_node.send_poll([best_block_hash]) poll_node.wait_for_avaresponse() def assert_response(response, expected): - votes = response.votes + r = response.response + assert_equal(r.cooldown, 0) + + # Verify signature. + assert schnorr.verify(response.sig, avakey, r.get_hash()) + + votes = r.votes self.log.info("response: {}".format(repr(response))) assert_equal(len(votes), len(expected)) for i in range(0, len(votes)): diff --git a/test/functional/rpc_help.py b/test/functional/rpc_help.py --- a/test/functional/rpc_help.py +++ b/test/functional/rpc_help.py @@ -28,7 +28,7 @@ # command titles titles = [line[3:-3] for line in node.help().splitlines() if line.startswith('==')] - components = ['Blockchain', 'Control', 'Generating', + components = ['Avalanche', 'Blockchain', 'Control', 'Generating', 'Mining', 'Network', 'Rawtransactions', 'Util'] if self.is_wallet_compiled(): 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 @@ -829,11 +829,35 @@ r += ser_vector(self.votes) return r + def get_hash(self): + return hash256(self.serialize()) + def __repr__(self): return "AvalancheResponse(round={}, cooldown={}, votes={})".format( self.round, self.cooldown, repr(self.votes)) +class TCPAvalancheResponse(): + + def __init__(self, response=AvalancheResponse()): + self.response = response + self.sig = b"\0" * 64 + + def deserialize(self, f): + self.response.deserialize(f) + self.sig = f.read(64) + + def serialize(self): + r = b"" + r += self.response.serialize() + r += self.sig + return r + + def __repr__(self): + return "TCPAvalancheResponse(response={}, sig={})".format( + repr(self.response), self.sig) + + class CPartialMerkleTree: __slots__ = ("fBad", "nTransactions", "vBits", "vHash") @@ -1430,3 +1454,21 @@ def __repr__(self): return "msg_avaresponse(response={})".format(repr(self.response)) + + +class msg_tcpavaresponse(): + command = b"avaresponse" + + def __init__(self): + self.response = TCPAvalancheResponse() + + def deserialize(self, f): + self.response.deserialize(f) + + def serialize(self): + r = b"" + r += self.response.serialize() + return r + + def __repr__(self): + return "msg_tcpavaresponse(response={})".format(repr(self.response)) 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 @@ -26,7 +26,7 @@ MIN_VERSION_SUPPORTED, msg_addr, msg_avapoll, - msg_avaresponse, + msg_tcpavaresponse, msg_block, MSG_BLOCK, msg_blocktxn, @@ -61,7 +61,7 @@ MESSAGEMAP = { b"addr": msg_addr, b"avapoll": msg_avapoll, - b"avaresponse": msg_avaresponse, + b"avaresponse": msg_tcpavaresponse, b"block": msg_block, b"blocktxn": msg_blocktxn, b"cmpctblock": msg_cmpctblock,