mirror of
https://github.com/ElementsProject/elements.git
synced 2026-08-16 13:01:19 +02:00
This modifies the CreateTransaction loop in a way not remotely worth the complexity, and includes an absurdly fragile test where I had to add a bunch of trace statements and tweak pretty-much every single hardcoded number. Not to name names, but it was Sjors. (In fairness, the PR is a pure simplification of the CreateTransaction logic, and it wasn't hard to merge even. It was just the test that caused my grief.) Adapting the "use a dummy CTxDestination in the case that we cannot retrieve one from the wallet" logic to our `mapScriptChange` map was not trivial. On my first attempt I incorrectly assigned a positive vout index to the dummy script, which caused us to call `ReturnDestination` later on the (unused) dummy destination. This is harmless now, but when descriptor wallets are introduced in #16528, they introduce an edge case where returning a null destination can incorrectly mark the 0th key of a BIP32 range as unused. This triggered a test failure much later, in #19504, which uses descriptor wallets in fundrawtransaction. The bug was that we'd import a descriptor, mark the first key as being used, lock the wallet, call `fundrawtransaction` on a transaction that did not require change (incorrectly marking the first key as unused but leaving it in the descriptor ScriptPubKeyMan's cache), then call `fundrawtransaction` again on a transaction that *did* require change. The wallet would then incorrectly retrieve the "unused" key from cache and use it for change, rather than correctly failing and advising the user that it could not produce change with a locked wallet and empty keypool. This was not a fun bug to track down. Another interesting observation is that branch-and-bound uses the CT size-overestimate for change when trying to create changeless outputs, while our normal dust detection uses Core's unchanged "an output is 133 bytes" logic. So when BnB is used we're willing to delete a far bigger change output than we are when we don't use BnB. Lest you think this works in Core, they're also inconsistent because BnB uses a normal fee estimate for gauging change cost, while non-BnB uses the discardfee rate. My advice is to hold your nose, pull stuff in from Core as it comes in, and thanks to Andy's efforts things are getting better. Don't bother reviewing this too closely.
137 lines
6.4 KiB
Python
Executable file
137 lines
6.4 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# Copyright (c) 2014-2019 The Bitcoin Core developers
|
|
# Distributed under the MIT software license, see the accompanying
|
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
"""Test the wallet keypool and interaction with wallet encryption/locking."""
|
|
|
|
import time
|
|
from decimal import Decimal
|
|
|
|
from test_framework.test_framework import BitcoinTestFramework
|
|
from test_framework.util import assert_equal, assert_raises_rpc_error
|
|
|
|
class KeyPoolTest(BitcoinTestFramework):
|
|
def set_test_params(self):
|
|
self.num_nodes = 1
|
|
|
|
def skip_test_if_missing_module(self):
|
|
self.skip_if_no_wallet()
|
|
|
|
def run_test(self):
|
|
nodes = self.nodes
|
|
addr_before_encrypting = nodes[0].getnewaddress()
|
|
addr_before_encrypting_data = nodes[0].getaddressinfo(addr_before_encrypting)
|
|
wallet_info_old = nodes[0].getwalletinfo()
|
|
assert addr_before_encrypting_data['hdseedid'] == wallet_info_old['hdseedid']
|
|
|
|
# Encrypt wallet and wait to terminate
|
|
nodes[0].encryptwallet('test')
|
|
# Keep creating keys
|
|
addr = nodes[0].getnewaddress()
|
|
addr_data = nodes[0].getaddressinfo(addr)
|
|
wallet_info = nodes[0].getwalletinfo()
|
|
assert addr_before_encrypting_data['hdseedid'] != wallet_info['hdseedid']
|
|
assert addr_data['hdseedid'] == wallet_info['hdseedid']
|
|
assert_raises_rpc_error(-12, "Error: Keypool ran out, please call keypoolrefill first", nodes[0].getnewaddress)
|
|
|
|
# put six (plus 2) new keys in the keypool (100% external-, +100% internal-keys, 1 in min)
|
|
nodes[0].walletpassphrase('test', 12000)
|
|
nodes[0].keypoolrefill(6)
|
|
nodes[0].walletlock()
|
|
wi = nodes[0].getwalletinfo()
|
|
assert_equal(wi['keypoolsize_hd_internal'], 6)
|
|
assert_equal(wi['keypoolsize'], 6)
|
|
|
|
# drain the internal keys
|
|
nodes[0].getrawchangeaddress()
|
|
nodes[0].getrawchangeaddress()
|
|
nodes[0].getrawchangeaddress()
|
|
nodes[0].getrawchangeaddress()
|
|
nodes[0].getrawchangeaddress()
|
|
nodes[0].getrawchangeaddress()
|
|
addr = set()
|
|
# the next one should fail
|
|
assert_raises_rpc_error(-12, "Keypool ran out", nodes[0].getrawchangeaddress)
|
|
|
|
# drain the external keys
|
|
addr.add(nodes[0].getnewaddress(address_type="bech32"))
|
|
addr.add(nodes[0].getnewaddress(address_type="bech32"))
|
|
addr.add(nodes[0].getnewaddress(address_type="bech32"))
|
|
addr.add(nodes[0].getnewaddress(address_type="bech32"))
|
|
addr.add(nodes[0].getnewaddress(address_type="bech32"))
|
|
addr.add(nodes[0].getnewaddress(address_type="bech32"))
|
|
assert len(addr) == 6
|
|
# the next one should fail
|
|
assert_raises_rpc_error(-12, "Error: Keypool ran out, please call keypoolrefill first", nodes[0].getnewaddress)
|
|
|
|
# refill keypool with three new addresses
|
|
nodes[0].walletpassphrase('test', 1)
|
|
nodes[0].keypoolrefill(3)
|
|
|
|
# test walletpassphrase timeout
|
|
time.sleep(1.1)
|
|
assert_equal(nodes[0].getwalletinfo()["unlocked_until"], 0)
|
|
|
|
# drain the keypool
|
|
for _ in range(3):
|
|
nodes[0].getnewaddress()
|
|
assert_raises_rpc_error(-12, "Keypool ran out", nodes[0].getnewaddress)
|
|
|
|
nodes[0].walletpassphrase('test', 100)
|
|
nodes[0].keypoolrefill(100)
|
|
wi = nodes[0].getwalletinfo()
|
|
assert_equal(wi['keypoolsize_hd_internal'], 100)
|
|
assert_equal(wi['keypoolsize'], 100)
|
|
|
|
# create a blank wallet
|
|
nodes[0].createwallet(wallet_name='w2', blank=True)
|
|
w2 = nodes[0].get_wallet_rpc('w2')
|
|
|
|
# refer to initial wallet as w1
|
|
w1 = nodes[0].get_wallet_rpc('')
|
|
|
|
# import private key and fund it
|
|
address = addr.pop()
|
|
privkey = w1.dumpprivkey(address)
|
|
res = w2.importmulti([{'scriptPubKey': {'address': address}, 'keys': [privkey], 'timestamp': 'now'}])
|
|
assert_equal(res[0]['success'], True)
|
|
w1.walletpassphrase('test', 100)
|
|
|
|
# ELEMENTS: the cost of change at a 10sat/b feerate is ~15000 sat,
|
|
# so we need to start with a bigger utxo to trigger change creation.
|
|
# all the below numbers are increased by 15000.
|
|
res = w1.sendtoaddress(address=address, amount=0.00025000)
|
|
nodes[0].generate(1)
|
|
destination = addr.pop()
|
|
|
|
# Using a fee rate (10 sat / byte) well above the minimum relay rate
|
|
# creating a 5,000 sat transaction with change should not be possible
|
|
assert_raises_rpc_error(-4, "Transaction needs a change address, but we can't generate it. Please call keypoolrefill first.", w2.walletcreatefundedpsbt, inputs=[], outputs=[{addr.pop(): 0.00005000}], options={"subtractFeeFromOutputs": [0], "feeRate": 0.00010})
|
|
|
|
# creating a 10,000 sat transaction without change, with a manual input, should still be possible
|
|
res = w2.walletcreatefundedpsbt(inputs=w2.listunspent(), outputs=[{destination: 0.00025000}], options={"subtractFeeFromOutputs": [0], "feeRate": 0.00010})
|
|
assert_equal("psbt" in res, True)
|
|
|
|
# creating a 10,000 sat transaction without change should still be possible
|
|
res = w2.walletcreatefundedpsbt(inputs=[], outputs=[{destination: 0.00025000}], options={"subtractFeeFromOutputs": [0], "feeRate": 0.00010})
|
|
assert_equal("psbt" in res, True)
|
|
# should work without subtractFeeFromOutputs if the exact fee is subtracted from the amount
|
|
res = w2.walletcreatefundedpsbt(inputs=[], outputs=[{destination: 0.00021650}], options={"feeRate": 0.00010})
|
|
assert_equal("psbt" in res, True)
|
|
|
|
# dust change should be removed
|
|
res = w2.walletcreatefundedpsbt(inputs=[], outputs=[{destination: 0.00021000}], options={"feeRate": 0.00010})
|
|
assert_equal("psbt" in res, True)
|
|
|
|
# create a transaction without change at the maximum fee rate, such that the output is still spendable:
|
|
res = w2.walletcreatefundedpsbt(inputs=[], outputs=[{destination: 0.00025000}], options={"subtractFeeFromOutputs": [0], "feeRate": 0.0008824})
|
|
assert_equal("psbt" in res, True)
|
|
assert_equal(res["fee"], Decimal("0.00016853"))
|
|
|
|
# creating a 10,000 sat transaction with a manual change address should be possible
|
|
res = w2.walletcreatefundedpsbt(inputs=[], outputs=[{destination: 0.00025000}], options={"subtractFeeFromOutputs": [0], "feeRate": 0.00010, "changeAddress": addr.pop()})
|
|
assert_equal("psbt" in res, True)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
KeyPoolTest().main()
|