Skip to content

Commit 688bf4d

Browse files
Merge pull request #456 from aguycalled/ignore-preq-invalid-hash-patch
Fix for Payment Request reorganizations
2 parents b4a1db5 + cb1a512 commit 688bf4d

17 files changed

Lines changed: 504 additions & 244 deletions

qa/pull-tester/rpc-tests.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@
165165
'cfund-vote.py',
166166
'cfund-proposalvotelist.py',
167167
'cfund-paymentrequestvotelist.py',
168+
'cfund-paymentrequest-state-reorg.py',
168169
'reject-version-bit.py',
169170
'getcoldstakingaddress.py',
170171
'getstakereport.py',

qa/rpc-tests/cfund-paymentrequest-extract-funds.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,21 @@ def run_test(self):
4747

4848
# Create 6 payment requests
4949
paymentRequests = []
50-
for x in range(6):
50+
for x in range(5):
5151
paymentRequests.append(self.nodes[0].createpaymentrequest(proposalid0, 20, "test0")["hash"])
5252

53+
# The last 1 should be rejected as it excedes proposal balance
54+
try:
55+
paymentRequests.append(self.nodes[0].createpaymentrequest(proposalid0, 20, "test0")["hash"])
56+
raise ValueError("Error should be thrown for invalid prequest")
57+
except JSONRPCException as e:
58+
assert("The transaction was rejected" in e.error['message'])
59+
5360
slow_gen(self.nodes[0], 1)
5461

5562
# One of them should have been rejected at creation
5663
valid = 0
5764
invalid = 0
58-
invalid_pos = -1
5965

6066
for paymentReq in paymentRequests:
6167
try:
@@ -64,13 +70,10 @@ def run_test(self):
6470
valid = valid + 1
6571
except JSONRPCException:
6672
invalid = invalid + 1
67-
invalid_pos = valid
6873
continue
6974

7075
assert(valid == 5)
71-
assert(invalid == 1)
72-
73-
paymentRequests.pop(invalid_pos)
76+
assert(invalid == 0)
7477

7578
assert(self.nodes[0].cfundstats()["funds"]["locked"] == locked_accepted)
7679

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2019 The Navcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
6+
from test_framework.test_framework import NavCoinTestFramework
7+
from test_framework.cfund_util import *
8+
9+
import time
10+
import urllib.parse
11+
12+
13+
class CFundPaymentRequestStateReorg(NavCoinTestFramework):
14+
"""Tests consistency of Community Fund Payment Requests state through reorgs."""
15+
16+
def __init__(self):
17+
super().__init__()
18+
self.setup_clean_chain = True
19+
self.num_nodes = 2
20+
21+
def setup_network(self, split=False):
22+
self.nodes = []
23+
self.nodes.append(start_node(0, self.options.tmpdir, ["-debug","-headerspamfiltermaxsize=1000"]))
24+
self.nodes.append(start_node(1, self.options.tmpdir, ["-debug","-headerspamfiltermaxsize=1000"]))
25+
connect_nodes(self.nodes[0], 1)
26+
self.is_network_split = False
27+
28+
def run_test(self):
29+
self.nodes[0].staking(False)
30+
self.nodes[1].staking(False)
31+
activate_cfund(self.nodes[0])
32+
self.sync_all()
33+
34+
self.nodes[0].donatefund(100)
35+
36+
# Generate our addresses
37+
node_0_address = self.nodes[0].getnewaddress()
38+
node_1_address = self.nodes[1].getnewaddress()
39+
40+
# Split funds
41+
self.nodes[0].sendtoaddress(node_1_address, 5000000)
42+
proposal=self.nodes[0].createproposal(node_0_address, 100, 36000, "test")
43+
proposal_id=proposal["hash"]
44+
45+
slow_gen(self.nodes[0], 1)
46+
end_cycle(self.nodes[0])
47+
48+
self.sync_all()
49+
50+
self.nodes[0].proposalvote(proposal_id, "yes")
51+
52+
slow_gen(self.nodes[0], 1)
53+
end_cycle(self.nodes[0])
54+
55+
self.sync_all()
56+
57+
assert(self.nodes[0].getproposal(proposal_id)["state"] == 1)
58+
assert(self.nodes[0].getproposal(proposal_id)["status"] == "accepted")
59+
60+
assert(self.nodes[1].getproposal(proposal_id)["state"] == 1)
61+
assert(self.nodes[1].getproposal(proposal_id)["status"] == "accepted")
62+
63+
raw_preq = self.nodes[0].createpaymentrequest(proposal_id, 100, "preq", True)
64+
self.sync_all()
65+
66+
# Disconnect Nodes 0 and 1
67+
url = urllib.parse.urlparse(self.nodes[1].url)
68+
self.nodes[0].disconnectnode(url.hostname+":"+str(p2p_port(1)))
69+
70+
self.nodes[0].forcetransactions([raw_preq])
71+
self.nodes[1].forcetransactions([raw_preq])
72+
73+
blockcount_0 = self.nodes[0].getblockcount()
74+
blockcount_1 = self.nodes[1].getblockcount()
75+
76+
self.nodes[0].staking(True)
77+
self.nodes[1].staking(True)
78+
79+
# Let's wait for at least 20 blocks from Node 0
80+
while self.nodes[0].getblockcount() - blockcount_0 < 20:
81+
time.sleep(1)
82+
83+
# Node 1 only has 1 output so it will only stake 1 block
84+
while self.nodes[1].getblockcount() == blockcount_1:
85+
time.sleep(1)
86+
87+
print("nodes staked!")
88+
89+
self.nodes[0].staking(False)
90+
self.nodes[1].staking(False)
91+
92+
node_0_best_hash = self.nodes[0].getblockhash(self.nodes[0].getblockcount())
93+
node_1_best_hash = self.nodes[1].getblockhash(self.nodes[1].getblockcount())
94+
95+
# Node 0 and Node 1 have forked.
96+
assert(node_0_best_hash != node_1_best_hash)
97+
98+
connect_nodes(self.nodes[0], 1)
99+
100+
self.sync_all()
101+
102+
node_1_best_hash_ = self.nodes[1].getblockhash(self.nodes[1].getblockcount())
103+
104+
# Node 1 must have reorg'd to Node 0 chain
105+
assert(node_0_best_hash == node_1_best_hash_)
106+
107+
if __name__ == '__main__':
108+
CFundPaymentRequestStateReorg().main()

qa/rpc-tests/cfund-rawtx-paymentrequest-create.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,16 @@ def run_test(self):
111111

112112
# Create multiple payment requests
113113
self.send_raw_paymentrequest(6, self.goodAddress, self.goodProposalHash, "sum_to_too_large_amount0")
114-
self.send_raw_paymentrequest(6, self.goodAddress, self.goodProposalHash, "sum_to_too_large_amount1")
115-
self.send_raw_paymentrequest(6, self.goodAddress, self.goodProposalHash, "sum_to_too_large_amount2")
114+
try:
115+
self.send_raw_paymentrequest(6, self.goodAddress, self.goodProposalHash, "sum_to_too_large_amount1")
116+
raise ValueError("Error should be thrown for invalid rawtx (prequest)")
117+
except JSONRPCException as e:
118+
assert("bad-cfund-payment-request" in e.error['message'])
119+
try:
120+
self.send_raw_paymentrequest(6, self.goodAddress, self.goodProposalHash, "sum_to_too_large_amount2")
121+
raise ValueError("Error should be thrown for invalid rawtx (prequest)")
122+
except JSONRPCException as e:
123+
assert("bad-cfund-payment-request" in e.error['message'])
116124

117125
slow_gen(self.nodes[0], 1)
118126

src/consensus/cfund.cpp

Lines changed: 88 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,12 @@ bool CFund::FindPaymentRequest(string preqstr, CFund::CPaymentRequest &prequest)
7979

8080
bool CFund::VoteProposal(string strProp, bool vote, bool &duplicate)
8181
{
82+
AssertLockHeld(cs_main);
8283

8384
CFund::CProposal proposal;
8485
bool found = CFund::FindProposal(uint256S("0x"+strProp), proposal);
8586

86-
if(!found || proposal.IsNull())
87+
if(!found || proposal.IsNull() || !proposal.CanVote())
8788
return false;
8889

8990
vector<std::pair<std::string, bool>>::iterator it = vAddedProposalVotes.begin();
@@ -133,11 +134,12 @@ bool CFund::RemoveVoteProposal(uint256 proposalHash)
133134

134135
bool CFund::VotePaymentRequest(string strProp, bool vote, bool &duplicate)
135136
{
137+
AssertLockHeld(cs_main);
136138

137139
CFund::CPaymentRequest prequest;
138140
bool found = CFund::FindPaymentRequest(uint256S("0x"+strProp), prequest);
139141

140-
if(!found || prequest.IsNull())
142+
if(!found || prequest.IsNull() || !prequest.CanVote(*pcoinsTip))
141143
return false;
142144

143145
vector<std::pair<std::string, bool>>::iterator it = vAddedPaymentRequestVotes.begin();
@@ -188,7 +190,7 @@ bool CFund::RemoveVotePaymentRequest(uint256 proposalHash)
188190
return RemoveVotePaymentRequest(proposalHash.ToString());
189191
}
190192

191-
bool CFund::IsValidPaymentRequest(CTransaction tx, int nMaxVersion)
193+
bool CFund::IsValidPaymentRequest(CTransaction tx, CCoinsViewCache& coins, int nMaxVersion)
192194
{
193195
if(tx.strDZeel.length() > 1024)
194196
return error("%s: Too long strdzeel for payment request %s", __func__, tx.GetHash().ToString());
@@ -258,9 +260,9 @@ bool CFund::IsValidPaymentRequest(CTransaction tx, int nMaxVersion)
258260
if (!pubkey.RecoverCompact(ss.GetHash(), vchSig) || pubkey.GetID() != keyID)
259261
return error("%s: Invalid signature for payment request %s", __func__, tx.GetHash().ToString());
260262

261-
if(nAmount > proposal.GetAvailable(true))
263+
if(nAmount > proposal.GetAvailable(coins, true))
262264
return error("%s: Invalid requested amount for payment request %s (%d vs %d available)",
263-
__func__, tx.GetHash().ToString(), nAmount, proposal.GetAvailable());
265+
__func__, tx.GetHash().ToString(), nAmount, proposal.GetAvailable(coins, true));
264266

265267
bool ret = (nVersion <= nMaxVersion);
266268

@@ -271,11 +273,23 @@ bool CFund::IsValidPaymentRequest(CTransaction tx, int nMaxVersion)
271273

272274
}
273275

274-
bool CFund::CPaymentRequest::CanVote() const {
276+
bool CFund::CPaymentRequest::CanVote(CCoinsViewCache& coins) const
277+
{
278+
AssertLockHeld(cs_main);
279+
280+
CBlockIndex* pindex;
281+
if(txblockhash == uint256() || !mapBlockIndex.count(txblockhash))
282+
return false;
283+
284+
pindex = mapBlockIndex[txblockhash];
285+
if(!chainActive.Contains(pindex))
286+
return false;
287+
275288
CFund::CProposal proposal;
276289
if(!CFund::FindProposal(proposalhash, proposal))
277290
return false;
278-
return nAmount <= proposal.GetAvailable() && fState != ACCEPTED && fState != REJECTED && fState != EXPIRED && !ExceededMaxVotingCycles();
291+
292+
return nAmount <= proposal.GetAvailable(coins) && fState != ACCEPTED && fState != REJECTED && fState != EXPIRED && !ExceededMaxVotingCycles();
279293
}
280294

281295
bool CFund::CPaymentRequest::IsExpired() const {
@@ -388,6 +402,16 @@ bool CFund::CProposal::IsRejected() const {
388402
}
389403

390404
bool CFund::CProposal::CanVote() const {
405+
AssertLockHeld(cs_main);
406+
407+
CBlockIndex* pindex;
408+
if(txblockhash == uint256() || !mapBlockIndex.count(txblockhash))
409+
return false;
410+
411+
pindex = mapBlockIndex[txblockhash];
412+
if(!chainActive.Contains(pindex))
413+
return false;
414+
391415
return (fState == NIL) && (!ExceededMaxVotingCycles());
392416
}
393417

@@ -418,6 +442,59 @@ bool CFund::CProposal::ExceededMaxVotingCycles() const {
418442
return nVotingCycle > Params().GetConsensus().nCyclesProposalVoting;
419443
}
420444

445+
CAmount CFund::CProposal::GetAvailable(CCoinsViewCache& coins, bool fIncludeRequests) const
446+
{
447+
AssertLockHeld(cs_main);
448+
449+
CAmount initial = nAmount;
450+
for (unsigned int i = 0; i < vPayments.size(); i++)
451+
{
452+
CFund::CPaymentRequest prequest;
453+
if(FindPaymentRequest(vPayments[i], prequest))
454+
{
455+
if (!coins.HaveCoins(prequest.hash))
456+
{
457+
CBlockIndex* pindex;
458+
if(prequest.txblockhash == uint256() || !mapBlockIndex.count(prequest.txblockhash))
459+
continue;
460+
pindex = mapBlockIndex[prequest.txblockhash];
461+
if(!chainActive.Contains(pindex))
462+
continue;
463+
}
464+
if((fIncludeRequests && prequest.fState != REJECTED && prequest.fState != EXPIRED) || (!fIncludeRequests && prequest.fState == ACCEPTED))
465+
initial -= prequest.nAmount;
466+
}
467+
}
468+
return initial;
469+
}
470+
471+
std::string CFund::CProposal::ToString(CCoinsViewCache& coins, uint32_t currentTime) const {
472+
std::string str;
473+
str += strprintf("CProposal(hash=%s, nVersion=%i, nAmount=%f, available=%f, nFee=%f, address=%s, nDeadline=%u, nVotesYes=%u, "
474+
"nVotesNo=%u, nVotingCycle=%u, fState=%s, strDZeel=%s, blockhash=%s)",
475+
hash.ToString(), nVersion, (float)nAmount/COIN, (float)GetAvailable(coins)/COIN, (float)nFee/COIN, Address, nDeadline,
476+
nVotesYes, nVotesNo, nVotingCycle, GetState(currentTime), strDZeel, blockhash.ToString().substr(0,10));
477+
for (unsigned int i = 0; i < vPayments.size(); i++) {
478+
CFund::CPaymentRequest prequest;
479+
if(FindPaymentRequest(vPayments[i], prequest))
480+
str += "\n " + prequest.ToString();
481+
}
482+
return str + "\n";
483+
}
484+
485+
bool CFund::CProposal::HasPendingPaymentRequests(CCoinsViewCache& coins) const {
486+
AssertLockHeld(cs_main);
487+
488+
for (unsigned int i = 0; i < vPayments.size(); i++)
489+
{
490+
CFund::CPaymentRequest prequest;
491+
if(FindPaymentRequest(vPayments[i], prequest))
492+
if(prequest.CanVote(coins))
493+
return true;
494+
}
495+
return false;
496+
}
497+
421498
std::string CFund::CProposal::GetState(uint32_t currentTime) const {
422499
std::string sFlags = "pending";
423500
if(IsAccepted()) {
@@ -444,13 +521,15 @@ std::string CFund::CProposal::GetState(uint32_t currentTime) const {
444521
return sFlags;
445522
}
446523

447-
void CFund::CProposal::ToJson(UniValue& ret) const {
524+
void CFund::CProposal::ToJson(UniValue& ret, CCoinsViewCache& coins) const {
525+
AssertLockHeld(cs_main);
526+
448527
ret.push_back(Pair("version", nVersion));
449528
ret.push_back(Pair("hash", hash.ToString()));
450529
ret.push_back(Pair("blockHash", txblockhash.ToString()));
451530
ret.push_back(Pair("description", strDZeel));
452531
ret.push_back(Pair("requestedAmount", FormatMoney(nAmount)));
453-
ret.push_back(Pair("notPaidYet", FormatMoney(GetAvailable())));
532+
ret.push_back(Pair("notPaidYet", FormatMoney(GetAvailable(coins))));
454533
ret.push_back(Pair("userPaidFee", FormatMoney(nFee)));
455534
ret.push_back(Pair("paymentAddress", Address));
456535
if(nVersion >= 2) {

0 commit comments

Comments
 (0)