Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Validation] [Masternode] [Test] SDMN (Shield Deterministic Masternodes) #2876

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Prev Previous commit
Next Next commit
DIP3 functional tests for SDMNs
  • Loading branch information
panleone committed Sep 5, 2023
commit b63eaea910daa14d2af3f104bb0219c38eb6ceca
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
77 changes: 52 additions & 25 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,24 @@ 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):
# send to the owner the exact collateral tx amount
funding_txid = miner.sendtoaddress(collateral_addr, Decimal('100'))
def protx_register(self, miner, controller, dmn, collateral_addr, transparent, outpoint):
# send to the owner the exact collateral tx amount, unless we already have an outpoint that we want to use
if outpoint is None:
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,
if outpoint is None:
json_tx = controller.getrawtransaction(funding_txid, True)
assert_greater_than(json_tx["confirmations"], 0)
# 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)
else:
dmn.collateral = outpoint
dmn.proTx = controller.protx_register("%064x" % dmn.collateral.hash, dmn.collateral.n, transparent, dmn.ipport, dmn.owner,
dmn.operator_pk, dmn.voting, dmn.payee)

"""
Expand All @@ -1249,7 +1254,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 +1275,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 +1285,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, outpoint)
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 +1314,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 +1344,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 +1405,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 +1476,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
10 changes: 9 additions & 1 deletion test/functional/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@
'tiertwo_masternode_activation.py', # ~ 352 sec
'tiertwo_masternode_ping.py', # ~ 293 sec
'tiertwo_governance_invalid_budget.py', # ~ 266 sec
'tiertwo_shield_deterministicmns.py', # ~ 160 sec
'tiertwo_reorg_mempool.py', # ~ 97 sec
]

Expand Down Expand Up @@ -246,7 +247,14 @@
'wallet_importmulti.py',
'wallet_import_rescan.py',
'wallet_multiwallet.py',
'sapling_wallet_encryption.py'
'sapling_wallet_encryption.py',
'tiertwo_shield_deterministicmns.py',
'tiertwo_dkg_errors.py',
'tiertwo_chainlocks.py',
'tiertwo_dkg_errors.py',
'tiertwo_dkg_pose.py',
'tiertwo_signing_session.py',
'p2p_quorum_connect.py'
]

# Place the lists with the longest tests (on average) first
Expand Down
Loading