mirror of
https://github.com/ElementsProject/elements.git
synced 2026-08-16 13:01:19 +02:00
First, this reverts commitca2d72ae8bto reinstate an assertion that was added in Bitcoin #22686. It did not compile because our `change_and_fee` variable is a map rather than number; I changed it to use `map_change_and_fee.at(policyAsset)` to match the equivalent change 2 lines down froma5d97b363b(merge of Bitcoin #22008). Then fix the following bugs: 1. Change the new test in rpc_fundrawtransaction.py to bump the -maxtxfee value, which we'd otherwise exceed, failing the test and masking actual failures. (This was just caused by the extreme fee settings of the test combined with Elements' large transactions.) 2. Change the fee-output size estimation for `tx_noinputs_size` to be 46 rather than 44 bytes; we forgot that even null surjection/rangeproofs need a 0 byte when output witnesses are present. This mistake triggered the new assertion. 3. Correct the logic in which change outputs are sometimes dropped even when they are the only blinded output in a transaction with blinded inputs. This would cause the new test to fail with `bad-txn-inputs-ne-outputs`; I'm very surprised that no existing tests hit this. (I have an existing comment block in this code where I "promise" that I had a good reason for doing something mysterious related to blinding. I was not able to reverse-engineer my intention here, though I think it is related to this, but since I couldn't understand it I just left this block intact and worked around it.) 4. This then triggered the assertion again since the coin selection code assumes that sufficiently-small change will always be dropped. If we prevent this drop we will have under-funded the transaction. To fix this we add Yet Another Flag `may_need_blinded_dummy` in which we add extra weight to `tx_noinputs_size` in the case that we're doing a blinded tx but have no blind destinations. We turn this off after coin selection if it turns out that we don't have any blinded inputs, though ofc at that point much of the damage/inefficiency has already been done.. 5. Fix some constants in other functional tests which assumed precise fee calculations; these precise values changed because of fixes (2) and (4). There is one new FIXME, which is that the "dummy change" value will now be a zero-valued OP_RETURN but we still put a full-size rangeproof and surjection proof on it. There is some plausible privacy benefit to this but not much, and wasting 5000+ bytes rather than the ~65 needed for an exact-value proof is not worth it. We will fix this in the future when we overhaul the wallet blinding logic.
195 lines
9.2 KiB
Python
Executable file
195 lines
9.2 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# Copyright (c) 2014-2020 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()
|
|
if not self.options.descriptors:
|
|
assert addr_before_encrypting_data['hdseedid'] == wallet_info_old['hdseedid']
|
|
|
|
# Encrypt wallet and wait to terminate
|
|
nodes[0].encryptwallet('test')
|
|
if self.options.descriptors:
|
|
# Import hardened derivation only descriptors
|
|
nodes[0].walletpassphrase('test', 10)
|
|
nodes[0].importdescriptors([
|
|
{
|
|
"desc": "wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/*h)#y4dfsj7n",
|
|
"timestamp": "now",
|
|
"range": [0,0],
|
|
"active": True
|
|
},
|
|
{
|
|
"desc": "pkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1h/*h)#a0nyvl0k",
|
|
"timestamp": "now",
|
|
"range": [0,0],
|
|
"active": True
|
|
},
|
|
{
|
|
"desc": "sh(wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/2h/*h))#lmeu2axg",
|
|
"timestamp": "now",
|
|
"range": [0,0],
|
|
"active": True
|
|
},
|
|
{
|
|
"desc": "wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/3h/*h)#jkl636gm",
|
|
"timestamp": "now",
|
|
"range": [0,0],
|
|
"active": True,
|
|
"internal": True
|
|
},
|
|
{
|
|
"desc": "pkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/4h/*h)#l3crwaus",
|
|
"timestamp": "now",
|
|
"range": [0,0],
|
|
"active": True,
|
|
"internal": True
|
|
},
|
|
{
|
|
"desc": "sh(wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/5h/*h))#qg8wa75f",
|
|
"timestamp": "now",
|
|
"range": [0,0],
|
|
"active": True,
|
|
"internal": True
|
|
}
|
|
])
|
|
nodes[0].walletlock()
|
|
# Keep creating keys
|
|
addr = nodes[0].getnewaddress()
|
|
addr_data = nodes[0].getaddressinfo(addr)
|
|
wallet_info = nodes[0].getwalletinfo()
|
|
assert addr_before_encrypting_data['hdmasterfingerprint'] != addr_data['hdmasterfingerprint']
|
|
if not self.options.descriptors:
|
|
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()
|
|
if self.options.descriptors:
|
|
assert_equal(wi['keypoolsize_hd_internal'], 18)
|
|
assert_equal(wi['keypoolsize'], 18)
|
|
else:
|
|
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()
|
|
if self.options.descriptors:
|
|
assert_equal(wi['keypoolsize_hd_internal'], 300)
|
|
assert_equal(wi['keypoolsize'], 300)
|
|
else:
|
|
assert_equal(wi['keypoolsize_hd_internal'], 100)
|
|
assert_equal(wi['keypoolsize'], 100)
|
|
|
|
# create a blank wallet
|
|
nodes[0].createwallet(wallet_name='w2', blank=True, disable_private_keys=True)
|
|
w2 = nodes[0].get_wallet_rpc('w2')
|
|
|
|
# refer to initial wallet as w1
|
|
w1 = nodes[0].get_wallet_rpc(self.default_wallet_name)
|
|
|
|
# import private key and fund it
|
|
address = addr.pop()
|
|
desc = w1.getaddressinfo(address)['desc']
|
|
if self.options.descriptors:
|
|
res = w2.importdescriptors([{'desc': desc, 'timestamp': 'now'}])
|
|
else:
|
|
res = w2.importmulti([{'desc': desc, '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.", 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.00008570}], options={"feeRate": 0.00010})
|
|
assert_equal("psbt" in res, True)
|
|
|
|
# dust change should be removed
|
|
res = w2.walletcreatefundedpsbt(inputs=[], outputs=[{destination: 0.00008200}], 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()
|