Skip to content

Commit

Permalink
Test: add MN-to-MN, MN-to-quorum, MN-to-relay_members and probe MN co…
Browse files Browse the repository at this point in the history
…nnection test coverage
  • Loading branch information
furszy committed Jan 17, 2022
1 parent 80d7d6b commit 72ef11d
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 4 deletions.
6 changes: 4 additions & 2 deletions src/net_processing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1349,11 +1349,13 @@ bool static ProcessMessage(CNode* pfrom, std::string strCommand, CDataStream& vR
// Must have a verack message before anything else
LOCK(cs_main);
Misbehaving(pfrom->GetId(), 1);
LogPrintf("ERROR Received %s before verack\n", strCommand);
return false;
}

if (!pfrom->fFirstMessageReceived.exchange(true)) {
// First message after VERSION/VERACK
if (strCommand != NetMsgType::SENDADDRV2 && // todo: remove this..
!pfrom->fFirstMessageReceived.exchange(true)) {
// First message after VERSION/VERACK (without counting the SENDADDRV2)
pfrom->fFirstMessageReceived = true;
pfrom->fFirstMessageIsMNAUTH = strCommand == NetMsgType::MNAUTH;
if (pfrom->m_masternode_probe_connection && !pfrom->fFirstMessageIsMNAUTH) {
Expand Down
4 changes: 4 additions & 0 deletions src/rpc/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ static const CRPCConvertParam vRPCConvertParams[] = {
{ "waitfornewblock", 0, "timeout" },
{ "walletpassphrase", 1, "timeout" },
{ "walletpassphrase", 2, "staking_only" },
{ "mnconnect", 0, "op_type" },
{ "mnconnect", 1, "arg1" },
{ "mnconnect", 2, "arg2" },
{ "mnconnect", 3, "arg3" },
// Echo with conversion (For testing only)
{ "echojson", 0, "arg0" },
{ "echojson", 1, "arg1" },
Expand Down
69 changes: 68 additions & 1 deletion src/rpc/misc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "messagesigner.h"
#include "net.h"
#include "netbase.h"
#include "tiertwo/net_masternodes.h"
#include "rpc/server.h"
#include "spork.h"
#include "timedata.h"
Expand All @@ -21,7 +22,6 @@
#ifdef ENABLE_WALLET
#include "wallet/rpcwallet.h"
#include "wallet/wallet.h"
#include "wallet/walletdb.h"
#endif
#include "warnings.h"

Expand Down Expand Up @@ -755,6 +755,72 @@ UniValue echo(const JSONRPCRequest& request)
return request.params;
}

std::set<uint256> parseProRegTxHashes(const UniValue& obj, int arrPos)
{
if (!obj[arrPos].isArray()) throw std::runtime_error("error: mnconnect arg1 must be an array of proreg txes hashes");
const auto& array{obj[arrPos].get_array()};
std::set<uint256> vec_dmn_protxhash;
for (unsigned int i = 0; i < array.size(); i++) {
vec_dmn_protxhash.emplace(uint256S(array[i].get_str()));
}
return vec_dmn_protxhash;
}

// mnconnect command operation types
const char* SINGLE_CONN = "single_conn";
const char* QUORUM_MEMBERS_CONN = "quorum_members_conn";
const char* IQR_MEMBERS_CONN = "iqr_members_conn";
const char* PROBE_CONN = "probe_conn";

/* What essentially does is add a pending MN connection
* Can be in the following forms:
* 1) Direct single DMN connection.
* 2) Quorum members connection (set of DMNs to connect).
* 3) Quorum relay members connections (set of DMNs to connect and relay intra-quorum messages).
* 4) Probe DMN connection.
**/
UniValue mnconnect(const JSONRPCRequest& request)
{
if (request.fHelp || request.params.size() > 4) {
throw std::runtime_error(
"mnconnect \"op_type\" \"[pro_tx_hash, pro_tx_hash,..]\"\n"
// todo: complete me..
);
}

const auto& chainparams = Params();
if (!chainparams.IsRegTestNet())
throw std::runtime_error("mnconnect for regression testing (-regtest mode) only");

// First obtain the connection type
const std::string& op_type = request.params[0].get_str();

const auto& mn_connan = g_connman->GetTierTwoConnMan();
std::set<uint256> vec_dmn_protxhash{parseProRegTxHashes(request.params, 1)};
if (op_type == SINGLE_CONN) {
for (const auto& protxhash : vec_dmn_protxhash) {
// if the connection exist or if the dmn doesn't exist,
// it will simply not even try to connect to it.
mn_connan->addPendingMasternode(protxhash);
}
return true;
} else if (op_type == QUORUM_MEMBERS_CONN) {
Consensus::LLMQType llmq_type = (Consensus::LLMQType) request.params[2].get_int();
const uint256& quorum_hash = uint256S(request.params[3].get_str());
mn_connan->setQuorumNodes(llmq_type, quorum_hash, vec_dmn_protxhash);
return true;
} else if (op_type == IQR_MEMBERS_CONN) {
Consensus::LLMQType llmq_type = (Consensus::LLMQType) request.params[2].get_int();
const uint256& quorum_hash = uint256S(request.params[3].get_str());
mn_connan->setMasternodeQuorumRelayMembers(llmq_type, quorum_hash, vec_dmn_protxhash);
return true;
} else if (op_type == PROBE_CONN) {
mn_connan->addPendingProbeConnections(vec_dmn_protxhash);
return true;
}
return false;
}

static const CRPCCommand commands[] =
{ // category name actor (function) okSafe argNames
// --------------------- ------------------------ ----------------------- ------ --------
Expand All @@ -772,6 +838,7 @@ static const CRPCCommand commands[] =
{ "hidden", "echo", &echo, true, {"arg0","arg1","arg2","arg3","arg4","arg5","arg6","arg7","arg8","arg9"}},
{ "hidden", "echojson", &echo, true, {"arg0","arg1","arg2","arg3","arg4","arg5","arg6","arg7","arg8","arg9"}},
{ "hidden", "setmocktime", &setmocktime, true, {"timestamp"} },
{ "hidden", "mnconnect", &mnconnect, true, {"op_type", "arg1"} },
};

void RegisterMiscRPCCommands(CRPCTable &tableRPC)
Expand Down
197 changes: 197 additions & 0 deletions test/functional/p2p_quorum_connect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
#!/usr/bin/env python3
# Copyright (c) 2021 The PIVX Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or https://www.opensource.org/licenses/mit-license.php.
"""Test MN quorum connection flows"""

import time

from test_framework.test_framework import PivxTestFramework
from test_framework.util import (
assert_equal,
assert_true,
bytes_to_hex_str,
disconnect_nodes,
hash256,
hex_str_to_bytes,
wait_until,
)

class DMNConnectionTest(PivxTestFramework):

def set_test_params(self):
# 1 miner, 1 controller, 6 remote dmns
self.num_nodes = 8
self.minerPos = 0
self.controllerPos = 1
self.setup_clean_chain = True
self.extra_args = [["-nuparams=v5_shield:1", "-nuparams=v6_evo:101"]] * self.num_nodes
self.extra_args[0].append("-sporkkey=932HEevBSujW2ud7RfB1YF91AFygbBRQj3de3LyaCRqNzKKgWXi")

def add_new_dmn(self, mns, strType, op_keys=None, from_out=None):
mn = self.register_new_dmn(2 + len(mns),
self.minerPos,
self.controllerPos,
strType,
outpoint=from_out,
op_blskeys=op_keys)
mns.append(mn)
return mn

def check_mn_enabled_count(self, enabled, total):
for node in self.nodes:
node_count = node.getmasternodecount()
assert_equal(node_count['enabled'], enabled)
assert_equal(node_count['total'], total)

def wait_until_mnsync_completed(self):
SYNC_FINISHED = [999] * self.num_nodes
synced = [-1] * self.num_nodes
timeout = time.time() + 160
while synced != SYNC_FINISHED and time.time() < timeout:
synced = [node.mnsync("status")["RequestedMasternodeAssets"]
for node in self.nodes]
if synced != SYNC_FINISHED:
time.sleep(5)
if synced != SYNC_FINISHED:
raise AssertionError("Unable to complete mnsync: %s" % str(synced))

def setup_phase(self):
# Mine 110 blocks
self.log.info("Mining...")
self.miner.generate(110)
assert_equal("success", self.set_spork(self.minerPos, "SPORK_21_LEGACY_MNS_MAX_HEIGHT", 105))
self.sync_blocks()
self.assert_equal_for_all(110, "getblockcount")

# -- DIP3 enforced and SPORK_21 active here --
self.wait_until_mnsync_completed()

# Create 6 DMNs and init the remote nodes
for i in range(6):
mn = self.add_new_dmn(self.mns, "fund")
self.nodes[mn.idx].initmasternode(mn.operator_sk, "", True)
time.sleep(1)
self.miner.generate(1)
self.sync_blocks()

# enabled/total masternodes: 6/6
self.check_mn_enabled_count(6, 6)
# Check status from remote nodes
assert_equal([self.nodes[idx].getmasternodestatus()['status'] for idx in range(2, self.num_nodes)],
["Ready"] * (self.num_nodes - 2))
self.log.info("All masternodes ready")

def disconnect_peers(self, node):
for i in range(0, 7):
disconnect_nodes(node, i)
assert_equal(len(node.getpeerinfo()), 0)

def check_peer_info(self, peer_info, mn, is_iqr_conn, inbound = False):
assert_equal(peer_info["masternode"], True)
assert_equal(peer_info["verif_mn_proreg_tx_hash"], mn.proTx)
assert_equal(peer_info["verif_mn_operator_pubkey_hash"], bytes_to_hex_str(hash256(hex_str_to_bytes(mn.operator_pk))))
assert_equal(peer_info["masternode_iqr_conn"], is_iqr_conn)
# An inbound connection has obviously a different internal port.
if not inbound:
assert_equal(peer_info["addr"], mn.ipport)

def check_peers_info(self, peers_info, quorum_members, is_iqr_conn, inbound = False):
for quorum_node in quorum_members:
found = False
for peer in peers_info:
if "verif_mn_proreg_tx_hash" in peer and peer["verif_mn_proreg_tx_hash"] == quorum_node.proTx:
self.check_peer_info(peer, quorum_node, is_iqr_conn, inbound)
found = True
break
if not found:
print(peers_info)
assert_true(found, "MN connection not found for ip: " + str(quorum_node.ipport))

def has_mn_auth_connection(self, node, expected_proreg_tx_hash):
peer_info = node.getpeerinfo()
return (len(peer_info) == 1) and (peer_info[0]["verif_mn_proreg_tx_hash"] == expected_proreg_tx_hash)

def run_test(self):
self.disable_mocktime()
self.miner = self.nodes[self.minerPos]
self.mns = []
# Create and start 6 DMN
self.setup_phase()

##############################################################
# 1) Disconnect peers from DMN and add a direct DMN connection
##############################################################
self.log.info("1) Testing single DMN connection, disconnecting nodes..")
mn1 = self.mns[0]
mn1_node = self.nodes[mn1.idx]
self.disconnect_peers(mn1_node)

self.log.info("disconnected, connecting to a single DMN and auth him..")
# Now try to connect to the second DMN only
mn2 = self.mns[1]
assert mn1_node.mnconnect("single_conn", [mn2.proTx])
wait_until(lambda: self.has_mn_auth_connection(mn1_node, mn2.proTx), timeout=60)
peer_info = mn1_node.getpeerinfo()[0]
# Check connected peer info: same DMN and mnauth succeeded
self.check_peer_info(peer_info, mn2, is_iqr_conn=False)
# Same for the the other side
mn2_node = self.nodes[mn2.idx]
peers_info = mn2_node.getpeerinfo()
self.check_peers_info(peers_info, [mn1], is_iqr_conn=False, inbound=True)
self.log.info("Completed DMN-to-DMN authenticated connection!")

################################################################
# 2) Disconnect peers from DMN and add quorum members connection
################################################################
self.log.info("2) Testing quorum connections, disconnecting nodes..")
mn3 = self.mns[2]
mn4 = self.mns[3]
mn5 = self.mns[4]
mn6 = self.mns[5]
quorum_nodes = [mn3, mn4, mn5, mn6]
self.disconnect_peers(mn2_node)
self.log.info("disconnected, connecting to quorum members..")
quorum_members = [mn2.proTx, mn3.proTx, mn4.proTx, mn5.proTx, mn6.proTx]
assert mn2_node.mnconnect("quorum_members_conn", quorum_members, 1, mn2_node.getbestblockhash())
# Check connected peer info: same quorum members and mnauth succeeded
wait_until(lambda: len(mn2_node.getpeerinfo()) == 4, timeout=90)
time.sleep(5) # wait a bit more to receive the mnauth
peers_info = mn2_node.getpeerinfo()
self.check_peers_info(peers_info, quorum_nodes, is_iqr_conn=False)
# Same for the other side (MNs receiving the new connection)
for mn_node in [self.nodes[mn3.idx], self.nodes[mn4.idx], self.nodes[mn5.idx], self.nodes[mn6.idx]]:
self.check_peers_info(mn_node.getpeerinfo(), [mn2], is_iqr_conn=False, inbound=True)
self.log.info("Completed DMN-to-quorum connections!")

##################################################################################
# 3) Update already connected quorum members in (2) to be intra-quorum connections
##################################################################################
self.log.info("3) Testing connections update to be intra-quorum relay connections")
assert mn2_node.mnconnect("iqr_members_conn", quorum_members, 1, mn2_node.getbestblockhash())
peers_info = mn2_node.getpeerinfo()
self.check_peers_info(peers_info, quorum_nodes, is_iqr_conn=True)
# Same for the other side (MNs receiving the new connection)
for mn_node in [self.nodes[mn3.idx], self.nodes[mn4.idx], self.nodes[mn5.idx], self.nodes[mn6.idx]]:
assert mn_node.mnconnect("iqr_members_conn", quorum_members, 1, mn2_node.getbestblockhash())
self.check_peers_info(mn_node.getpeerinfo(), [mn2], is_iqr_conn=True, inbound=True)
self.log.info("Completed update to quorum relay members!")

###########################################
# 4) Now test the connections probe process
###########################################
self.log.info("3) Testing MN probe connection process..")
# Take mn6, disconnect all the nodes and try to probe connection to one of them
mn6_node = self.nodes[mn6.idx]
self.disconnect_peers(mn6_node)
self.log.info("disconnected, probing MN connection..")
with mn6_node.assert_debug_log(["Masternode probe successful for " + mn5.proTx]):
assert mn_node.mnconnect("probe_conn", [mn5.proTx])
time.sleep(10) # wait a bit until the connection gets established
self.log.info("Completed MN connection probe!")


if __name__ == '__main__':
DMNConnectionTest().main()


2 changes: 1 addition & 1 deletion test/functional/test_framework/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from .util import hex_str_to_bytes, bytes_to_hex_str

MIN_VERSION_SUPPORTED = 60001
MY_VERSION = 70924
MY_VERSION = 70925
MY_SUBVERSION = "/python-mininode-tester:0.0.3/"
MY_RELAY = 1 # from version 70001 onwards, fRelay should be appended to version messages (BIP37)

Expand Down
1 change: 1 addition & 0 deletions test/functional/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
'p2p_addr_relay.py',
'p2p_addrv2_relay.py',
'p2p_invalid_messages.py',
'p2p_quorum_connect.py',
'feature_reindex.py', # ~ 205 sec
'feature_logging.py', # ~ 195 sec
'wallet_multiwallet.py', # ~ 190 sec
Expand Down

0 comments on commit 72ef11d

Please sign in to comment.