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

Return a more informative error message when trying to spend coinbase; select non-coinbase inputs when sending to a transparent output if needed #1431

Merged
merged 1 commit into from Oct 17, 2016

Conversation

bitcartel
Copy link
Contributor

@bitcartel bitcartel commented Sep 22, 2016

For #1373 and #1519

Code change:

  • Extra parameter added to AvailableCoins to include or exclude Coinbase coins. Default value of parameter is 'true' as current behaviour is to include Coinbase coins.
  • SelectCoins, used for sending taddr->taddr, will now exclude Coinbase coins.

Unit test:
Tried to write a test to focus on the extra parameter added to AvailableCoins but could not.

Empirical testing on Testnet:
Current behaviour is that upstream RPC commands sendfrom and sendtoaddress try to spend coinbase coins returned by AvailableCoins. So the user will see:

./zcash-cli sendtoaddress mrEGRmGJhmwAa4MQjzGd86ry63vrvovu9b 1000.0
error: {"code":-6,"message":"Insufficient funds"}

./zcash-cli sendtoaddress mrEGRmGJhmwAa4MQjzGd86ry63vrvovu9b 0.00003000
error: {"code":-4,"message":"Error: The transaction was rejected! This might happen if some of the coins in your wallet were already spent, such as if you used a copy of wallet.dat and coins were spent in the copy but not marked as spent here."}

./zcash-cli sendfrom "" mrEGRmGJhmwAa4MQjzGd86ry63vrvovu9b 0.00003000
error: {"code":-4,"message":"Error: The transaction was rejected! This might happen if some of the coins in your wallet were already spent, such as if you used a copy of wallet.dat and coins were spent in the copy but not marked as spent here."}

After fix is applied:

./zcash-cli sendtoaddress mrEGRmGJhmwAa4MQjzGd86ry63vrvovu9b 1000.0
error: {"code":-6,"message":"Insufficient funds"}

./zcash-cli sendtoaddress mrEGRmGJhmwAa4MQjzGd86ry63vrvovu9b 0.00003000
error: {"code":-4,"message":"Coinbase funds can only be sent to a zaddr"}

When non-coinbase UTXOs exist, they will now be selected and used:

./zcash-cli z_sendmany tnPJZHeVxegCg91utaquBRPEDBGjozfz9iLDHt7zvphFbZdspNgkTVLCGjDcadQBKNyUwKs8pNjDXuEZKrE1aNLpFwHgz4t '[{"address":"mx5fTRhLZwbYE7ZqhAPueZgQGSnwTbdvKU", "amount":0.01}]'

./zcash-cli sendtoaddress mrEGRmGJhmwAa4MQjzGd86ry63vrvovu9b 1000.0
error: {"code":-6,"message":"Insufficient funds"}

./zcash-cli sendtoaddress mrEGRmGJhmwAa4MQjzGd86ry63vrvovu9b 0.00003000
9818e543ac2f689d4ce8b52087607d73fecd771d45d316a1d9db092f0485aff2

./zcash-cli sendfrom "" mrEGRmGJhmwAa4MQjzGd86ry63vrvovu9b 0.00003000
899f2894823f51f15fc73b5e0871ac943edbe0ff88e1635f86906087b72caf30

@bitcartel bitcartel added A-wallet Area: Wallet A-rpc-interface Area: RPC interface labels Sep 22, 2016
@bitcartel bitcartel added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Sep 22, 2016
vector<COutput> vAllCoins;
AvailableCoins(vAllCoins, true, coinControl, false, true);
fOnlyCoinbaseCoinsRet = vAllCoins.size()>0;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

The code is correct but confusing. I suggest adding a comment:

// fOnlyCoinbaseCoinsRet should be true when there are coinbase coins
// available, but no non-coinbase coins.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Contributor

@defuse defuse Oct 8, 2016

Choose a reason for hiding this comment

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

If I read the code correctly it has this behavior:

  • If all my coins are coinbases, I'll get the error message "Coinbase funds can only be sent to a zaddr".
  • Otherwise, (when I have some coinbases and some non-coinbases, but the non-coinbases are insufficient) I'll get the error "Insufficient funds."

This means that in the case where my non-coinbases aren't enough, but my non-coinbases plus my coinbases would be, then I still get the error "Insufficient funds." Should that be a separate case, i.e. "You have sufficient funds, but coinbases must be spent to a zaddr first"?

There might be TOCTTOU bug: the wallet lock is acquired inside AvailableCoins() so between the first call and the second, and the wallet state might change in between. fOnlyCoinbaseCoinsRet would be set incorrectly if new non-coinbase coins were acquired. I'm not sure if that matters.

{
strFailReason = _("Insufficient funds");
if (fOnlyCoinbaseCoins) {
strFailReason =_("Coinbase funds can only be sent to a zaddr");
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: space after =

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@daira
Copy link
Contributor

daira commented Sep 27, 2016

Code is okay, but it definitely needs a unit test of AvailableCoins covering both cases for fOnlyCoinbaseCoins.

@bitcartel
Copy link
Contributor Author

@daira I spent fair bit of time trying to add a unit test for AvailableCoins. Created a chain of transactions, with valid UTXOs, adding them to a wallet, but could not get AvailableCoins to see those UTXOs as being available for selection. At this moment in time I can only provide results from empirical testing on testnet.

@str4d
Copy link
Contributor

str4d commented Sep 29, 2016

@nathan-at-least please comment re: testing strategy.

@nathan-at-least
Copy link
Contributor

nathan-at-least commented Sep 30, 2016

@bitcartel: the fact that your unit test isn't working could tell us something interesting about the wallet, so keep that code around. (In fact, if the DISABLED gtest functionality is already landed, simply DISABLE that test and merge it, with an appropriate comment to the fact that we're not sure why it fails and that we want it to pass.)

I'd accept any other kind of automated test, such as a case in ./qa/rpc-tester/ if those are more feasible.

I'm not comfortable merging this with only manual testing.

@bitcartel
Copy link
Contributor Author

@daira @nathan @str4d I'm fairly comfortable to have this merged because:

  1. Running tests via qa/pull-tester/rpc-tests.sh passes (does not break existing expected behaviour)
  2. qa tests run in regtest mode, so we must manually edit regtest params in chainparams.cpp to enforce coinbase validity, consensus.fCoinbaseMustBeProtected = true;. Now when we run the wallet test, it confirms that AvailableCoins is not selecting coinbase utxos for sending to a taddr.
../qa/pull-tester/rpc-tests.sh wallet
=== Running testscript wallet.py ===
...
  Initializing test directory /tmp/testDvQQ2O
  Mining blocks...
  JSONRPC error: Coinbase funds can only be sent to a zaddr
  Stopping nodes
  Cleaning up
  Failed
!!! FAIL: wallet.py !!!

Note: To write a specific case we will need to create a new qa harness which use new chain parameters, like regtest, but which enforce coinbase validity. If we simply update regtest params, most of the existing qa tests will fail.

@bitcartel
Copy link
Contributor Author

This needs review and see comment above.

@bitcartel
Copy link
Contributor Author

@nathan See review comment above. Ack?

@bitcartel
Copy link
Contributor Author

@ebfull @str4d needs review.

@str4d
Copy link
Contributor

str4d commented Oct 4, 2016

Have not had time to review. Up to @nathan-at-least whether he is happy to include this tomorrow if it gets review.

@nathan
Copy link

nathan commented Oct 4, 2016

@bitcartel wrong nathan ;)

@nathan-at-least
Copy link
Contributor

IIUC, there is at least one ./qa/rpc-test case that verifies this error case and the resulting message. If so, then I ACK.

@bitcartel
Copy link
Contributor Author

@nathan-at-least Kind of. I have to manually:

  1. change a boolean in chainparams.cpp for regtest params
  2. run the wallet test in /qa/rpc-test.
    We can't leave the change in 1 permanent as that would break all tests.

@nathan-at-least
Copy link
Contributor

I see, so the process is still manual but the manual part has been reduced to tweaking a single flag and rebuilding then running just that one test...

Let's postpone this until rc1 and ensure we have a fully automated test of any type running in our CI.

(As an aside, I'd much rather have a localized unit test than a new test harness with different chain params... How feasible is that?)

@str4d
Copy link
Contributor

str4d commented Oct 4, 2016

ACK, postponing.

@bitcartel
Copy link
Contributor Author

bitcartel commented Oct 4, 2016

Summary:

  • We can't unit test because the wallet is not easy to mock out.
  • The RPC tests operate in regtest mode which does not enforce all testnet rules.
  • If we run RPC tests in testnet mode rather than regtest mode, we can't just make a call to mine 100 blocks, so coinbase maturity becomes an issue as does how long it takes to set-up and run the tests.

@nathan-at-least
Copy link
Contributor

nathan-at-least commented Oct 4, 2016

We can't easily unit test. ;-)

Seriously though: it sounds like unit testing would require a lot of work, but it would be simpler to create a new fourth chain params mode, and a new harness just like ./qa/rpc-tests that uses this new fourth mode. I think even if that's simpler, the cognitive load and the technical debt of having yet a fourth mode is onerous. I'd like us to move towards fewer modes.

However, if this is the only thing we can get working for rc1, then that's what we'll have to do.

Longer term, if we pay the admittedly heavy up front cost of refactoring systems so that we can mock them out, then eventually we can have just a single mode. This would replace a bunch of runtime branches in application code with consistent (though perhaps verbose) test mocking. Mocing is the systematic use branches or compile-time conditionals which are quarantined in our unittest binary and not present on mainnet. That's the world I want to move towards.

It may very well be that rewriting the wallet code from scratch would be less effort than refactoring the existing code to be mockable. So if that's the case, let's seriously think about it for our longer term plans.

@daira
Copy link
Contributor

daira commented Oct 4, 2016

-1 on any fourth chainparams mode.

@bitcartel bitcartel force-pushed the master_1373_taddr_coinbase_error branch from 121218b to 9bbc4d6 Compare October 7, 2016 19:19
Copy link
Contributor

@defuse defuse left a comment

Choose a reason for hiding this comment

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

ACK modulo possible TOCTTOU bug

vector<COutput> vAllCoins;
AvailableCoins(vAllCoins, true, coinControl, false, true);
fOnlyCoinbaseCoinsRet = vAllCoins.size()>0;
}
Copy link
Contributor

@defuse defuse Oct 8, 2016

Choose a reason for hiding this comment

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

If I read the code correctly it has this behavior:

  • If all my coins are coinbases, I'll get the error message "Coinbase funds can only be sent to a zaddr".
  • Otherwise, (when I have some coinbases and some non-coinbases, but the non-coinbases are insufficient) I'll get the error "Insufficient funds."

This means that in the case where my non-coinbases aren't enough, but my non-coinbases plus my coinbases would be, then I still get the error "Insufficient funds." Should that be a separate case, i.e. "You have sufficient funds, but coinbases must be spent to a zaddr first"?

There might be TOCTTOU bug: the wallet lock is acquired inside AvailableCoins() so between the first call and the second, and the wallet state might change in between. fOnlyCoinbaseCoinsRet would be set incorrectly if new non-coinbase coins were acquired. I'm not sure if that matters.

@bitcartel bitcartel force-pushed the master_1373_taddr_coinbase_error branch from 9bbc4d6 to 3f34d62 Compare October 9, 2016 05:17
@bitcartel
Copy link
Contributor Author

bitcartel commented Oct 9, 2016

Rebased, added test, push -f.

Thanks @defuse for comments. (1) I've added a more fine-grained error message, per your suggestion, of "Insufficient funds, coinbase funds can only be spent after they have been sent to a zaddr". (2) Regarding lock behaviour, its inherited from upstream and changed by this commit. The wallet lock is a recursive lock and the caller, CWallet::CreateTransaction, already obtains it, so there shouldn't be any issue.

@daira @nathan-at-least Added qa rpc test to test all three conditions (1) can't send coinbase utxos to taddr (2) insufficient funds unless coinbase utxos are sent to zaddr (and then to taddr) (3) insufficient funds even when including coinbase utxos.

The test was made possible by getting the qa rpc test to pass a runtime parameter -regtestprotectcoinbase to zcashd, where upon detection zcashd updates the regtest params to enforce the coinbase->zcaddr consensus rule.

@bitcartel
Copy link
Contributor Author

Note that a third party needs this merged asap for their integration work with zcashd.
@nathan-at-least Test wallet_protectcoinbase.py added, so this should turn your provisional ACK into an ACK?
@defuse Do my comments and change turn your provisional ACK into an ACK?
@daira Test added, which is what you wanted in previous review comment.

@daira
Copy link
Contributor

daira commented Oct 15, 2016

Although I think there's still no unit test of AvailableCoins covering both cases for fOnlyCoinbaseCoins (please correct me if I missed it), I believe this is now adequately covered by RPC tests. ACK+cov from me (but this is complicated so it should have at least one more ACK+cov).

@bitcartel
Copy link
Contributor Author

@daira Correct, no unit test, but covered by RPC tests.
@str4d Are you able to review? Thanks.

@str4d
Copy link
Contributor

str4d commented Oct 16, 2016

I'll review in the morning.

@daira daira changed the title Return a more informative error message when trying to spend coinbase Return a more informative error message when trying to spend coinbase; select non-coinbase inputs when sending to a transparent output if needed Oct 16, 2016
except JSONRPCException,e:
errorString = e.error['message']
print "error: ", errorString
assert_equal("Insufficient funds, coinbase funds can only be spent after they have been sent to a zaddr" in errorString, True)
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this test need another case in which the send succeeds because there are enough non-coinbase funds, but the coin selection prior to this PR would have chosen a coinbase input?

(Sorry to raise this after already having ACKed; I know we're short on time.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I force pushed this:

+        # Send will succeed because the balance of non-coinbase utxos is 20.0
+        errorString = ""
+        try:
+            self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 19.0)
+        except JSONRPCException,e:
+            errorString = e.error['message']
+        assert_equal("" in errorString, True)
+
+        self.nodes[1].generate(10)
+        self.sync_all()
+
+        # check balance
+        assert_equal(self.nodes[2].getbalance(), Decimal('19'))
+

Copy link
Contributor

@daira daira Oct 16, 2016

Choose a reason for hiding this comment

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

"" in errorString is always True. I think you want assert(False) in the except: clause. ACK if that is fixed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@daira Ok, done.

@bitcartel bitcartel force-pushed the master_1373_taddr_coinbase_error branch 2 times, most recently from a760cc1 to f3240a3 Compare October 17, 2016 00:04
self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 19.0)
except JSONRPCException:
assert(False)
assert_equal("" in errorString, True)
Copy link
Contributor

Choose a reason for hiding this comment

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

You can/should also remove errorString = "" and the assert_equal, since they have no effect.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@@ -336,6 +336,11 @@ CChainParams &Params(CBaseChainParams::Network network) {
void SelectParams(CBaseChainParams::Network network) {
SelectBaseParams(network);
pCurrentParams = &Params(network);

// Some python qa rpc tests need to enforce the coinbase consensue rule
Copy link
Contributor

Choose a reason for hiding this comment

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

consensus

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@bitcartel bitcartel force-pushed the master_1373_taddr_coinbase_error branch from f3240a3 to cdf4f98 Compare October 17, 2016 01:20
Copy link
Contributor

@str4d str4d left a comment

Choose a reason for hiding this comment

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

ut(ACK+cov) mod comment

@@ -0,0 +1,126 @@
#!/usr/bin/env python2
# Copyright (c) 2014 The Bitcoin Core developers
Copy link
Contributor

Choose a reason for hiding this comment

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

# Copyright (c) 2016 The Zcash developers

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@daira
Copy link
Contributor

daira commented Oct 17, 2016

ut(ACK+cov)

…ash#1373).

Extra parameter added to AvailableCoins to include or exclude Coinbase coins.
SelectCoins, used for sending taddr->taddr, will exclude Coinbase coins.

Added qa rpc test and a runtime parameter -regtestprotectcoinbase to enforce
the coinbase->zaddr consensus rule in regtest mode.
@bitcartel bitcartel force-pushed the master_1373_taddr_coinbase_error branch from cdf4f98 to 2b1cda3 Compare October 17, 2016 02:06
@bitcartel bitcartel removed the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Oct 17, 2016
@bitcartel
Copy link
Contributor Author

Thanks @daira and @str4d for review and all other reviewer comments.
@zkbot r+

@zkbot
Copy link
Contributor

zkbot commented Oct 17, 2016

📌 Commit 2b1cda3 has been approved by bitcartel

@zkbot
Copy link
Contributor

zkbot commented Oct 17, 2016

⌛ Testing commit 2b1cda3 with merge ec8dc3a...

zkbot pushed a commit that referenced this pull request Oct 17, 2016
…itcartel

Return a more informative error message when trying to spend coinbase; select non-coinbase inputs when sending to a transparent output if needed

For #1373 and #1519

Code change:
- Extra parameter added to AvailableCoins to include or exclude Coinbase coins.  Default value of parameter is 'true' as current behaviour is to include Coinbase coins.
- SelectCoins, used for sending taddr->taddr, will now exclude Coinbase coins.

Unit test:
Tried to write a test to focus on the extra parameter added to AvailableCoins but could not.

Empirical testing on Testnet:
Current behaviour is that upstream RPC commands sendfrom and sendtoaddress try to spend coinbase coins returned by AvailableCoins.  So the user will see:

```
./zcash-cli sendtoaddress mrEGRmGJhmwAa4MQjzGd86ry63vrvovu9b 1000.0
error: {"code":-6,"message":"Insufficient funds"}

./zcash-cli sendtoaddress mrEGRmGJhmwAa4MQjzGd86ry63vrvovu9b 0.00003000
error: {"code":-4,"message":"Error: The transaction was rejected! This might happen if some of the coins in your wallet were already spent, such as if you used a copy of wallet.dat and coins were spent in the copy but not marked as spent here."}

./zcash-cli sendfrom "" mrEGRmGJhmwAa4MQjzGd86ry63vrvovu9b 0.00003000
error: {"code":-4,"message":"Error: The transaction was rejected! This might happen if some of the coins in your wallet were already spent, such as if you used a copy of wallet.dat and coins were spent in the copy but not marked as spent here."}
```

After fix is applied:

```
./zcash-cli sendtoaddress mrEGRmGJhmwAa4MQjzGd86ry63vrvovu9b 1000.0
error: {"code":-6,"message":"Insufficient funds"}

./zcash-cli sendtoaddress mrEGRmGJhmwAa4MQjzGd86ry63vrvovu9b 0.00003000
error: {"code":-4,"message":"Coinbase funds can only be sent to a zaddr"}
```

When non-coinbase UTXOs exist, they will now be selected and used:

```
./zcash-cli z_sendmany tnPJZHeVxegCg91utaquBRPEDBGjozfz9iLDHt7zvphFbZdspNgkTVLCGjDcadQBKNyUwKs8pNjDXuEZKrE1aNLpFwHgz4t '[{"address":"mx5fTRhLZwbYE7ZqhAPueZgQGSnwTbdvKU", "amount":0.01}]'

./zcash-cli sendtoaddress mrEGRmGJhmwAa4MQjzGd86ry63vrvovu9b 1000.0
error: {"code":-6,"message":"Insufficient funds"}

./zcash-cli sendtoaddress mrEGRmGJhmwAa4MQjzGd86ry63vrvovu9b 0.00003000
9818e543ac2f689d4ce8b52087607d73fecd771d45d316a1d9db092f0485aff2

./zcash-cli sendfrom "" mrEGRmGJhmwAa4MQjzGd86ry63vrvovu9b 0.00003000
899f2894823f51f15fc73b5e0871ac943edbe0ff88e1635f86906087b72caf30
```
@zkbot
Copy link
Contributor

zkbot commented Oct 17, 2016

☀️ Test successful - zcash

@zkbot zkbot merged commit 2b1cda3 into zcash:master Oct 17, 2016
@defuse
Copy link
Contributor

defuse commented Oct 19, 2016

This was already merged, but yeah my provisional ACK changed into an ACK.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-rpc-interface Area: RPC interface A-wallet Area: Wallet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants