diff --git a/src/avalanche/peermanager.h b/src/avalanche/peermanager.h --- a/src/avalanche/peermanager.h +++ b/src/avalanche/peermanager.h @@ -185,6 +185,9 @@ uint64_t getSlotCount() const { return slotCount; } uint64_t getFragmentation() const { return fragmentation; } + std::vector getPeers() const; + std::vector getNodeIdsForPeer(PeerId peerId) const; + private: PeerSet::iterator fetchOrCreatePeer(const Proof &proof); bool addNodeToPeer(const PeerSet::iterator &it); diff --git a/src/avalanche/peermanager.cpp b/src/avalanche/peermanager.cpp --- a/src/avalanche/peermanager.cpp +++ b/src/avalanche/peermanager.cpp @@ -438,4 +438,22 @@ return NO_PEER; } +std::vector PeerManager::getPeers() const { + std::vector vpeers; + for (auto &it : peers.get<0>()) { + vpeers.emplace_back(it); + } + return vpeers; +} + +std::vector PeerManager::getNodeIdsForPeer(PeerId peerId) const { + std::vector nodeids; + auto &nview = nodes.get(); + auto nodeRange = nview.equal_range(peerId); + for (auto it = nodeRange.first; it != nodeRange.second; ++it) { + nodeids.emplace_back(it->nodeid); + } + return nodeids; +} + } // namespace avalanche diff --git a/src/avalanche/processor.h b/src/avalanche/processor.h --- a/src/avalanche/processor.h +++ b/src/avalanche/processor.h @@ -6,6 +6,7 @@ #define BITCOIN_AVALANCHE_PROCESSOR_H #include +#include #include #include #include @@ -280,6 +281,9 @@ CPubKey getSessionPubKey() const; bool sendHello(CNode *pfrom) const; + std::vector getPeers() const; + std::vector getNodeIdsForPeer(PeerId peerId) 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 @@ -587,4 +587,14 @@ } while (nodeid != NO_NODE); } +std::vector Processor::getPeers() const { + LOCK(cs_peerManager); + return peerManager->getPeers(); +} + +std::vector Processor::getNodeIdsForPeer(PeerId peerId) const { + LOCK(cs_peerManager); + return peerManager->getNodeIdsForPeer(peerId); +} + } // namespace avalanche diff --git a/src/rpc/avalanche.cpp b/src/rpc/avalanche.cpp --- a/src/rpc/avalanche.cpp +++ b/src/rpc/avalanche.cpp @@ -3,10 +3,12 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include #include #include #include #include +#include #include #include #include @@ -203,6 +205,110 @@ return HexStr(ss); } +static UniValue getavalanchepeerinfo(const Config &config, + const JSONRPCRequest &request) { + RPCHelpMan{ + "getavalanchepeerinfo", + "Returns data about each connected avalanche peer as a json array of " + "objects.\n", + {}, + RPCResult{ + RPCResult::Type::ARR, + "", + "", + {{ + RPCResult::Type::OBJ, + "", + "", + {{ + {RPCResult::Type::NUM, "peerid", "The peer id"}, + {RPCResult::Type::STR_HEX, "proof", + "The avalanche proof used by this peer"}, + {RPCResult::Type::NUM, "sequence", "The proof's sequence"}, + {RPCResult::Type::NUM_TIME, "expiration", + "The proof's expiration timestamp"}, + {RPCResult::Type::STR_HEX, "master", + "The proof's master public key"}, + { + RPCResult::Type::ARR, + "stakes", + "", + {{ + RPCResult::Type::OBJ, + "", + "", + {{ + {RPCResult::Type::STR_HEX, "txid", ""}, + {RPCResult::Type::NUM, "vout", ""}, + {RPCResult::Type::STR_AMOUNT, "amount", + "The amount in this UTXO"}, + {RPCResult::Type::NUM, "height", + "The height at which this UTXO was mined"}, + {RPCResult::Type::BOOL, "iscoinbase", + "Indicate wether the UTXO is a coinbase"}, + {RPCResult::Type::STR_HEX, "pubkey", ""}, + }}, + }}, + }, + {RPCResult::Type::ARR, + "nodes", + "", + { + {RPCResult::Type::NUM, "nodeid", + "Node id, as returned by getpeerinfo"}, + }}, + }}, + }}, + }, + RPCExamples{HelpExampleCli("getavalanchepeerinfo", "") + + HelpExampleRpc("getavalanchepeerinfo", "")}, + } + .Check(request); + + if (!g_avalanche) { + throw JSONRPCError(RPC_INTERNAL_ERROR, "Avalanche is not initialized"); + } + + UniValue ret(UniValue::VARR); + + for (const auto &peer : g_avalanche->getPeers()) { + UniValue obj(UniValue::VOBJ); + + CDataStream serproof(SER_NETWORK, PROTOCOL_VERSION); + serproof << peer.proof; + + obj.pushKV("peerid", uint64_t(peer.peerid)); + obj.pushKV("proof", HexStr(serproof)); + obj.pushKV("sequence", peer.proof.getSequence()); + obj.pushKV("expiration", peer.proof.getExpirationTime()); + obj.pushKV("master", HexStr(peer.proof.getMaster())); + + UniValue stakes(UniValue::VARR); + for (const auto &s : peer.proof.getStakes()) { + UniValue stake(UniValue::VOBJ); + stake.pushKV("txid", s.getStake().getUTXO().GetTxId().GetHex()); + stake.pushKV("vout", uint64_t(s.getStake().getUTXO().GetN())); + stake.pushKV("amount", ValueFromAmount(s.getStake().getAmount())); + stake.pushKV("height", uint64_t(s.getStake().getHeight())); + stake.pushKV("iscoinbase", s.getStake().isCoinbase()); + stake.pushKV("pubkey", HexStr(s.getStake().getPubkey())); + stakes.push_back(stake); + } + obj.pushKV("stakes", stakes); + + UniValue nodes(UniValue::VARR); + for (const auto &id : g_avalanche->getNodeIdsForPeer(peer.peerid)) { + nodes.push_back(id); + } + obj.pushKV("nodes", nodes); + obj.pushKV("nodecount", uint64_t(peer.node_count)); + + ret.push_back(obj); + } + + return ret; +} + void RegisterAvalancheRPCCommands(CRPCTable &t) { // clang-format off static const CRPCCommand commands[] = { @@ -211,6 +317,7 @@ { "avalanche", "getavalanchekey", getavalanchekey, {}}, { "avalanche", "addavalanchenode", addavalanchenode, {"nodeid"}}, { "avalanche", "buildavalancheproof", buildavalancheproof, {"sequence", "expiration", "master", "stakes"}}, + { "avalanche", "getavalanchepeerinfo", getavalanchepeerinfo, {}}, }; // clang-format on 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 @@ -35,6 +35,8 @@ BLOCK_MISSING = -2 BLOCK_PENDING = -3 +QUORUM_NODE_COUNT = 16 + class TestNode(P2PInterface): @@ -137,7 +139,7 @@ return n - return [get_node() for _ in range(0, 16)] + return [get_node() for _ in range(0, QUORUM_NODE_COUNT)] # Pick on node from the quorum for polling. quorum = get_quorum() @@ -240,14 +242,18 @@ pubkey = privkey.get_pubkey() privatekey = node.get_deterministic_priv_key().key - proof = node.buildavalancheproof(11, 12, pubkey.get_bytes().hex(), [{ - 'txid': coinbases[0]['txid'], - 'vout': coinbases[0]['n'], - 'amount': coinbases[0]['value'], - 'height': coinbases[0]['height'], - 'iscoinbase': True, - 'privatekey': privatekey, - }]) + proof_sequence = 11 + proof_expiration = 12 + proof = node.buildavalancheproof( + proof_sequence, proof_expiration, pubkey.get_bytes().hex(), + [{ + 'txid': coinbases[0]['txid'], + 'vout': coinbases[0]['n'], + 'amount': coinbases[0]['value'], + 'height': coinbases[0]['height'], + 'iscoinbase': True, + 'privatekey': privatekey, + }]) # Activate the quorum. for n in quorum: @@ -255,6 +261,22 @@ n.nodeid, pubkey.get_bytes().hex(), proof) assert success is True + self.log.info("Testing getavalanchepeerinfo...") + avapeerinfo = node.getavalanchepeerinfo() + # There is a single peer because all nodes share the same proof. + assert_equal(len(avapeerinfo), 1) + assert_equal(avapeerinfo[0]["peerid"], 0) + assert_equal(avapeerinfo[0]["nodecount"], len(quorum)) + # The first avalanche node index is 1, because 0 is self.nodes[1]. + assert_equal(sorted(avapeerinfo[0]["nodes"]), + list(range(1, QUORUM_NODE_COUNT + 1))) + assert_equal(avapeerinfo[0]["sequence"], proof_sequence) + assert_equal(avapeerinfo[0]["expiration"], proof_expiration) + assert_equal(avapeerinfo[0]["master"], pubkey.get_bytes().hex()) + assert_equal(avapeerinfo[0]["proof"], proof) + assert_equal(len(avapeerinfo[0]["stakes"]), 1) + assert_equal(avapeerinfo[0]["stakes"][0]["txid"], coinbases[0]['txid']) + def can_find_block_in_poll(hash, resp=BLOCK_ACCEPTED): found_hash = False for n in quorum: