2019-04-27 19:11:43 +02:00
#!/usr/bin/env python3
2021-07-28 13:57:16 +02:00
# Copyright (c) 2017-2021 The Bitcoin Core developers
2019-04-27 19:11:43 +02:00
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
""" Test external signer.
Verify that a bitcoind node can use an external signer command
2021-03-18 14:17:39 +01:00
See also rpc_signer . py for tests without wallet context .
2019-04-27 19:11:43 +02:00
"""
import os
import platform
from test_framework . test_framework import BitcoinTestFramework
from test_framework . util import (
assert_equal ,
assert_raises_rpc_error ,
)
2021-03-18 14:17:39 +01:00
class WalletSignerTest ( BitcoinTestFramework ) :
2019-04-27 19:11:43 +02:00
def mock_signer_path ( self ) :
path = os . path . join ( os . path . dirname ( os . path . realpath ( __file__ ) ) , ' mocks ' , ' signer.py ' )
if platform . system ( ) == " Windows " :
return " py " + path
else :
return path
2021-11-29 15:48:36 +01:00
def mock_invalid_signer_path ( self ) :
path = os . path . join ( os . path . dirname ( os . path . realpath ( __file__ ) ) , ' mocks ' , ' invalid_signer.py ' )
if platform . system ( ) == " Windows " :
return " py " + path
else :
return path
2022-05-28 20:40:51 +02:00
def mock_multi_signers_path ( self ) :
path = os . path . join ( os . path . dirname ( os . path . realpath ( __file__ ) ) , ' mocks ' , ' multi_signers.py ' )
if platform . system ( ) == " Windows " :
return " py " + path
else :
return path
2019-04-27 19:11:43 +02:00
def set_test_params ( self ) :
2021-03-18 14:17:39 +01:00
self . num_nodes = 2
2021-10-01 15:53:59 +02:00
# The experimental syscall sandbox feature (-sandbox) is not compatible with -signer (which
# invokes execve).
self . disable_syscall_sandbox = True
2019-04-27 19:11:43 +02:00
self . extra_args = [
[ ] ,
[ f " -signer= { self . mock_signer_path ( ) } " , ' -keypool=10 ' ] ,
]
def skip_test_if_missing_module ( self ) :
self . skip_if_no_external_signer ( )
2021-03-18 14:17:39 +01:00
self . skip_if_no_wallet ( )
2019-04-27 19:11:43 +02:00
def set_mock_result ( self , node , res ) :
with open ( os . path . join ( node . cwd , " mock_result " ) , " w " , encoding = " utf8 " ) as f :
f . write ( res )
def clear_mock_result ( self , node ) :
os . remove ( os . path . join ( node . cwd , " mock_result " ) )
def run_test ( self ) :
2021-11-29 15:48:36 +01:00
self . test_valid_signer ( )
self . restart_node ( 1 , [ f " -signer= { self . mock_invalid_signer_path ( ) } " , " -keypool=10 " ] )
self . test_invalid_signer ( )
2022-05-28 20:40:51 +02:00
self . restart_node ( 1 , [ f " -signer= { self . mock_multi_signers_path ( ) } " , " -keypool=10 " ] )
self . test_multiple_signers ( )
2021-11-29 15:48:36 +01:00
def test_valid_signer ( self ) :
2019-04-27 19:11:43 +02:00
self . log . debug ( f " -signer= { self . mock_signer_path ( ) } " )
2019-08-04 17:56:17 +02:00
# Create new wallets for an external signer.
# disable_private_keys and descriptors must be true:
assert_raises_rpc_error ( - 4 , " Private keys must be disabled when using an external signer " , self . nodes [ 1 ] . createwallet , wallet_name = ' not_hww ' , disable_private_keys = False , descriptors = True , external_signer = True )
if self . is_bdb_compiled ( ) :
assert_raises_rpc_error ( - 4 , " Descriptor support must be enabled when using an external signer " , self . nodes [ 1 ] . createwallet , wallet_name = ' not_hww ' , disable_private_keys = True , descriptors = False , external_signer = True )
else :
assert_raises_rpc_error ( - 4 , " Compiled without bdb support (required for legacy wallets) " , self . nodes [ 1 ] . createwallet , wallet_name = ' not_hww ' , disable_private_keys = True , descriptors = False , external_signer = True )
self . nodes [ 1 ] . createwallet ( wallet_name = ' hww ' , disable_private_keys = True , descriptors = True , external_signer = True )
2019-02-15 12:54:29 +01:00
hww = self . nodes [ 1 ] . get_wallet_rpc ( ' hww ' )
2022-02-10 02:23:47 +01:00
assert_equal ( hww . getwalletinfo ( ) [ " external_signer " ] , True )
2019-02-15 12:54:29 +01:00
2019-08-04 17:56:39 +02:00
# Flag can't be set afterwards (could be added later for non-blank descriptor based watch-only wallets)
self . nodes [ 1 ] . createwallet ( wallet_name = ' not_hww ' , disable_private_keys = True , descriptors = True , external_signer = False )
not_hww = self . nodes [ 1 ] . get_wallet_rpc ( ' not_hww ' )
2022-02-10 02:23:47 +01:00
assert_equal ( not_hww . getwalletinfo ( ) [ " external_signer " ] , False )
2019-08-04 17:56:39 +02:00
assert_raises_rpc_error ( - 8 , " Wallet flag is immutable: external_signer " , not_hww . setwalletflag , " external_signer " , True )
2019-10-31 10:27:47 +01:00
# assert_raises_rpc_error(-4, "Multiple signers found, please specify which to use", wallet_name='not_hww', disable_private_keys=True, descriptors=True, external_signer=True)
# TODO: Handle error thrown by script
# self.set_mock_result(self.nodes[1], "2")
# assert_raises_rpc_error(-1, 'Unable to parse JSON',
# self.nodes[1].createwallet, wallet_name='not_hww2', disable_private_keys=True, descriptors=True, external_signer=False
# )
# self.clear_mock_result(self.nodes[1])
2022-05-20 09:54:41 +02:00
assert_equal ( hww . getwalletinfo ( ) [ " keypoolsize " ] , 40 )
2019-10-31 10:27:47 +01:00
address1 = hww . getnewaddress ( address_type = " bech32 " )
assert_equal ( address1 , " bcrt1qm90ugl4d48jv8n6e5t9ln6t9zlpm5th68x4f8g " )
address_info = hww . getaddressinfo ( address1 )
assert_equal ( address_info [ ' solvable ' ] , True )
assert_equal ( address_info [ ' ismine ' ] , True )
assert_equal ( address_info [ ' hdkeypath ' ] , " m/84 ' /1 ' /0 ' /0/0 " )
address2 = hww . getnewaddress ( address_type = " p2sh-segwit " )
assert_equal ( address2 , " 2N2gQKzjUe47gM8p1JZxaAkTcoHPXV6YyVp " )
address_info = hww . getaddressinfo ( address2 )
assert_equal ( address_info [ ' solvable ' ] , True )
assert_equal ( address_info [ ' ismine ' ] , True )
assert_equal ( address_info [ ' hdkeypath ' ] , " m/49 ' /1 ' /0 ' /0/0 " )
address3 = hww . getnewaddress ( address_type = " legacy " )
assert_equal ( address3 , " n1LKejAadN6hg2FrBXoU1KrwX4uK16mco9 " )
address_info = hww . getaddressinfo ( address3 )
assert_equal ( address_info [ ' solvable ' ] , True )
assert_equal ( address_info [ ' ismine ' ] , True )
assert_equal ( address_info [ ' hdkeypath ' ] , " m/44 ' /1 ' /0 ' /0/0 " )
2022-05-20 09:54:41 +02:00
address4 = hww . getnewaddress ( address_type = " bech32m " )
assert_equal ( address4 , " bcrt1phw4cgpt6cd30kz9k4wkpwm872cdvhss29jga2xpmftelhqll62ms4e9sqj " )
address_info = hww . getaddressinfo ( address4 )
assert_equal ( address_info [ ' solvable ' ] , True )
assert_equal ( address_info [ ' ismine ' ] , True )
assert_equal ( address_info [ ' hdkeypath ' ] , " m/86 ' /1 ' /0 ' /0/0 " )
2021-03-18 14:17:39 +01:00
self . log . info ( ' Test walletdisplayaddress ' )
result = hww . walletdisplayaddress ( address1 )
2020-02-19 14:33:37 +01:00
assert_equal ( result , { " address " : address1 } )
# Handle error thrown by script
self . set_mock_result ( self . nodes [ 1 ] , " 2 " )
assert_raises_rpc_error ( - 1 , ' RunCommandParseJSON error ' ,
2021-03-18 14:17:39 +01:00
hww . walletdisplayaddress , address1
2020-02-19 14:33:37 +01:00
)
self . clear_mock_result ( self . nodes [ 1 ] )
2019-08-04 23:26:01 +02:00
self . log . info ( ' Prepare mock PSBT ' )
2022-05-20 09:54:41 +02:00
self . nodes [ 0 ] . sendtoaddress ( address4 , 1 )
2021-08-19 17:10:24 +02:00
self . generate ( self . nodes [ 0 ] , 1 )
2019-08-04 23:26:01 +02:00
# Load private key into wallet to generate a signed PSBT for the mock
self . nodes [ 1 ] . createwallet ( wallet_name = " mock " , disable_private_keys = False , blank = True , descriptors = True )
mock_wallet = self . nodes [ 1 ] . get_wallet_rpc ( " mock " )
assert mock_wallet . getwalletinfo ( ) [ ' private_keys_enabled ' ]
result = mock_wallet . importdescriptors ( [ {
2022-05-20 09:54:41 +02:00
" desc " : " tr([00000001/86 ' /1 ' /0 ' ]tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0/*)#0jtt2jc9 " ,
2019-08-04 23:26:01 +02:00
" timestamp " : 0 ,
" range " : [ 0 , 1 ] ,
" internal " : False ,
" active " : True
} ,
{
2022-05-20 09:54:41 +02:00
" desc " : " tr([00000001/86 ' /1 ' /0 ' ]tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/*)#7xw2h8ga " ,
2019-08-04 23:26:01 +02:00
" timestamp " : 0 ,
" range " : [ 0 , 0 ] ,
" internal " : True ,
" active " : True
} ] )
assert_equal ( result [ 0 ] , { ' success ' : True } )
assert_equal ( result [ 1 ] , { ' success ' : True } )
assert_equal ( mock_wallet . getwalletinfo ( ) [ " txcount " ] , 1 )
dest = self . nodes [ 0 ] . getnewaddress ( address_type = ' bech32 ' )
mock_psbt = mock_wallet . walletcreatefundedpsbt ( [ ] , { dest : 0.5 } , 0 , { } , True ) [ ' psbt ' ]
mock_psbt_signed = mock_wallet . walletprocesspsbt ( psbt = mock_psbt , sign = True , sighashtype = " ALL " , bip32derivs = True )
mock_psbt_final = mock_wallet . finalizepsbt ( mock_psbt_signed [ " psbt " ] )
mock_tx = mock_psbt_final [ " hex " ]
assert ( mock_wallet . testmempoolaccept ( [ mock_tx ] ) [ 0 ] [ " allowed " ] )
# # Create a new wallet and populate with specific public keys, in order
# # to work with the mock signed PSBT.
# self.nodes[1].createwallet(wallet_name="hww4", disable_private_keys=True, descriptors=True, external_signer=True)
# hww4 = self.nodes[1].get_wallet_rpc("hww4")
#
# descriptors = [{
# "desc": "wpkh([00000001/84'/1'/0']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/*)#x30uthjs",
# "timestamp": "now",
# "range": [0, 1],
# "internal": False,
# "watchonly": True,
# "active": True
# },
# {
# "desc": "wpkh([00000001/84'/1'/0']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/*)#h92akzzg",
# "timestamp": "now",
# "range": [0, 0],
# "internal": True,
# "watchonly": True,
# "active": True
# }]
# result = hww4.importdescriptors(descriptors)
# assert_equal(result[0], {'success': True})
# assert_equal(result[1], {'success': True})
assert_equal ( hww . getwalletinfo ( ) [ " txcount " ] , 1 )
assert ( hww . testmempoolaccept ( [ mock_tx ] ) [ 0 ] [ " allowed " ] )
with open ( os . path . join ( self . nodes [ 1 ] . cwd , " mock_psbt " ) , " w " , encoding = " utf8 " ) as f :
f . write ( mock_psbt_signed [ " psbt " ] )
self . log . info ( ' Test send using hww1 ' )
res = hww . send ( outputs = { dest : 0.5 } , options = { " add_to_wallet " : False } )
assert ( res [ " complete " ] )
assert_equal ( res [ " hex " ] , mock_tx )
Add sendall RPC née sweep
_Motivation_
Currently, the wallet uses a fSubtractFeeAmount (SFFO) flag on the
recipients objects for all forms of sending calls. According to the
commit discussion, this flag was chiefly introduced to permit sweeping
without manually calculating the fees of transactions. However, the flag
leads to unintuitive behavior and makes it more complicated to test
many wallet RPCs exhaustively. We proposed to introduce a dedicated
`sendall` RPC with the intention to cover this functionality.
Since the proposal, it was discovered in further discussion that our
proposed `sendall` rpc and SFFO have subtly different scopes of
operation.
• sendall:
Use _specific UTXOs_ to pay a destination the remainder after fees.
• SFFO:
Use a _specific budget_ to pay an address the remainder after fees.
While `sendall` will simplify cases of spending from specific UTXOs,
emptying a wallet, or burning dust, we realized that there are some
cases in which SFFO is used to pay other parties from a limited budget,
which can often lead to the creation of change outputs. This cannot be
easily replicated using `sendall` as it would require manual computation
of the appropriate change amount.
As such, sendall cannot replace all uses of SFFO, but it still has a
different use case and will aid in simplifying some wallet calls and
numerous wallet tests.
_Sendall call details_
The proposed sendall call builds a transaction from a specific subset of
the wallet's UTXO pool (by default all of them) and assigns the funds to
one or more receivers. Receivers can either be specified with a specific
amount or receive an equal share of the remaining unassigned funds. At
least one recipient must be provided without assigned amount to collect
the remainder. The `sendall` call will never create change. The call has
a `send_max` option that changes the default behavior of spending all
UTXOs ("no UTXO left behind"), to maximizing the output amount of the
transaction by skipping uneconomic UTXOs. The `send_max` option is
incompatible with providing a specific set of inputs.
2022-01-12 19:59:41 +01:00
self . log . info ( ' Test sendall using hww1 ' )
res = hww . sendall ( recipients = [ { dest : 0.5 } , hww . getrawchangeaddress ( ) ] , options = { " add_to_wallet " : False } )
assert ( res [ " complete " ] )
assert_equal ( res [ " hex " ] , mock_tx )
2019-08-04 23:26:01 +02:00
# # Handle error thrown by script
# self.set_mock_result(self.nodes[4], "2")
# assert_raises_rpc_error(-1, 'Unable to parse JSON',
# hww4.signerprocesspsbt, psbt_orig, "00000001"
# )
# self.clear_mock_result(self.nodes[4])
2021-11-29 15:48:36 +01:00
def test_invalid_signer ( self ) :
self . log . debug ( f " -signer= { self . mock_invalid_signer_path ( ) } " )
self . log . info ( ' Test invalid external signer ' )
assert_raises_rpc_error ( - 1 , " Invalid descriptor " , self . nodes [ 1 ] . createwallet , wallet_name = ' hww_invalid ' , disable_private_keys = True , descriptors = True , external_signer = True )
2022-05-28 20:40:51 +02:00
def test_multiple_signers ( self ) :
self . log . debug ( f " -signer= { self . mock_multi_signers_path ( ) } " )
self . log . info ( ' Test multiple external signers ' )
assert_raises_rpc_error ( - 1 , " GetExternalSigner: More than one external signer found " , self . nodes [ 1 ] . createwallet , wallet_name = ' multi_hww ' , disable_private_keys = True , descriptors = True , external_signer = True )
2019-04-27 19:11:43 +02:00
if __name__ == ' __main__ ' :
2021-03-18 14:17:39 +01:00
WalletSignerTest ( ) . main ( )