Skip to content

Commit

Permalink
Add support for batch generation of voting TXs (#1786)
Browse files Browse the repository at this point in the history
* Add RPC call to create multiple MNs votes TXs

* Allow multiple proposal votes with different MNs and vote decisions

* Rename votegovmulti to votegovbatch

---------

Co-authored-by: Shoham Chakraborty <[email protected]>
  • Loading branch information
Bushstar and shohamc1 authored Mar 2, 2023
1 parent d5b3aae commit a97fc6c
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 12 deletions.
159 changes: 159 additions & 0 deletions src/masternodes/rpc_proposals.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,164 @@ UniValue votegov(const JSONRPCRequest &request) {
return signsend(rawTx, pwallet, optAuthTx)->GetHash().GetHex();
}


UniValue votegovbatch(const JSONRPCRequest &request) {
auto pwallet = GetWallet(request);

RPCHelpMan{
"votegovbatch",
"\nVote for community proposal with multiple masternodes" + HelpRequiringPassphrase(pwallet) + "\n",
{
{"votes", RPCArg::Type::ARR, RPCArg::Optional::NO, "A json array of proposal ID, masternode IDs, operator or owner addresses and vote decision (yes/no/neutral).",
{
{"proposalId", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "The proposal txid"},
{"masternodeId", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "The masternode ID, operator or owner address"},
{"decision", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "The vote decision (yes/no/neutral)"},
}},
},
RPCResult{"\"hash\" (string) The hex-encoded hash of broadcasted transaction\n"},
RPCExamples{HelpExampleCli("votegovbatch", "{{proposalId, masternodeId, yes}...}") +
HelpExampleRpc("votegovbatch", "{{proposalId, masternodeId, yes}...}")},
}
.Check(request);

if (pwallet->chain().isInitialBlockDownload()) {
throw JSONRPCError(RPC_CLIENT_IN_INITIAL_DOWNLOAD, "Cannot vote while still in Initial Block Download");
}
pwallet->BlockUntilSyncedToCurrentChain();

RPCTypeCheck(request.params, {UniValue::VARR}, false);

const auto &keys = request.params[0].get_array();
auto neutralVotesAllowed = gArgs.GetBoolArg("-rpc-governance-accept-neutral", DEFAULT_RPC_GOV_NEUTRAL);

int targetHeight;

struct MasternodeMultiVote {
uint256 propId;
uint256 mnId;
CTxDestination dest;
CProposalVoteType type;
};

std::vector<MasternodeMultiVote> mnMultiVotes;
{
CCustomCSView view(*pcustomcsview);

for (size_t i{}; i < keys.size(); ++i) {

const auto &votes{keys[i].get_array()};
if (votes.size() != 3) {
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Incorrect number of items, three expected, proposal ID, masternode ID and vote expected. %d entries provided.", votes.size()));
}

const auto propId = ParseHashV(votes[0].get_str(), "proposalId");
const auto prop = view.GetProposal(propId);
if (!prop) {
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Proposal <%s> does not exist", propId.GetHex()));
}

if (prop->status != CProposalStatusType::Voting) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
strprintf("Proposal <%s> is not in voting period", propId.GetHex()));
}

uint256 mnId;
const auto &id = votes[1].get_str();

if (id.length() == 64) {
mnId = ParseHashV(id, "masternodeId");
} else {
const CTxDestination dest = DecodeDestination(id);
if (!IsValidDestination(dest)) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
strprintf("The masternode id or address is not valid: %s", id));
}
CKeyID ckeyId;
if (dest.index() == PKHashType) {
ckeyId = CKeyID(std::get<PKHash>(dest));
} else if (dest.index() == WitV0KeyHashType) {
ckeyId = CKeyID(std::get<WitnessV0KeyHash>(dest));
} else {
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("%s does not refer to a P2PKH or P2WPKH address", id));
}
if (auto masterNodeIdByOwner = view.GetMasternodeIdByOwner(ckeyId)) {
mnId = masterNodeIdByOwner.value();
} else if (auto masterNodeIdByOperator = view.GetMasternodeIdByOperator(ckeyId)) {
mnId = masterNodeIdByOperator.value();
}
}

const auto node = view.GetMasternode(mnId);
if (!node) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
strprintf("The masternode does not exist or the address doesn't own a masternode: %s", id));
}

auto vote = CProposalVoteType::VoteNeutral;
auto voteStr = ToLower(votes[2].get_str());

if (voteStr == "no") {
vote = CProposalVoteType::VoteNo;
} else if (voteStr == "yes") {
vote = CProposalVoteType::VoteYes;
} else if (neutralVotesAllowed && voteStr != "neutral") {
throw JSONRPCError(RPC_INVALID_PARAMETER, "decision supports yes/no/neutral");
} else if (!neutralVotesAllowed) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Decision supports yes or no. Neutral is currently disabled because of issue https://github.com/DeFiCh/ain/issues/1704");
}

mnMultiVotes.push_back({
propId,
mnId,
node->ownerType == 1 ? CTxDestination(PKHash(node->ownerAuthAddress)) : CTxDestination(WitnessV0KeyHash(node->ownerAuthAddress)),
vote
});
}

targetHeight = view.GetLastHeight() + 1;
}

UniValue ret(UniValue::VARR);

for (const auto& [propId, mnId, ownerDest, vote] : mnMultiVotes) {

CProposalVoteMessage msg;
msg.propId = propId;
msg.masternodeId = mnId;
msg.vote = vote;

// encode
CDataStream metadata(DfTxMarker, SER_NETWORK, PROTOCOL_VERSION);
metadata << static_cast<unsigned char>(CustomTxType::Vote) << msg;

CScript scriptMeta;
scriptMeta << OP_RETURN << ToByteVector(metadata);

const auto txVersion = GetTransactionVersion(targetHeight);
CMutableTransaction rawTx(txVersion);

CTransactionRef optAuthTx;
std::set<CScript> auths = {GetScriptForDestination(ownerDest)};
rawTx.vin = GetAuthInputsSmart(pwallet, rawTx.nVersion, auths, false /*needFoundersAuth*/, optAuthTx, {});
rawTx.vout.emplace_back(0, scriptMeta);

CCoinControl coinControl;
if (IsValidDestination(ownerDest)) {
coinControl.destChange = ownerDest;
}

fund(rawTx, pwallet, optAuthTx, &coinControl);

// check execution
execTestTx(CTransaction(rawTx), targetHeight, optAuthTx);

ret.push_back(signsend(rawTx, pwallet, optAuthTx)->GetHash().GetHex());
}

return ret;
}

UniValue listgovproposalvotes(const JSONRPCRequest &request) {
auto pwallet = GetWallet(request);
RPCHelpMan{
Expand Down Expand Up @@ -1192,6 +1350,7 @@ static const CRPCCommand commands[] = {
{"proposals", "creategovcfp", &creategovcfp, {"data", "inputs"} },
{"proposals", "creategovvoc", &creategovvoc, {"data", "inputs"} },
{"proposals", "votegov", &votegov, {"proposalId", "masternodeId", "decision", "inputs"}},
{"proposals", "votegovbatch", &votegovbatch, {"proposalId", "masternodeIds", "decision"}},
{"proposals", "listgovproposalvotes", &listgovproposalvotes, {"proposalId", "masternode", "cycle", "pagination"} },
{"proposals", "getgovproposal", &getgovproposal, {"proposalId"} },
{"proposals", "listgovproposals", &listgovproposals, {"type", "status", "cycle", "pagination"} },
Expand Down
23 changes: 11 additions & 12 deletions test/functional/feature_on_chain_government_govvar_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,24 +71,23 @@ def test_cfp_update_automatic_payout(self):
self.nodes[0].setgov({"ATTRIBUTES":{'v0/params/feature/gov-payout':'true'}})
self.nodes[0].generate(1)

# Vote during second cycle
self.nodes[0].votegov(propId, self.mn0, "yes")
# Import MN keys into MN0
self.nodes[0].importprivkey(self.nodes[1].dumpprivkey(self.nodes[0].getmasternode(self.mn1)[self.mn1]['ownerAuthAddress']))
self.nodes[0].importprivkey(self.nodes[2].dumpprivkey(self.nodes[0].getmasternode(self.mn2)[self.mn2]['ownerAuthAddress']))
self.nodes[0].importprivkey(self.nodes[3].dumpprivkey(self.nodes[0].getmasternode(self.mn3)[self.mn3]['ownerAuthAddress']))

# Vote during second cycle using multi-vote
self.nodes[0].votegovbatch([[propId, self.mn0, "yes"], [propId, self.mn1, "yes"], [propId, self.mn2, "yes"], [propId, self.mn3, "yes"]])
self.nodes[0].generate(1)
self.sync_blocks(timeout=120)
self.nodes[1].votegov(propId, self.mn1, "yes")
self.nodes[1].generate(1)
self.sync_blocks(timeout=120)
self.nodes[2].votegov(propId, self.mn2, "yes")
self.nodes[2].generate(1)
self.sync_blocks(timeout=120)
self.nodes[3].votegov(propId, self.mn3, "yes")
self.nodes[3].generate(1)
self.sync_blocks(timeout=120)

# End proposal
self.nodes[0].generate(VOTING_PERIOD)
self.sync_blocks(timeout=120)

result = self.nodes[0].getgovproposal(propId)
assert_equal(result['currentCycle'], 2)
assert_equal(result['votesYes'], 4)

# Automatic payout only for last cycle
account = self.nodes[0].getaccount(address)
assert_equal(account, ['100.00000000@DFI'])
Expand Down

0 comments on commit a97fc6c

Please sign in to comment.