diff --git a/test/functional/abc-wallet-standardness.py b/test/functional/abc-wallet-standardness.py new file mode 100755 index 000000000..b9f604440 --- /dev/null +++ b/test/functional/abc-wallet-standardness.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 The Bitcoin developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test the response of wallet to a variety of weird / nonstandard coins +that it might try to spend.""" + +from decimal import Decimal +from test_framework.script import ( + CScript, + OP_1, + OP_5, + OP_CHECKSIG, + OP_CHECKMULTISIG, + OP_DUP, + OP_EQUALVERIFY, + OP_HASH160, + OP_PUSHDATA1, + hash160, +) +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_raises_rpc_error, + assert_equal, + sync_blocks, +) +from test_framework.messages import ( + CTransaction, + CTxOut, + FromHex, + ToHex, +) + +SATOSHI = Decimal('0.00000001') + + +class WalletStandardnessTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 2 + self.extra_args = [['-acceptnonstdtxn=0'], ['-acceptnonstdtxn=1']] + + def run_test(self): + std_node, nonstd_node = self.nodes + + address_nonstd = nonstd_node.getnewaddress() + + # make and mature some coins for the nonstandard node + nonstd_node.generate(120) + sync_blocks(self.nodes) + + def fund_and_test_wallet(scriptPubKey, shouldBeStandard, shouldBeInWallet, amount=10000, spendfee=500, nonstd_error="scriptpubkey (code 64)"): + """ Get the nonstandard node to fund a transaction, test its + standardness by trying to broadcast on the standard node, then + mine it and see if it ended up in the standard node's wallet. + Finally, it attempts to spend the coin. + """ + + self.log.info("Trying script {}".format(scriptPubKey.hex(),)) + + # get nonstandard node to fund the script + tx = CTransaction() + tx.vout.append(CTxOut(max(amount, 10000), scriptPubKey)) + rawtx = nonstd_node.fundrawtransaction( + ToHex(tx), {'lockUnspents': True, 'changePosition': 1})['hex'] + # fundrawtransaction doesn't like to fund dust outputs, so we + # have to manually override the amount. + FromHex(tx, rawtx) + tx.vout[0].nValue = min(amount, 10000) + rawtx = nonstd_node.signrawtransactionwithwallet(ToHex(tx))['hex'] + + # ensure signing process did not disturb scriptPubKey + signedtx = FromHex(CTransaction(), rawtx) + assert_equal(scriptPubKey, signedtx.vout[0].scriptPubKey) + txid = signedtx.rehash() + + balance_initial = std_node.getbalance() + + # try broadcasting it on the standard node + if shouldBeStandard: + std_node.sendrawtransaction(rawtx) + else: + assert_raises_rpc_error(-26, nonstd_error, + std_node.sendrawtransaction, rawtx) + + # make sure it's in nonstandard node's mempool, then mine it + nonstd_node.sendrawtransaction(rawtx) + assert txid in nonstd_node.getrawmempool() + [blockhash] = nonstd_node.generate(1) + # make sure it was mined + assert txid in nonstd_node.getblock(blockhash)["tx"] + + sync_blocks(self.nodes) + + wallet_outpoints = {(entry['txid'], entry['vout']) + for entry in std_node.listunspent()} + + # calculate wallet balance change just as a double check + balance_change = std_node.getbalance() - balance_initial + + # try spending the funds using the wallet. + outamount = (amount-spendfee) * SATOSHI + if outamount < 546 * SATOSHI: + # If the final amount would be too small, then just donate + # to miner fees. + outputs = [{"data": b"to miner, with love".hex()}] + else: + outputs = [{address_nonstd: outamount}] + spendtx = std_node.createrawtransaction( + [{'txid': txid, 'vout': 0}], outputs) + signresult = std_node.signrawtransactionwithwallet(spendtx) + + if shouldBeInWallet: + assert (txid, 0) in wallet_outpoints + assert balance_change == amount * SATOSHI + assert_equal(signresult['complete'], True) + txid = std_node.sendrawtransaction(signresult['hex']) + [blockhash] = std_node.generate(1) + # make sure it was mined + assert txid in std_node.getblock(blockhash)["tx"] + sync_blocks(self.nodes) + else: + assert (txid, 0) not in wallet_outpoints + assert balance_change == 0 + # signresult['errors'] will vary depending on input script. What + # occurs is that in sign.cpp, ProduceSignature gets back + # solved=false since SignStep sees a nonstandard input. Then, + # an empty SignatureData results. Back in rawtransaction.cpp's + # SignTransaction, it will then attempt to execute the + # scriptPubKey with an empty scriptSig. A P2PKH script will thus + # fail at OP_DUP with stack error, and P2PK/Multisig will fail + # once they hit a nonminimal push. The error message is just an + # artifact of the script type, basically. + assert_equal(signresult['complete'], False) + + # we start with an empty wallet + assert_equal(std_node.getbalance(), 0) + + address = std_node.getnewaddress() + pubkey = bytes.fromhex(std_node.getaddressinfo(address)['pubkey']) + pubkeyhash = hash160(pubkey) + + # P2PK + fund_and_test_wallet(CScript([pubkey, OP_CHECKSIG]), True, True) + fund_and_test_wallet( + CScript([OP_PUSHDATA1, pubkey, OP_CHECKSIG]), False, False) + + # P2PKH + fund_and_test_wallet(CScript( + [OP_DUP, OP_HASH160, pubkeyhash, OP_EQUALVERIFY, OP_CHECKSIG]), True, True) + fund_and_test_wallet(CScript( + [OP_DUP, OP_HASH160, OP_PUSHDATA1, pubkeyhash, OP_EQUALVERIFY, OP_CHECKSIG]), False, False) + + # Bare multisig + fund_and_test_wallet( + CScript([OP_1, pubkey, OP_1, OP_CHECKMULTISIG]), True, True) + fund_and_test_wallet( + CScript([OP_1, OP_PUSHDATA1, pubkey, OP_1, OP_CHECKMULTISIG]), False, False) + fund_and_test_wallet( + CScript([OP_1, pubkey, b'\x01', OP_CHECKMULTISIG]), False, False) + fund_and_test_wallet( + CScript([b'\x01', pubkey, OP_1, OP_CHECKMULTISIG]), False, False) + # Note: 1-of-5 is nonstandard to fund but standard to spend. + fund_and_test_wallet( + CScript([OP_1, pubkey, pubkey, pubkey, pubkey, pubkey, OP_5, OP_CHECKMULTISIG]), False, True) + fund_and_test_wallet( + CScript([OP_1, pubkey, pubkey, pubkey, OP_PUSHDATA1, pubkey, pubkey, OP_5, OP_CHECKMULTISIG]), False, False) + + # Dust also is nonstandard to fund but standard to spend. + fund_and_test_wallet( + CScript([pubkey, OP_CHECKSIG]), False, True, amount=200, nonstd_error="dust (code 64)") + + # and we end with an empty wallet + assert_equal(std_node.getbalance(), 0) + + +if __name__ == '__main__': + WalletStandardnessTest().main()