Skip to content

Commit

Permalink
Functional test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
panleone committed Aug 31, 2023
1 parent 9bf4592 commit c6ad3e0
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 98 deletions.
2 changes: 2 additions & 0 deletions src/evo/providertx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include "bls/key_io.h"
#include "key_io.h"
#include "primitives/transaction.h"
#include "uint256.h"

std::string ProRegPL::MakeSignString() const
Expand Down Expand Up @@ -40,6 +41,7 @@ void ProRegPL::ToJson(UniValue& obj) const
obj.pushKV("version", nVersion);
obj.pushKV("collateralHash", collateralOutpoint.hash.ToString());
obj.pushKV("collateralIndex", (int)collateralOutpoint.n);
obj.pushKV("nullifier", shieldCollateral.input.nullifier.ToString());
obj.pushKV("service", addr.ToString());
obj.pushKV("ownerAddress", EncodeDestination(keyIDOwner));
obj.pushKV("operatorPubKey", bls::EncodePublic(Params(), pubKeyOperator));
Expand Down
15 changes: 4 additions & 11 deletions src/rpc/masternode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -146,16 +146,9 @@ static inline bool filter(const std::string& str, const std::string& strFilter)
return str.find(strFilter) != std::string::npos;
}

static inline bool filterMasternode(const UniValue& dmno, const std::string& strFilter, bool fEnabled)
static inline bool filterMasternode(const UniValue& dmno, const std::string& strFilter, bool fEnabled, bool isShield)
{
return strFilter.empty() || (filter("ENABLED", strFilter) && fEnabled)
|| (filter("POSE_BANNED", strFilter) && !fEnabled)
|| (filter(dmno["proTxHash"].get_str(), strFilter))
|| (filter(dmno["collateralHash"].get_str(), strFilter))
|| (filter(dmno["collateralAddress"].get_str(), strFilter))
|| (filter(dmno["dmnstate"]["ownerAddress"].get_str(), strFilter))
|| (filter(dmno["dmnstate"]["operatorPubKey"].get_str(), strFilter))
|| (filter(dmno["dmnstate"]["votingAddress"].get_str(), strFilter));
return strFilter.empty() || (filter("ENABLED", strFilter) && fEnabled) || (filter("POSE_BANNED", strFilter) && !fEnabled) || (filter("SHIELD", strFilter) && isShield) || (filter(dmno["proTxHash"].get_str(), strFilter)) || (filter(dmno["collateralHash"].get_str(), strFilter)) || (!isShield && filter(dmno["collateralAddress"].get_str(), strFilter)) || (filter(dmno["dmnstate"]["ownerAddress"].get_str(), strFilter)) || (filter(dmno["dmnstate"]["operatorPubKey"].get_str(), strFilter)) || (filter(dmno["dmnstate"]["votingAddress"].get_str(), strFilter));
}

UniValue listmasternodes(const JSONRPCRequest& request)
Expand Down Expand Up @@ -198,7 +191,7 @@ UniValue listmasternodes(const JSONRPCRequest& request)
auto mnList = deterministicMNManager->GetListAtChainTip();
mnList.ForEachMN(false, [&](const CDeterministicMNCPtr& dmn) {
UniValue obj = DmnToJson(dmn);
if (filterMasternode(obj, strFilter, !dmn->IsPoSeBanned())) {
if (filterMasternode(obj, strFilter, !dmn->IsPoSeBanned(), !dmn->nullifier.IsNull())) {
ret.push_back(obj);
}
});
Expand All @@ -224,7 +217,7 @@ UniValue listmasternodes(const JSONRPCRequest& request)
if (dmn) {
UniValue obj = DmnToJson(dmn);
bool fEnabled = !dmn->IsPoSeBanned();
if (filterMasternode(obj, strFilter, fEnabled)) {
if (filterMasternode(obj, strFilter, fEnabled, false)) {
// Added for backward compatibility with legacy masternodes
obj.pushKV("type", "deterministic");
obj.pushKV("txhash", obj["proTxHash"].get_str());
Expand Down
20 changes: 13 additions & 7 deletions test/functional/test_framework/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,11 +350,12 @@ def __repr__(self):


class COutPoint:
__slots__ = ("hash", "n")
__slots__ = ("hash", "n", "transparent")

def __init__(self, hash=0, n=0):
def __init__(self, hash=0, n=0, transparent=True):
self.hash = hash
self.n = n
self.transparent = transparent

def deserialize(self, f):
self.hash = deser_uint256(f)
Expand All @@ -380,7 +381,8 @@ def __repr__(self):
return "COutPoint(hash=%064x n=%i)" % (self.hash, self.n)

def to_json(self):
return {"txid": "%064x" % self.hash, "vout": self.n}
voutStr = "vout" if self.transparent else "vShieldedOutput"
return {"txid": "%064x" % self.hash, voutStr: self.n}


NullOutPoint = COutPoint(0, 0xffffffff)
Expand Down Expand Up @@ -1575,10 +1577,12 @@ def serialize(self):


# PIVX Classes
# NB: for shielded masternode the field collateral is the ShieldOutPoint of the shield collateral
# notice the difference from the ProRegTx in which the collateral is the Null default value
class Masternode(object):
__slots__ = ("idx", "owner", "operator_pk", "voting", "ipport", "payee", "operator_sk", "proTx", "collateral")
__slots__ = ("idx", "owner", "operator_pk", "voting", "ipport", "payee", "operator_sk", "proTx", "collateral", "nullifier", "transparent")

def __init__(self, idx, owner_addr, operator_pk, voting_addr, ipport, payout_addr, operator_sk):
def __init__(self, idx, owner_addr, operator_pk, voting_addr, ipport, payout_addr, operator_sk, transparent):
self.idx = idx
self.owner = owner_addr
self.operator_pk = operator_pk
Expand All @@ -1588,16 +1592,18 @@ def __init__(self, idx, owner_addr, operator_pk, voting_addr, ipport, payout_add
self.operator_sk = operator_sk
self.proTx = None
self.collateral = None
self.nullifier = "%064x" % 0 if transparent else None
self.transparent = transparent

def revoked(self):
self.ipport = "[::]:0"
self.operator_pk = ""
self.operator_sk = None

def __repr__(self):
return "Masternode(idx=%d, owner=%s, operator=%s, voting=%s, ip=%s, payee=%s, opkey=%s, protx=%s, collateral=%s)" % (
return "Masternode(idx=%d, owner=%s, operator=%s, voting=%s, ip=%s, payee=%s, opkey=%s, protx=%s, collateral=%s, transparent=%s)" % (
self.idx, str(self.owner), str(self.operator_pk), str(self.voting), str(self.ipport),
str(self.payee), str(self.operator_sk), str(self.proTx), str(self.collateral)
str(self.payee), str(self.operator_sk), str(self.proTx), str(self.collateral), str(self.transparent)
)

def __str__(self):
Expand Down
65 changes: 44 additions & 21 deletions test/functional/test_framework/test_framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -1117,13 +1117,13 @@ def setupDMN(self,
break
assert_greater_than(collateralTxId_n, -1)
assert_greater_than(json_tx["confirmations"], 0)
proTxId = mnOwner.protx_register(collateralTxId, collateralTxId_n, ipport, ownerAdd,
proTxId = mnOwner.protx_register(collateralTxId, collateralTxId_n, True, ipport, ownerAdd,
bls_keypair["public"], votingAdd, collateralAdd)
elif strType == "external":
self.log.info("Setting up ProRegTx with collateral externally-signed...")
# send the tx from the miner
payoutAdd = mnOwner.getnewaddress("payout")
register_res = miner.protx_register_prepare(outpoint.hash, outpoint.n, ipport, ownerAdd,
register_res = miner.protx_register_prepare(outpoint.hash, outpoint.n, True, ipport, ownerAdd,
bls_keypair["public"], votingAdd, payoutAdd)
self.log.info("ProTx prepared")
message_to_sign = register_res["signMessage"]
Expand Down Expand Up @@ -1218,19 +1218,20 @@ def protx_register_fund(self, miner, controller, dmn, collateral_addr, op_rew=No
Create a ProReg tx, which references an 100 PIV UTXO as collateral.
The controller node owns the collateral and creates the ProReg tx.
"""
def protx_register(self, miner, controller, dmn, collateral_addr):
def protx_register(self, miner, controller, dmn, collateral_addr, transparent):
# send to the owner the exact collateral tx amount
funding_txid = miner.sendtoaddress(collateral_addr, Decimal('100'))
# send another output to be used for the fee of the proReg tx
miner.sendtoaddress(collateral_addr, Decimal('1'))
feeAddr = collateral_addr if transparent else controller.getnewaddress("feeAddr")
miner.sendtoaddress(feeAddr, Decimal('1'))
# confirm and verify reception
miner.generate(1)
self.sync_blocks([miner, controller])
json_tx = controller.getrawtransaction(funding_txid, True)
assert_greater_than(json_tx["confirmations"], 0)
# create and send the ProRegTx
dmn.collateral = COutPoint(int(funding_txid, 16), get_collateral_vout(json_tx))
dmn.proTx = controller.protx_register(funding_txid, dmn.collateral.n, dmn.ipport, dmn.owner,
# create and send the ProRegTx, FOR SHIELD DMNS THIS IS NOT THE COLLATERAL CONTAINED IN THE PROREGTX (which is instead the null COutPoint (0,-1))
dmn.collateral = COutPoint(int(funding_txid, 16), get_collateral_vout(json_tx)) if transparent else COutPoint(int(funding_txid, 16), 0, transparent)
dmn.proTx = controller.protx_register(funding_txid, dmn.collateral.n, transparent, dmn.ipport, dmn.owner,
dmn.operator_pk, dmn.voting, dmn.payee)

"""
Expand All @@ -1249,7 +1250,7 @@ def protx_register_ext(self, miner, controller, dmn, outpoint, fSubmit):
outpoint = COutPoint(int(funding_txid, 16), get_collateral_vout(json_tx))
dmn.collateral = outpoint
# Prepare the message to be signed externally by the owner of the collateral (the controller)
reg_tx = miner.protx_register_prepare("%064x" % outpoint.hash, outpoint.n, dmn.ipport, dmn.owner,
reg_tx = miner.protx_register_prepare("%064x" % outpoint.hash, outpoint.n, True, dmn.ipport, dmn.owner,
dmn.operator_pk, dmn.voting, dmn.payee)
sig = controller.signmessage(reg_tx["collateralAddress"], reg_tx["signMessage"])
if fSubmit:
Expand All @@ -1270,7 +1271,7 @@ def protx_register_ext(self, miner, controller, dmn, outpoint, fSubmit):
If not provided, a new address-key pair is generated.
:return: dmn: (Masternode) the deterministic masternode object
"""
def register_new_dmn(self, idx, miner_idx, controller_idx, strType,
def register_new_dmn(self, idx, miner_idx, controller_idx, strType, transparent,
payout_addr=None, outpoint=None, op_blskeys=None):
# Prepare remote node
assert idx != miner_idx
Expand All @@ -1280,19 +1281,21 @@ def register_new_dmn(self, idx, miner_idx, controller_idx, strType,
mn_node = self.nodes[idx]

# Generate ip and addresses/keys
collateral_addr = controller_node.getnewaddress("mncollateral-%d" % idx)
collateral_addr = controller_node.getnewaddress("mncollateral-%d" % idx) if transparent else controller_node.getnewshieldaddress("shieldmncollateral-%d" % idx)
if payout_addr is None:
payout_addr = collateral_addr
dmn = create_new_dmn(idx, controller_node, payout_addr, op_blskeys)
payout_addr = collateral_addr if transparent else controller_node.getnewaddress("mncollateral-%d" % idx)
dmn = create_new_dmn(idx, controller_node, payout_addr, op_blskeys, transparent)

# Create ProRegTx
self.log.info("Creating%s proRegTx for deterministic masternode idx=%d..." % (
" and funding" if strType == "fund" else "", idx))
if strType == "fund":
assert (transparent)
self.protx_register_fund(miner_node, controller_node, dmn, collateral_addr)
elif strType == "internal":
self.protx_register(miner_node, controller_node, dmn, collateral_addr)
self.protx_register(miner_node, controller_node, dmn, collateral_addr, transparent)
elif strType == "external":
assert (transparent)
self.protx_register_ext(miner_node, controller_node, dmn, outpoint, True)
else:
raise Exception("Type %s not available" % strType)
Expand All @@ -1307,7 +1310,7 @@ def register_new_dmn(self, idx, miner_idx, controller_idx, strType,
assert dmn.proTx in mn_node.protx_list(False)

# check coin locking
assert is_coin_locked_by(controller_node, dmn.collateral)
assert is_coin_locked_by(controller_node, dmn.collateral, dmn.transparent)

# check json payload against local dmn object
self.check_proreg_payload(dmn, json_tx)
Expand Down Expand Up @@ -1337,23 +1340,38 @@ def check_mn_list_on_node(self, idx, mns):
assert_equal(mn.voting, mn2["dmnstate"]["votingAddress"])
assert_equal(mn.ipport, mn2["dmnstate"]["service"])
assert_equal(mn.payee, mn2["dmnstate"]["payoutAddress"])
assert_equal(collateral["txid"], mn2["collateralHash"])
assert_equal(collateral["vout"], mn2["collateralIndex"])
assert_equal(mn.nullifier, mn2["nullifier"])
# Usual story, For shield Dmns the value we store in collateral (i.e. the sapling outpoint referring to the note)
# Is different from the default null collateral in the ProRegTx
if mn.transparent:
assert_equal(collateral["txid"], mn2["collateralHash"])
assert_equal(collateral["vout"], mn2["collateralIndex"])
else:
assert_equal("%064x" % 0, mn2["collateralHash"])
assert_equal(-1, mn2["collateralIndex"])

def check_proreg_payload(self, dmn, json_tx):
assert "payload" in json_tx
# null hash if funding collateral
collateral_hash = 0 if int(json_tx["txid"], 16) == dmn.collateral.hash \
else dmn.collateral.hash
collateral_n = dmn.collateral.n
# null Outpoint if dmn is shielded
if not dmn.transparent:
collateral_hash = 0
collateral_n = -1
pl = json_tx["payload"]
assert_equal(pl["version"], 1)
assert_equal(pl["version"], 2)
assert_equal(pl["collateralHash"], "%064x" % collateral_hash)
assert_equal(pl["collateralIndex"], dmn.collateral.n)
assert_equal(pl["collateralIndex"], collateral_n)
assert_equal(pl["service"], dmn.ipport)
assert_equal(pl["ownerAddress"], dmn.owner)
assert_equal(pl["votingAddress"], dmn.voting)
assert_equal(pl["operatorPubKey"], dmn.operator_pk)
assert_equal(pl["payoutAddress"], dmn.payee)
# fix the nullifier
dmn.nullifier = pl["nullifier"]


# ------------------------------------------------------

Expand Down Expand Up @@ -1383,17 +1401,18 @@ def __init__(self,
class PivxDMNTestFramework(PivxTestFramework):

def set_base_test_params(self):
# 1 miner, 1 controller, 6 remote mns
# 1 miner, 1 controller, 6 remote mns 2 of which shielded
self.num_nodes = 8
self.minerPos = 0
self.controllerPos = 1
self.setup_clean_chain = True

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

Expand Down Expand Up @@ -1453,10 +1472,14 @@ def setup_test(self):
# Create 6 DMNs and init the remote nodes
self.log.info("Initializing masternodes...")
for _ in range(2):
self.add_new_dmn("internal")
self.add_new_dmn("internal", False)
self.add_new_dmn("external")
self.add_new_dmn("fund")
assert_equal(len(self.mns), 6)
# Sanity check that we have 2 shielded masternodes
assert_equal(len(self.nodes[self.controllerPos].listlockunspent()["shielded"]), 2)
assert_equal(self.mns[0].transparent, False)
assert_equal(self.mns[3].transparent, False)
for mn in self.mns:
self.nodes[mn.idx].initmasternode(mn.operator_sk)
time.sleep(1)
Expand Down
9 changes: 5 additions & 4 deletions test/functional/test_framework/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,8 +587,9 @@ def get_coinstake_address(node, expected_utxos=None):
return addrs[0]

# Deterministic masternodes
def is_coin_locked_by(node, outpoint):
return outpoint.to_json() in node.listlockunspent()["transparent"]
def is_coin_locked_by(node, outpoint, transparent=True):
returnStr = "transparent" if transparent else "shielded"
return outpoint.to_json() in node.listlockunspent()[returnStr]

def get_collateral_vout(json_tx):
funding_txidn = -1
Expand All @@ -601,7 +602,7 @@ def get_collateral_vout(json_tx):

# owner and voting keys are created from controller node.
# operator keys are created, if operator_keys is None.
def create_new_dmn(idx, controller, payout_addr, operator_keys):
def create_new_dmn(idx, controller, payout_addr, operator_keys, transparent):
port = p2p_port(idx) if idx <= MAX_NODES else p2p_port(MAX_NODES) + (idx - MAX_NODES)
ipport = "127.0.0.1:" + str(port)
owner_addr = controller.getnewaddress("mnowner-%d" % idx)
Expand All @@ -613,7 +614,7 @@ def create_new_dmn(idx, controller, payout_addr, operator_keys):
else:
operator_pk = operator_keys[0]
operator_sk = operator_keys[1]
return messages.Masternode(idx, owner_addr, operator_pk, voting_addr, ipport, payout_addr, operator_sk)
return messages.Masternode(idx, owner_addr, operator_pk, voting_addr, ipport, payout_addr, operator_sk, transparent)

def spend_mn_collateral(spender, dmn):
inputs = [dmn.collateral.to_json()]
Expand Down
Loading

0 comments on commit c6ad3e0

Please sign in to comment.