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

CFundDB extra log and ensure read before modify #622

Merged
merged 22 commits into from
Nov 19, 2019

Conversation

aguycalled
Copy link
Member

@aguycalled aguycalled commented Nov 8, 2019

This PR adds extra log for all the modifications of the CFundDB and ensures entries are read in the memory cache before being modified

@mxaddict
Copy link
Contributor

mxaddict commented Nov 8, 2019

utACK

@aguycalled aguycalled changed the title CFundDB extra log CFundDB extra log and ensure read before modify Nov 8, 2019
@chasingkirkjufell
Copy link
Contributor

chasingkirkjufell commented Nov 8, 2019

if a node submits a payment request and that transaction is orphaned. the payment request will still be in the qt wallet in that node.
Edit: Same with proposals, if the transaction is in a orphan block, it'll still be in the "proposalvotelist".
image
image

image

@aguycalled
Copy link
Member Author

@chasingkirkjufell fixed in 96e9d51

@aguycalled
Copy link
Member Author

aguycalled commented Nov 11, 2019

added test unit 5f43066

@aguycalled
Copy link
Member Author

test unit cfunddb_tests.cpp

fails on master:

$ git name-rev --name-only HEAD
remotes/upstream/master
$ ./src/test/test_navcoin 
Running 100 test cases...
test/cfunddb_tests.cpp:41: error: in "coins_tests/cfunddb_state": check !view.GetProposal(emptyProposal.hash, proposal) has failed
test/cfunddb_tests.cpp:45: error: in "coins_tests/cfunddb_state": check mapProposals.size()==0 has failed
test/cfunddb_tests.cpp:55: error: in "coins_tests/cfunddb_state": check !view.GetPaymentRequest(emptyPaymentRequest.hash, prequest) has failed
test/cfunddb_tests.cpp:59: error: in "coins_tests/cfunddb_state": check mapProposals.size()==0 has failed
test/cfunddb_tests.cpp:61: error: in "coins_tests/cfunddb_state": check mapPaymentRequests.size()==0 has failed
test/cfunddb_tests.cpp:120: error: in "coins_tests/cfunddb_state": check mapProposals.size()==0 has failed
test/cfunddb_tests.cpp:122: error: in "coins_tests/cfunddb_state": check mapPaymentRequests.size()==0 has failed
test/cfunddb_tests.cpp:137: error: in "coins_tests/cfunddb_state": check mapProposals.size()==1 has failed
test/cfunddb_tests.cpp:139: error: in "coins_tests/cfunddb_state": check mapPaymentRequests.size()==1 has failed
test/cfunddb_tests.cpp:162: error: in "coins_tests/cfunddb_state": check mapProposals.size()==2 has failed
test/cfunddb_tests.cpp:164: error: in "coins_tests/cfunddb_state": check mapPaymentRequests.size()==1 has failed
test/cfunddb_tests.cpp:202: error: in "coins_tests/cfunddb_state": check !view.GetProposal(hash2, proposal2) has failed
test/cfunddb_tests.cpp:204: error: in "coins_tests/cfunddb_state": check !view.GetPaymentRequest(hash3, prequest) has failed
test/cfunddb_tests.cpp:207: error: in "coins_tests/cfunddb_state": check mapProposals.size()==1 has failed
test/cfunddb_tests.cpp:209: error: in "coins_tests/cfunddb_state": check mapPaymentRequests.size()==0 has failed
test/cfunddb_tests.cpp:229: error: in "coins_tests/cfunddb_state": check !base->GetProposal(hash2, proposal2) has failed
test/cfunddb_tests.cpp:231: error: in "coins_tests/cfunddb_state": check !base->GetPaymentRequest(hash3, prequest) has failed
test/cfunddb_tests.cpp:250: error: in "coins_tests/cfunddb_state": check !base->GetProposal(hash2, proposal2) has failed
test/cfunddb_tests.cpp:252: error: in "coins_tests/cfunddb_state": check !base->GetPaymentRequest(hash3, prequest) has failed
test/cfunddb_tests.cpp:255: error: in "coins_tests/cfunddb_state": check mapProposals.size()==2 has failed
test/cfunddb_tests.cpp:257: error: in "coins_tests/cfunddb_state": check mapPaymentRequests.size()==1 has failed
test/cfunddb_tests.cpp:278: error: in "coins_tests/cfunddb_state": check mapProposals.size()==2 has failed
test/cfunddb_tests.cpp:280: error: in "coins_tests/cfunddb_state": check mapPaymentRequests.size()==1 has failed
test/cfunddb_tests.cpp:384: error: in "coins_tests/cfunddb_state": check mapProposals.size()==2 has failed
test/cfunddb_tests.cpp:387: error: in "coins_tests/cfunddb_state": check mapPaymentRequests.size()==1 has failed

*** 25 failures are detected in the test module "NavCoin Test Suite"
$ 

passes on this branch:

$ git name-rev --name-only HEAD
cfund-log-modifiers
$ ./src/test/test_navcoin 
Running 100 test cases...

*** No errors detected
$ 

@mxaddict
Copy link
Contributor

utACK

new native testcases make sense.

Ran the test_navcoin binary from this branch against master and master failed.

Ran the test_navcoin binary from this branch against this pr and it passed.

I'll approve as soon as I'm done with the manual test scenario that I used to replicate the 000000000 hash request.

@mxaddict
Copy link
Contributor

Tested with this manual scenario:

Node 1 has block A with payment request A
Node 2 has block B with payment request A

Node 2 has more blocks so Node 1 tries to reorg against the blocks in Node 2
Node 1 does syncs blocks from Node 2
Node 1 no longer has block A which in it's copy of payment request A is referred to
Node 1 checks block A, it can't find it, so it thinks that Node 2 is lying
Node 1 bans Node 2

On master I was able to get node 1 to fork, still testing on this PR.

@aguycalled
Copy link
Member Author

Tested with this manual scenario:

Node 1 has block A with payment request A
Node 2 has block B with payment request A

Node 2 has more blocks so Node 1 tries to reorg against the blocks in Node 2
Node 1 does syncs blocks from Node 2
Node 1 no longer has block A which in it's copy of payment request A is referred to
Node 1 checks block A, it can't find it, so it thinks that Node 2 is lying
Node 1 bans Node 2

On master I was able to get node 1 to fork, still testing on this PR.

To completely reproduce the issue we've seen we need to bring it further to the payment request's payout.

@mxaddict
Copy link
Contributor

Tested with this manual scenario:

Node 1 has block A with payment request A
Node 2 has block B with payment request A

Node 2 has more blocks so Node 1 tries to reorg against the blocks in Node 2
Node 1 does syncs blocks from Node 2
Node 1 no longer has block A which in it's copy of payment request A is referred to
Node 1 checks block A, it can't find it, so it thinks that Node 2 is lying
Node 1 bans Node 2

On master I was able to get node 1 to fork, still testing on this PR.

To completely reproduce the issue we've seen we need to bring it further to the payment request's payout.

I think that might be covered in my test, cause I let Node 2 stake up to 300+ more blocks than node 1. Which should be more than enough for the payment request to be paid already

@aguycalled can you confirm this is the case with my test scenario?

@aguycalled
Copy link
Member Author

aguycalled commented Nov 12, 2019

@mxaddict that is one possible wrong case.
the one we saw on mainnet had the block of payment request A in Node 1 set as 0x000 after the reorg
we need to be sure the blockhash is set correctly to the right block after reorgs

@aguycalled
Copy link
Member Author

test from c27635f passes on this branch and 4.7.1. But does not pass in 4.7.0 c8d9d72.

My theory:

Bootstrap downloaded from https://www.navexplorer.com/bootstrap.tar (node was running 4.7.0)
Client runs with -txindex=1 -spendindex=1 -addressindex=1
Best block on launch: 3259a848b73c2fd215382b74ac923536889c89bf45c87059e60ae689401ee18b - height 3582947
Let it sync with mainnet, it will reject the block 53c79cd433465b163dc760d3239f95edf0504d8b2a9ecc3cf76def8a77f7eddb - height 3628851
That block contains a payout for payment request bc6f31a269a9733be2dc8e2d1cfd1102bd569c736346139933755e7bab5f8e9c
The votes of this payment request have not been counted in previous blocks, so it never reaches the accepted state and that’s why the payout is rejected. We've seen this issue in 4.7.1 nodes too.
Querying the payment request status on launch with the navexplorer bootstrap we can see:

$ ./src/navcoin-cli -datadir=/Users/alex/bootstrap getpaymentrequest bc6f31a269a9733be2dc8e2d1cfd1102bd569c736346139933755e7bab5f8e9c
{
  "version": 2,
  "hash": "bc6f31a269a9733be2dc8e2d1cfd1102bd569c736346139933755e7bab5f8e9c",
  "blockHash": "9721c16edcacb11e83c69b873d9d67949b938a934ffad2f270fd0a80f0cade13",
  "description": "NavCoin Portuguese-CriptoBlock 2019PT-http://bit.ly/2o6pKRD",
  "requestedAmount": "2250.00",
  "votesYes": 0,
  "votesNo": 0,
  "votingCycle": 2,
  "status": "pending",
  "state": 0,
  "stateChangedOnBlock": "0000000000000000000000000000000000000000000000000000000000000000"
}

9721c16edcacb11e83c69b873d9d67949b938a934ffad2f270fd0a80f0cade13 is not part of the main chain, that’s the reason why the votes are not counted.
The payment request was created on 2019-10-08, so if it was affected by a reorg, we can be sure it happened with a 4.7.0 wallet and the entry is corrupted since then on the local cfunddb.
Payouts are the only consensus-critical action of payment requests. It makes sense wallets with that corrupted entry did not fork until the payout.
When 4.7.1 was released, nodes which were not aware of being suffering any issue just updated and inherited the corrupted entry from 4.7.0.
4.7.1 nodes would have needed to reindex to be completely sure their state was valid.

@mxaddict
Copy link
Contributor

@aguycalled I agree with your idea to add a new hash to the merkle root, should we add it into this PR?

@mxaddict
Copy link
Contributor

@aguycalled I agree with your idea to add a new hash to the merkle root, should we add it into this PR?

Nevermind this comment :D

@aguycalled
Copy link
Member Author

Related to previous comments: #625

@proletesseract
Copy link
Member

This PR compiles and runs on OSX 10.14.5.

I can verify that 4.7.0 fails the test cfund-fork-reorg.py on line 92 when it tries to reconnect the nodes with raise AssertionError("Block sync failed").

This branch (which is forked from master after 4.7.1) passes the test.

Now continuing with the code & test review.

@proletesseract
Copy link
Member

proletesseract commented Nov 16, 2019

Test makes sense, thanks for leaving comments in the file.

Regarding these changes, as i understand it we have a few areas with the potential to cause a fork if they were unable to be reorganised correctly;

  • submitted proposal
  • proposal votes
  • submitted payment request
  • payment request votes
  • payment request payout

As far as I can tell the test only reorgs a payment request, after the proposal is accepted on the same chain by each node. Is it also worth having the same test but reorg the proposal itself and ensure payment requests can still be submitted?

We have; cfund-paymentrequest-state-reorg.py i can see also but that also appears to be focused on payment request reorganisations and actually only checks the bestblockhashes after reorganisation occurs and does not confirm the cfund state.

These and probably more permutations are the types of things i want to map out in miniature and make sure are covered in a robust and micro focused python test framework. After we get the new testnet up and running.

I think this test is probably enough for now, and we can leave the full audit of the cfund tests for that stage of the network stability review.

mapProposal.insert(make_pair(it->first, it->second));

for (auto it = mapProposal.begin(); it != mapProposal.end();)
it->second.IsNull() ? mapProposal.erase(it++) : ++it;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to erase entries with the null proposal pair since the loop above only inserts when it is not null?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, because it can have null entries from cacheProposals, which are entries marked to be removed in the base view when a flush is executed later

mapPaymentRequests.insert(make_pair(it->first, it->second));

for (auto it = mapPaymentRequests.begin(); it != mapPaymentRequests.end();)
it->second.IsNull() ? mapPaymentRequests.erase(it++) : ++it;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comment as above for here, if my above comment is valid.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same answer as before

@@ -221,14 +229,22 @@ CCoinsModifier CCoinsViewCache::ModifyCoins(const uint256 &txid) {
CProposalModifier CCoinsViewCache::ModifyProposal(const uint256 &pid) {
assert(!hasModifier);
std::pair<CProposalMap::iterator, bool> ret = cacheProposals.insert(std::make_pair(pid, CProposal()));
ret.first->second.fDirty = true;
if (ret.second) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain what this function is doing sorry? I can see it's called before adding votes to proposals in the cache and.. again when we're updating the state at the end of the voting cycles.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

::ModifyProposal returns a pointer to an entry in the view cache we want to modify.

    std::pair<CProposalMap::iterator, bool> ret = cacheProposals.insert(std::make_pair(pid, CProposal()));

Here we try to insert in the view cache an empty proposal with the hash pid.

     if (ret.second) {

This means the insert was successful (no previous entry with that hash in the cache).

     if (!base->GetProposal(pid, ret.first->second)) {
          ret.first->second.SetNull();
    }

We try to get the entry from the base view, and if it's not possible we set it to null (which is redundant but safer).

     return CProposalModifier(*this, ret.first);

Finally we construct the CProposalModifier object and return it.

CProposalModifier proposal = view.ModifyProposal(it->first);
proposal->nVotesYes = it->second.first;
proposal->nVotesNo = it->second.second;
if (*proposal != oldproposal)
{
proposal->fDirty = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the fDirty boolean used for?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only entries flagged as dirty are written to the base view when flushed.

@@ -815,6 +847,13 @@ void CFund::CFundStep(const CValidationState& state, CBlockIndex *pindexNew, con
prequest->nVotesNo = 0;
}
}

if (*prequest != oldprequest)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does the == operator overload match != as well for these if statements?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an operator!= declaration which simply returns the inverse of ==, is that what you are referring to?

@@ -3425,13 +3475,15 @@ bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockIndex* pin
{
uint256 prid = uint256S(metadata[nPaymentRequestsCount].get_str());

if(!view.HavePaymentRequest(prid))
CPaymentRequest prequest;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the benefit of doing the following operations on the cache prequest rather than the CPaymentRequestModifier? We now guarantee that we've done all the null checks on the payment request before running these block checks?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there somewhere in main where we should be doing a similar thing with the cache for proposals like we're doing for payment requests here?

Copy link
Member Author

@aguycalled aguycalled Nov 16, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no real benefit more than being more strictly correct with the purpose of each object.
CProposal and CPaymentRequest changes are not reflected in the cache view, while Modifiers can be changed and the changes are reflected in the cache view.
The declaration of mprequest could be moved forward, so it only happens if all the previous checks are satisfied. 36269a2
CProposalModifier (3 times) and CPaymentRequestModifier (4 times) are used very rarely in the code. A global search would show where one can put some more attention.

@proletesseract
Copy link
Member

proletesseract commented Nov 16, 2019

Diff reviewed in full while exploring some horizontal and vertical context around the changes. Comments above.

@navbuilder
Copy link

A new build of 36269a2 has completed succesfully!
Binaries available at https://build.nav.community/binaries/cfund-log-modifiers

proletesseract
proletesseract previously approved these changes Nov 16, 2019
Copy link
Member

@proletesseract proletesseract left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changing approval status until discussed test is added

@proletesseract proletesseract dismissed their stale review November 16, 2019 22:32

re-reviewing with test

@proletesseract
Copy link
Member

proletesseract commented Nov 16, 2019

I've added the test for checking a re-organised proposal can still make it through payment request submission, voting and payment. The test passes on this branch and fails on v4.7.0.

the 4.7.0 test failure is on line 96
the 4.7.0 error is JSONRPC error: Block not found

Which confirms this action would end with the nodes disagreeing on the blockHash of the proposal.

If someone can review the test that would be great; https://github.com/navcoin/navcoin-core/blob/09b636b3954b1d7374156339da10f6bb1ad91c61/qa/rpc-tests/cfund-fork-reorg-proposal.py

@navbuilder
Copy link

A new build of b0af91b has completed succesfully!
Binaries available at https://build.nav.community/binaries/cfund-log-modifiers

@mxaddict
Copy link
Contributor

Build on Ubuntu 19.10 and ran the 2 new tests for cfund-fork-reorg*, tests passed

@mxaddict
Copy link
Contributor

Read the new test that @proletesseract added, makes sense.

@mxaddict
Copy link
Contributor

@aguycalled @proletesseract I'll let you do the honors incase you want to add more changes to the tests.

@aguycalled aguycalled merged commit 37fa72e into navcoin:master Nov 19, 2019
aguycalled pushed a commit to aguycalled/navcoin-core that referenced this pull request Dec 5, 2019
* add extra log

* add __func__

* ensure read before modify

* fix log

* optimize log

* do not access modifier

* only set dirty when necessary

* check for nullified

* add extra log

* do not insert nullified entries

* HaveProposalInCache/HavePaymentRequestInCache

* add cfunddb_tests.cpp

* add 250 rounds and random remove

* Added new test for cfund reorg scenario

* Updates to the test as per aguycalled's suggestions

* update  qa/rpc-tests/cfund-fork-reorg.py

* Added new test to the suite

* move mprequest

* adding (failing) test for proposal reorg

* removed 5th cycle

* fixed preq voting, removed logs added final payout check
@proletesseract proletesseract mentioned this pull request Dec 13, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants