2018-09-20 21:23:42 -03:00
#!/usr/bin/env python3
2022-12-24 20:49:50 -03:00
# Copyright (c) 2018-2022 The Bitcoin Core developers
2018-09-20 21:23:42 -03:00
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
""" Test bitcoin-wallet. """
2019-03-27 12:34:39 -03:00
2019-04-03 12:54:49 -03:00
import os
2024-04-22 16:42:44 -04:00
import platform
2019-04-03 12:54:49 -03:00
import stat
2018-09-20 21:23:42 -03:00
import subprocess
import textwrap
2020-06-01 18:22:56 -04:00
from collections import OrderedDict
2018-09-20 21:23:42 -03:00
from test_framework . test_framework import BitcoinTestFramework
2023-05-04 08:08:09 -04:00
from test_framework . util import (
assert_equal ,
2024-04-22 16:06:29 -04:00
assert_greater_than ,
2023-05-04 08:08:09 -04:00
sha256sum_file ,
)
2019-04-03 12:54:49 -03:00
2020-04-18 18:31:01 -04:00
2018-09-20 21:23:42 -03:00
class ToolWalletTest ( BitcoinTestFramework ) :
2022-11-09 08:53:13 -03:00
def add_options ( self , parser ) :
self . add_wallet_options ( parser )
2024-01-02 18:36:17 -03:00
parser . add_argument ( " --bdbro " , action = " store_true " , help = " Use the BerkeleyRO internal parser when dumping a Berkeley DB wallet file " )
2024-04-22 17:08:18 -04:00
parser . add_argument ( " --swap-bdb-endian " , action = " store_true " , help = " When making Legacy BDB wallets, always make then byte swapped internally " )
2022-11-09 08:53:13 -03:00
2018-09-20 21:23:42 -03:00
def set_test_params ( self ) :
self . num_nodes = 1
self . setup_clean_chain = True
2019-09-16 14:19:29 -03:00
self . rpc_timeout = 120
2024-04-22 17:08:18 -04:00
if self . options . swap_bdb_endian :
self . extra_args = [ [ " -swapbdbendian " ] ]
2018-09-20 21:23:42 -03:00
def skip_test_if_missing_module ( self ) :
self . skip_if_no_wallet ( )
2019-11-18 18:48:52 -03:00
self . skip_if_no_wallet_tool ( )
2018-09-20 21:23:42 -03:00
def bitcoin_wallet_process ( self , * args ) :
2023-09-02 01:09:43 -04:00
default_args = [ ' -datadir= {} ' . format ( self . nodes [ 0 ] . datadir_path ) , ' -chain= %s ' % self . chain ]
2021-09-16 19:05:44 -03:00
if not self . options . descriptors and ' create ' in args :
default_args . append ( ' -legacy ' )
2024-01-02 18:36:17 -03:00
if " dump " in args and self . options . bdbro :
default_args . append ( " -withinternalbdb " )
2020-11-13 07:52:28 -03:00
2023-05-05 08:35:30 -04:00
return subprocess . Popen ( [ self . options . bitcoinwallet ] + default_args + list ( args ) , stdin = subprocess . PIPE , stdout = subprocess . PIPE , stderr = subprocess . PIPE , text = True )
2018-09-20 21:23:42 -03:00
def assert_raises_tool_error ( self , error , * args ) :
p = self . bitcoin_wallet_process ( * args )
stdout , stderr = p . communicate ( )
assert_equal ( stdout , ' ' )
2024-04-22 16:42:44 -04:00
if isinstance ( error , tuple ) :
assert_equal ( p . poll ( ) , error [ 0 ] )
assert error [ 1 ] in stderr . strip ( )
else :
assert_equal ( p . poll ( ) , 1 )
assert error in stderr . strip ( )
2018-09-20 21:23:42 -03:00
def assert_tool_output ( self , output , * args ) :
p = self . bitcoin_wallet_process ( * args )
stdout , stderr = p . communicate ( )
assert_equal ( stderr , ' ' )
assert_equal ( stdout , output )
2019-04-03 12:54:49 -03:00
assert_equal ( p . poll ( ) , 0 )
def wallet_shasum ( self ) :
2023-05-04 08:08:09 -04:00
return sha256sum_file ( self . wallet_path ) . hex ( )
2019-04-03 12:54:49 -03:00
def wallet_timestamp ( self ) :
return os . path . getmtime ( self . wallet_path )
def wallet_permissions ( self ) :
return oct ( os . lstat ( self . wallet_path ) . st_mode ) [ - 3 : ]
def log_wallet_timestamp_comparison ( self , old , new ) :
result = ' unchanged ' if new == old else ' increased! '
self . log . debug ( ' Wallet file timestamp {} ' . format ( result ) )
2018-09-20 21:23:42 -03:00
2022-08-25 15:40:08 -04:00
def get_expected_info_output ( self , name = " " , transactions = 0 , keypool = 2 , address = 0 , imported_privs = 0 ) :
2020-11-13 07:52:28 -03:00
wallet_name = self . default_wallet_name if name == " " else name
if self . options . descriptors :
2021-06-23 16:11:51 -04:00
output_types = 4 # p2pkh, p2sh, segwit, bech32m
2020-11-13 07:52:28 -03:00
return textwrap . dedent ( ''' \
Wallet info
== == == == == =
Name : % s
Format : sqlite
Descriptors : yes
Encrypted : no
HD ( hd seed available ) : yes
Keypool Size : % d
Transactions : % d
Address Book : % d
2022-08-25 15:40:08 -04:00
''' % (wallet_name, keypool * output_types, transactions, imported_privs * 3 + address))
2020-11-13 07:52:28 -03:00
else :
2021-06-23 16:11:51 -04:00
output_types = 3 # p2pkh, p2sh, segwit. Legacy wallets do not support bech32m.
2020-11-13 07:52:28 -03:00
return textwrap . dedent ( ''' \
Wallet info
== == == == == =
Name : % s
Format : bdb
Descriptors : no
Encrypted : no
HD ( hd seed available ) : yes
Keypool Size : % d
Transactions : % d
Address Book : % d
2022-08-25 15:40:08 -04:00
''' % (wallet_name, keypool, transactions, (address + imported_privs) * output_types))
2020-11-13 07:52:28 -03:00
2020-06-01 18:22:56 -04:00
def read_dump ( self , filename ) :
dump = OrderedDict ( )
with open ( filename , " r " , encoding = " utf8 " ) as f :
for row in f :
row = row . strip ( )
key , value = row . split ( ' , ' )
dump [ key ] = value
return dump
def assert_is_sqlite ( self , filename ) :
with open ( filename , ' rb ' ) as f :
file_magic = f . read ( 16 )
assert file_magic == b ' SQLite format 3 \x00 '
def assert_is_bdb ( self , filename ) :
with open ( filename , ' rb ' ) as f :
f . seek ( 12 , 0 )
file_magic = f . read ( 4 )
assert file_magic == b ' \x00 \x05 \x31 \x62 ' or file_magic == b ' \x62 \x31 \x05 \x00 '
def write_dump ( self , dump , filename , magic = None , skip_checksum = False ) :
if magic is None :
magic = " BITCOIN_CORE_WALLET_DUMP "
with open ( filename , " w " , encoding = " utf8 " ) as f :
row = " , " . join ( [ magic , dump [ magic ] ] ) + " \n "
f . write ( row )
for k , v in dump . items ( ) :
if k == magic or k == " checksum " :
continue
row = " , " . join ( [ k , v ] ) + " \n "
f . write ( row )
if not skip_checksum :
row = " , " . join ( [ " checksum " , dump [ " checksum " ] ] ) + " \n "
f . write ( row )
def assert_dump ( self , expected , received ) :
e = expected . copy ( )
r = received . copy ( )
# BDB will add a "version" record that is not present in sqlite
# In that case, we should ignore this record in both
# But because this also effects the checksum, we also need to drop that.
v_key = " 0776657273696f6e " # Version key
if v_key in e and v_key not in r :
del e [ v_key ]
del e [ " checksum " ]
del r [ " checksum " ]
if v_key not in e and v_key in r :
del r [ v_key ]
del e [ " checksum " ]
del r [ " checksum " ]
assert_equal ( len ( e ) , len ( r ) )
for k , v in e . items ( ) :
assert_equal ( v , r [ k ] )
def do_tool_createfromdump ( self , wallet_name , dumpfile , file_format = None ) :
2023-09-02 01:09:43 -04:00
dumppath = self . nodes [ 0 ] . datadir_path / dumpfile
rt_dumppath = self . nodes [ 0 ] . datadir_path / " rt- {} .dump " . format ( wallet_name )
2020-06-01 18:22:56 -04:00
dump_data = self . read_dump ( dumppath )
args = [ " -wallet= {} " . format ( wallet_name ) ,
" -dumpfile= {} " . format ( dumppath ) ]
if file_format is not None :
args . append ( " -format= {} " . format ( file_format ) )
args . append ( " createfromdump " )
load_output = " "
if file_format is not None and file_format != dump_data [ " format " ] :
load_output + = " Warning: Dumpfile wallet format \" {} \" does not match command line specified format \" {} \" . \n " . format ( dump_data [ " format " ] , file_format )
self . assert_tool_output ( load_output , * args )
2023-06-14 07:00:11 -04:00
assert ( self . nodes [ 0 ] . wallets_path / wallet_name ) . is_dir ( )
2020-06-01 18:22:56 -04:00
self . assert_tool_output ( " The dumpfile may contain private keys. To ensure the safety of your Bitcoin, do not share the dumpfile. \n " , ' -wallet= {} ' . format ( wallet_name ) , ' -dumpfile= {} ' . format ( rt_dumppath ) , ' dump ' )
rt_dump_data = self . read_dump ( rt_dumppath )
2023-06-14 07:00:11 -04:00
wallet_dat = self . nodes [ 0 ] . wallets_path / wallet_name / " wallet.dat "
2020-06-01 18:22:56 -04:00
if rt_dump_data [ " format " ] == " bdb " :
self . assert_is_bdb ( wallet_dat )
else :
self . assert_is_sqlite ( wallet_dat )
2019-06-27 09:22:17 -04:00
def test_invalid_tool_commands_and_args ( self ) :
2019-03-27 12:34:39 -03:00
self . log . info ( ' Testing that various invalid commands raise with specific error messages ' )
2021-01-13 05:00:43 -03:00
self . assert_raises_tool_error ( " Error parsing command line arguments: Invalid command ' foo ' " , ' foo ' )
2019-03-27 12:34:39 -03:00
# `bitcoin-wallet help` raises an error. Use `bitcoin-wallet -help`.
2021-01-13 05:00:43 -03:00
self . assert_raises_tool_error ( " Error parsing command line arguments: Invalid command ' help ' " , ' help ' )
self . assert_raises_tool_error ( ' Error: Additional arguments provided (create). Methods do not take arguments. Please refer to `-help`. ' , ' info ' , ' create ' )
2018-09-20 21:23:42 -03:00
self . assert_raises_tool_error ( ' Error parsing command line arguments: Invalid parameter -foo ' , ' -foo ' )
2021-01-21 09:57:59 -03:00
self . assert_raises_tool_error ( ' No method provided. Run `bitcoin-wallet -help` for valid methods. ' )
self . assert_raises_tool_error ( ' Wallet name must be provided when creating a new wallet. ' , ' create ' )
2023-06-14 07:00:11 -04:00
locked_dir = self . nodes [ 0 ] . wallets_path
2020-04-06 19:23:25 -04:00
error = ' Error initializing wallet database environment " {} " ! ' . format ( locked_dir )
if self . options . descriptors :
2021-10-19 17:53:23 -03:00
error = f " SQLiteDatabase: Unable to obtain an exclusive lock on the database, is it being used by another instance of { self . config [ ' environment ' ] [ ' PACKAGE_NAME ' ] } ? "
2020-04-18 18:31:01 -04:00
self . assert_raises_tool_error (
2020-04-06 19:23:25 -04:00
error ,
2020-09-28 21:24:06 -03:00
' -wallet= ' + self . default_wallet_name ,
2020-04-18 18:31:01 -04:00
' info ' ,
)
2023-06-14 07:00:11 -04:00
path = self . nodes [ 0 ] . wallets_path / " nonexistent.dat "
2020-08-04 21:16:48 -04:00
self . assert_raises_tool_error ( " Failed to load database path ' {} ' . Path does not exist. " . format ( path ) , ' -wallet=nonexistent.dat ' , ' info ' )
2018-09-20 21:23:42 -03:00
2019-06-27 09:22:17 -04:00
def test_tool_wallet_info ( self ) :
2019-03-27 12:34:39 -03:00
# Stop the node to close the wallet to call the info command.
2018-09-20 21:23:42 -03:00
self . stop_node ( 0 )
2019-03-27 12:34:39 -03:00
self . log . info ( ' Calling wallet tool info, testing output ' )
2019-04-03 12:54:49 -03:00
#
# TODO: Wallet tool info should work with wallet file permissions set to
# read-only without raising:
# "Error loading wallet.dat. Is wallet being used by another process?"
# The following lines should be uncommented and the tests still succeed:
#
# self.log.debug('Setting wallet file permissions to 400 (read-only)')
# os.chmod(self.wallet_path, stat.S_IRUSR)
2020-04-18 18:31:01 -04:00
# assert self.wallet_permissions() in ['400', '666'] # Sanity check. 666 because Appveyor.
2019-04-03 12:54:49 -03:00
# shasum_before = self.wallet_shasum()
timestamp_before = self . wallet_timestamp ( )
self . log . debug ( ' Wallet file timestamp before calling info: {} ' . format ( timestamp_before ) )
2022-08-25 15:40:08 -04:00
out = self . get_expected_info_output ( imported_privs = 1 )
2020-09-28 21:24:06 -03:00
self . assert_tool_output ( out , ' -wallet= ' + self . default_wallet_name , ' info ' )
2019-04-03 12:54:49 -03:00
timestamp_after = self . wallet_timestamp ( )
self . log . debug ( ' Wallet file timestamp after calling info: {} ' . format ( timestamp_after ) )
self . log_wallet_timestamp_comparison ( timestamp_before , timestamp_after )
self . log . debug ( ' Setting wallet file permissions back to 600 (read/write) ' )
os . chmod ( self . wallet_path , stat . S_IRUSR | stat . S_IWUSR )
2020-04-18 18:31:01 -04:00
assert self . wallet_permissions ( ) in [ ' 600 ' , ' 666 ' ] # Sanity check. 666 because Appveyor.
2019-04-03 12:54:49 -03:00
#
# TODO: Wallet tool info should not write to the wallet file.
# The following lines should be uncommented and the tests still succeed:
#
# assert_equal(timestamp_before, timestamp_after)
# shasum_after = self.wallet_shasum()
# assert_equal(shasum_before, shasum_after)
# self.log.debug('Wallet file shasum unchanged\n')
2018-09-20 21:23:42 -03:00
2019-06-27 09:22:17 -04:00
def test_tool_wallet_info_after_transaction ( self ) :
"""
Mutate the wallet with a transaction to verify that the info command
output changes accordingly .
"""
2018-09-20 21:23:42 -03:00
self . start_node ( 0 )
2019-03-27 12:34:39 -03:00
self . log . info ( ' Generating transaction to mutate wallet ' )
2021-08-19 11:10:24 -04:00
self . generate ( self . nodes [ 0 ] , 1 )
2018-09-20 21:23:42 -03:00
self . stop_node ( 0 )
2019-03-27 12:34:39 -03:00
self . log . info ( ' Calling wallet tool info after generating a transaction, testing output ' )
2019-04-03 12:54:49 -03:00
shasum_before = self . wallet_shasum ( )
timestamp_before = self . wallet_timestamp ( )
self . log . debug ( ' Wallet file timestamp before calling info: {} ' . format ( timestamp_before ) )
2022-08-25 15:40:08 -04:00
out = self . get_expected_info_output ( transactions = 1 , imported_privs = 1 )
2020-09-28 21:24:06 -03:00
self . assert_tool_output ( out , ' -wallet= ' + self . default_wallet_name , ' info ' )
2019-04-03 12:54:49 -03:00
shasum_after = self . wallet_shasum ( )
timestamp_after = self . wallet_timestamp ( )
self . log . debug ( ' Wallet file timestamp after calling info: {} ' . format ( timestamp_after ) )
self . log_wallet_timestamp_comparison ( timestamp_before , timestamp_after )
#
# TODO: Wallet tool info should not write to the wallet file.
# This assertion should be uncommented and succeed:
# assert_equal(timestamp_before, timestamp_after)
assert_equal ( shasum_before , shasum_after )
self . log . debug ( ' Wallet file shasum unchanged \n ' )
2018-09-20 21:23:42 -03:00
2019-06-27 09:22:17 -04:00
def test_tool_wallet_create_on_existing_wallet ( self ) :
2019-03-27 12:34:39 -03:00
self . log . info ( ' Calling wallet tool create on an existing wallet, testing output ' )
2019-04-03 12:54:49 -03:00
shasum_before = self . wallet_shasum ( )
timestamp_before = self . wallet_timestamp ( )
self . log . debug ( ' Wallet file timestamp before calling create: {} ' . format ( timestamp_before ) )
2020-11-13 07:52:28 -03:00
out = " Topping up keypool... \n " + self . get_expected_info_output ( name = " foo " , keypool = 2000 )
2018-09-20 21:23:42 -03:00
self . assert_tool_output ( out , ' -wallet=foo ' , ' create ' )
2019-04-03 12:54:49 -03:00
shasum_after = self . wallet_shasum ( )
timestamp_after = self . wallet_timestamp ( )
self . log . debug ( ' Wallet file timestamp after calling create: {} ' . format ( timestamp_after ) )
self . log_wallet_timestamp_comparison ( timestamp_before , timestamp_after )
assert_equal ( timestamp_before , timestamp_after )
assert_equal ( shasum_before , shasum_after )
self . log . debug ( ' Wallet file shasum unchanged \n ' )
2018-09-20 21:23:42 -03:00
2019-06-27 09:22:17 -04:00
def test_getwalletinfo_on_different_wallet ( self ) :
2019-03-27 12:34:39 -03:00
self . log . info ( ' Starting node with arg -wallet=foo ' )
2020-09-28 21:24:06 -03:00
self . start_node ( 0 , [ ' -nowallet ' , ' -wallet=foo ' ] )
2019-03-27 12:34:39 -03:00
self . log . info ( ' Calling getwalletinfo on a different wallet ( " foo " ), testing output ' )
2019-04-03 12:54:49 -03:00
shasum_before = self . wallet_shasum ( )
timestamp_before = self . wallet_timestamp ( )
self . log . debug ( ' Wallet file timestamp before calling getwalletinfo: {} ' . format ( timestamp_before ) )
2018-09-20 21:23:42 -03:00
out = self . nodes [ 0 ] . getwalletinfo ( )
self . stop_node ( 0 )
2019-04-03 12:54:49 -03:00
shasum_after = self . wallet_shasum ( )
timestamp_after = self . wallet_timestamp ( )
self . log . debug ( ' Wallet file timestamp after calling getwalletinfo: {} ' . format ( timestamp_after ) )
2018-09-20 21:23:42 -03:00
assert_equal ( 0 , out [ ' txcount ' ] )
2020-11-13 07:52:28 -03:00
if not self . options . descriptors :
assert_equal ( 1000 , out [ ' keypoolsize ' ] )
assert_equal ( 1000 , out [ ' keypoolsize_hd_internal ' ] )
assert_equal ( True , ' hdseedid ' in out )
else :
2021-06-23 16:11:51 -04:00
assert_equal ( 4000 , out [ ' keypoolsize ' ] )
assert_equal ( 4000 , out [ ' keypoolsize_hd_internal ' ] )
2018-09-20 21:23:42 -03:00
2019-04-03 12:54:49 -03:00
self . log_wallet_timestamp_comparison ( timestamp_before , timestamp_after )
assert_equal ( timestamp_before , timestamp_after )
assert_equal ( shasum_after , shasum_before )
self . log . debug ( ' Wallet file shasum unchanged \n ' )
2020-05-25 12:23:26 -04:00
def test_salvage ( self ) :
# TODO: Check salvage actually salvages and doesn't break things. https://github.com/bitcoin/bitcoin/issues/7463
self . log . info ( ' Check salvage ' )
2020-10-19 09:24:27 -03:00
self . start_node ( 0 )
self . nodes [ 0 ] . createwallet ( " salvage " )
2020-05-25 12:23:26 -04:00
self . stop_node ( 0 )
self . assert_tool_output ( ' ' , ' -wallet=salvage ' , ' salvage ' )
2020-06-01 18:22:56 -04:00
def test_dump_createfromdump ( self ) :
self . start_node ( 0 )
self . nodes [ 0 ] . createwallet ( " todump " )
file_format = self . nodes [ 0 ] . get_wallet_rpc ( " todump " ) . getwalletinfo ( ) [ " format " ]
self . nodes [ 0 ] . createwallet ( " todump2 " )
self . stop_node ( 0 )
self . log . info ( ' Checking dump arguments ' )
self . assert_raises_tool_error ( ' No dump file provided. To use dump, -dumpfile=<filename> must be provided. ' , ' -wallet=todump ' , ' dump ' )
self . log . info ( ' Checking basic dump ' )
2023-09-02 01:09:43 -04:00
wallet_dump = self . nodes [ 0 ] . datadir_path / " wallet.dump "
2020-06-01 18:22:56 -04:00
self . assert_tool_output ( ' The dumpfile may contain private keys. To ensure the safety of your Bitcoin, do not share the dumpfile. \n ' , ' -wallet=todump ' , ' -dumpfile= {} ' . format ( wallet_dump ) , ' dump ' )
dump_data = self . read_dump ( wallet_dump )
orig_dump = dump_data . copy ( )
# Check the dump magic
assert_equal ( dump_data [ ' BITCOIN_CORE_WALLET_DUMP ' ] , ' 1 ' )
# Check the file format
assert_equal ( dump_data [ " format " ] , file_format )
self . log . info ( ' Checking that a dumpfile cannot be overwritten ' )
self . assert_raises_tool_error ( ' File {} already exists. If you are sure this is what you want, move it out of the way first. ' . format ( wallet_dump ) , ' -wallet=todump2 ' , ' -dumpfile= {} ' . format ( wallet_dump ) , ' dump ' )
self . log . info ( ' Checking createfromdump arguments ' )
self . assert_raises_tool_error ( ' No dump file provided. To use createfromdump, -dumpfile=<filename> must be provided. ' , ' -wallet=todump ' , ' createfromdump ' )
2023-09-02 01:09:43 -04:00
non_exist_dump = self . nodes [ 0 ] . datadir_path / " wallet.nodump "
2020-06-01 18:22:56 -04:00
self . assert_raises_tool_error ( ' Unknown wallet file format " notaformat " provided. Please provide one of " bdb " or " sqlite " . ' , ' -wallet=todump ' , ' -format=notaformat ' , ' -dumpfile= {} ' . format ( wallet_dump ) , ' createfromdump ' )
self . assert_raises_tool_error ( ' Dump file {} does not exist. ' . format ( non_exist_dump ) , ' -wallet=todump ' , ' -dumpfile= {} ' . format ( non_exist_dump ) , ' createfromdump ' )
2023-06-14 07:00:11 -04:00
wallet_path = self . nodes [ 0 ] . wallets_path / " todump2 "
2020-06-01 18:22:56 -04:00
self . assert_raises_tool_error ( ' Failed to create database path \' {} \' . Database already exists. ' . format ( wallet_path ) , ' -wallet=todump2 ' , ' -dumpfile= {} ' . format ( wallet_dump ) , ' createfromdump ' )
2020-12-17 15:52:36 -03:00
self . assert_raises_tool_error ( " The -descriptors option can only be used with the ' create ' command. " , ' -descriptors ' , ' -wallet=todump2 ' , ' -dumpfile= {} ' . format ( wallet_dump ) , ' createfromdump ' )
2020-06-01 18:22:56 -04:00
self . log . info ( ' Checking createfromdump ' )
self . do_tool_createfromdump ( " load " , " wallet.dump " )
2021-01-08 21:31:18 -03:00
if self . is_bdb_compiled ( ) :
self . do_tool_createfromdump ( " load-bdb " , " wallet.dump " , " bdb " )
2020-12-17 15:52:58 -03:00
if self . is_sqlite_compiled ( ) :
self . do_tool_createfromdump ( " load-sqlite " , " wallet.dump " , " sqlite " )
2020-06-01 18:22:56 -04:00
self . log . info ( ' Checking createfromdump handling of magic and versions ' )
2023-09-02 01:09:43 -04:00
bad_ver_wallet_dump = self . nodes [ 0 ] . datadir_path / " wallet-bad_ver1.dump "
2020-06-01 18:22:56 -04:00
dump_data [ " BITCOIN_CORE_WALLET_DUMP " ] = " 0 "
self . write_dump ( dump_data , bad_ver_wallet_dump )
self . assert_raises_tool_error ( ' Error: Dumpfile version is not supported. This version of bitcoin-wallet only supports version 1 dumpfiles. Got dumpfile with version 0 ' , ' -wallet=badload ' , ' -dumpfile= {} ' . format ( bad_ver_wallet_dump ) , ' createfromdump ' )
2023-06-14 07:00:11 -04:00
assert not ( self . nodes [ 0 ] . wallets_path / " badload " ) . is_dir ( )
2023-09-02 01:09:43 -04:00
bad_ver_wallet_dump = self . nodes [ 0 ] . datadir_path / " wallet-bad_ver2.dump "
2020-06-01 18:22:56 -04:00
dump_data [ " BITCOIN_CORE_WALLET_DUMP " ] = " 2 "
self . write_dump ( dump_data , bad_ver_wallet_dump )
self . assert_raises_tool_error ( ' Error: Dumpfile version is not supported. This version of bitcoin-wallet only supports version 1 dumpfiles. Got dumpfile with version 2 ' , ' -wallet=badload ' , ' -dumpfile= {} ' . format ( bad_ver_wallet_dump ) , ' createfromdump ' )
2023-06-14 07:00:11 -04:00
assert not ( self . nodes [ 0 ] . wallets_path / " badload " ) . is_dir ( )
2023-09-02 01:09:43 -04:00
bad_magic_wallet_dump = self . nodes [ 0 ] . datadir_path / " wallet-bad_magic.dump "
2020-06-01 18:22:56 -04:00
del dump_data [ " BITCOIN_CORE_WALLET_DUMP " ]
dump_data [ " not_the_right_magic " ] = " 1 "
self . write_dump ( dump_data , bad_magic_wallet_dump , " not_the_right_magic " )
self . assert_raises_tool_error ( ' Error: Dumpfile identifier record is incorrect. Got " not_the_right_magic " , expected " BITCOIN_CORE_WALLET_DUMP " . ' , ' -wallet=badload ' , ' -dumpfile= {} ' . format ( bad_magic_wallet_dump ) , ' createfromdump ' )
2023-06-14 07:00:11 -04:00
assert not ( self . nodes [ 0 ] . wallets_path / " badload " ) . is_dir ( )
2020-06-01 18:22:56 -04:00
self . log . info ( ' Checking createfromdump handling of checksums ' )
2023-09-02 01:09:43 -04:00
bad_sum_wallet_dump = self . nodes [ 0 ] . datadir_path / " wallet-bad_sum1.dump "
2020-06-01 18:22:56 -04:00
dump_data = orig_dump . copy ( )
checksum = dump_data [ " checksum " ]
dump_data [ " checksum " ] = " 1 " * 64
self . write_dump ( dump_data , bad_sum_wallet_dump )
self . assert_raises_tool_error ( ' Error: Dumpfile checksum does not match. Computed {} , expected {} ' . format ( checksum , " 1 " * 64 ) , ' -wallet=bad ' , ' -dumpfile= {} ' . format ( bad_sum_wallet_dump ) , ' createfromdump ' )
2023-06-14 07:00:11 -04:00
assert not ( self . nodes [ 0 ] . wallets_path / " badload " ) . is_dir ( )
2023-09-02 01:09:43 -04:00
bad_sum_wallet_dump = self . nodes [ 0 ] . datadir_path / " wallet-bad_sum2.dump "
2020-06-01 18:22:56 -04:00
del dump_data [ " checksum " ]
self . write_dump ( dump_data , bad_sum_wallet_dump , skip_checksum = True )
self . assert_raises_tool_error ( ' Error: Missing checksum ' , ' -wallet=badload ' , ' -dumpfile= {} ' . format ( bad_sum_wallet_dump ) , ' createfromdump ' )
2023-06-14 07:00:11 -04:00
assert not ( self . nodes [ 0 ] . wallets_path / " badload " ) . is_dir ( )
2023-09-02 01:09:43 -04:00
bad_sum_wallet_dump = self . nodes [ 0 ] . datadir_path / " wallet-bad_sum3.dump "
2020-06-01 18:22:56 -04:00
dump_data [ " checksum " ] = " 2 " * 10
self . write_dump ( dump_data , bad_sum_wallet_dump )
2021-12-21 13:02:41 -03:00
self . assert_raises_tool_error ( ' Error: Checksum is not the correct size ' , ' -wallet=badload ' , ' -dumpfile= {} ' . format ( bad_sum_wallet_dump ) , ' createfromdump ' )
2023-06-14 07:00:11 -04:00
assert not ( self . nodes [ 0 ] . wallets_path / " badload " ) . is_dir ( )
2021-12-21 13:02:41 -03:00
dump_data [ " checksum " ] = " 3 " * 66
self . write_dump ( dump_data , bad_sum_wallet_dump )
self . assert_raises_tool_error ( ' Error: Checksum is not the correct size ' , ' -wallet=badload ' , ' -dumpfile= {} ' . format ( bad_sum_wallet_dump ) , ' createfromdump ' )
2023-06-14 07:00:11 -04:00
assert not ( self . nodes [ 0 ] . wallets_path / " badload " ) . is_dir ( )
2020-06-01 18:22:56 -04:00
2023-09-26 22:23:18 -03:00
def test_chainless_conflicts ( self ) :
self . log . info ( " Test wallet tool when wallet contains conflicting transactions " )
self . restart_node ( 0 )
self . generate ( self . nodes [ 0 ] , 101 )
def_wallet = self . nodes [ 0 ] . get_wallet_rpc ( self . default_wallet_name )
self . nodes [ 0 ] . createwallet ( " conflicts " )
wallet = self . nodes [ 0 ] . get_wallet_rpc ( " conflicts " )
def_wallet . sendtoaddress ( wallet . getnewaddress ( ) , 10 )
self . generate ( self . nodes [ 0 ] , 1 )
# parent tx
parent_txid = wallet . sendtoaddress ( wallet . getnewaddress ( ) , 9 )
parent_txid_bytes = bytes . fromhex ( parent_txid ) [ : : - 1 ]
conflict_utxo = wallet . gettransaction ( txid = parent_txid , verbose = True ) [ " decoded " ] [ " vin " ] [ 0 ]
# The specific assertion in MarkConflicted being tested requires that the parent tx is already loaded
# by the time the child tx is loaded. Since transactions end up being loaded in txid order due to how both
# and sqlite store things, we can just grind the child tx until it has a txid that is greater than the parent's.
locktime = 500000000 # Use locktime as nonce, starting at unix timestamp minimum
addr = wallet . getnewaddress ( )
while True :
child_send_res = wallet . send ( outputs = [ { addr : 8 } ] , add_to_wallet = False , locktime = locktime )
child_txid = child_send_res [ " txid " ]
child_txid_bytes = bytes . fromhex ( child_txid ) [ : : - 1 ]
if ( child_txid_bytes > parent_txid_bytes ) :
wallet . sendrawtransaction ( child_send_res [ " hex " ] )
break
locktime + = 1
# conflict with parent
conflict_unsigned = self . nodes [ 0 ] . createrawtransaction ( inputs = [ conflict_utxo ] , outputs = [ { wallet . getnewaddress ( ) : 9.9999 } ] )
conflict_signed = wallet . signrawtransactionwithwallet ( conflict_unsigned ) [ " hex " ]
conflict_txid = self . nodes [ 0 ] . sendrawtransaction ( conflict_signed )
self . generate ( self . nodes [ 0 ] , 1 )
assert_equal ( wallet . gettransaction ( txid = parent_txid ) [ " confirmations " ] , - 1 )
assert_equal ( wallet . gettransaction ( txid = child_txid ) [ " confirmations " ] , - 1 )
assert_equal ( wallet . gettransaction ( txid = conflict_txid ) [ " confirmations " ] , 1 )
self . stop_node ( 0 )
# Wallet tool should successfully give info for this wallet
expected_output = textwrap . dedent ( f ''' \
Wallet info
== == == == == =
Name : conflicts
Format : { " sqlite " if self . options . descriptors else " bdb " }
Descriptors : { " yes " if self . options . descriptors else " no " }
Encrypted : no
HD ( hd seed available ) : yes
Keypool Size : { " 8 " if self . options . descriptors else " 1 " }
Transactions : 4
Address Book : 4
''' )
self . assert_tool_output ( expected_output , " -wallet=conflicts " , " info " )
2020-06-01 18:22:56 -04:00
2024-04-22 15:39:07 -04:00
def test_dump_endianness ( self ) :
self . log . info ( " Testing dumps of the same contents with different BDB endianness " )
self . start_node ( 0 )
self . nodes [ 0 ] . createwallet ( " endian " )
self . stop_node ( 0 )
wallet_dump = self . nodes [ 0 ] . datadir_path / " endian.dump "
self . assert_tool_output ( " The dumpfile may contain private keys. To ensure the safety of your Bitcoin, do not share the dumpfile. \n " , " -wallet=endian " , f " -dumpfile= { wallet_dump } " , " dump " )
expected_dump = self . read_dump ( wallet_dump )
self . do_tool_createfromdump ( " native_endian " , " endian.dump " , " bdb " )
native_dump = self . read_dump ( self . nodes [ 0 ] . datadir_path / " rt-native_endian.dump " )
self . assert_dump ( expected_dump , native_dump )
self . do_tool_createfromdump ( " other_endian " , " endian.dump " , " bdb_swap " )
other_dump = self . read_dump ( self . nodes [ 0 ] . datadir_path / " rt-other_endian.dump " )
self . assert_dump ( expected_dump , other_dump )
2024-04-22 16:06:29 -04:00
def test_dump_very_large_records ( self ) :
self . log . info ( " Test that wallets with large records are successfully dumped " )
self . start_node ( 0 )
self . nodes [ 0 ] . createwallet ( " bigrecords " )
wallet = self . nodes [ 0 ] . get_wallet_rpc ( " bigrecords " )
# Both BDB and sqlite have maximum page sizes of 65536 bytes, with defaults of 4096
# When a record exceeds some size threshold, both BDB and SQLite will store the data
# in one or more overflow pages. We want to make sure that our tooling can dump such
# records, even when they span multiple pages. To make a large record, we just need
# to make a very big transaction.
self . generate ( self . nodes [ 0 ] , 101 )
def_wallet = self . nodes [ 0 ] . get_wallet_rpc ( self . default_wallet_name )
outputs = { }
for i in range ( 500 ) :
outputs [ wallet . getnewaddress ( address_type = " p2sh-segwit " ) ] = 0.01
def_wallet . sendmany ( amounts = outputs )
self . generate ( self . nodes [ 0 ] , 1 )
send_res = wallet . sendall ( [ def_wallet . getnewaddress ( ) ] )
self . generate ( self . nodes [ 0 ] , 1 )
assert_equal ( send_res [ " complete " ] , True )
tx = wallet . gettransaction ( txid = send_res [ " txid " ] , verbose = True )
assert_greater_than ( tx [ " decoded " ] [ " size " ] , 70000 )
self . stop_node ( 0 )
wallet_dump = self . nodes [ 0 ] . datadir_path / " bigrecords.dump "
self . assert_tool_output ( " The dumpfile may contain private keys. To ensure the safety of your Bitcoin, do not share the dumpfile. \n " , " -wallet=bigrecords " , f " -dumpfile= { wallet_dump } " , " dump " )
dump = self . read_dump ( wallet_dump )
for k , v in dump . items ( ) :
if tx [ " hex " ] in v :
break
else :
assert False , " Big transaction was not found in wallet dump "
2024-04-22 16:42:44 -04:00
def test_dump_unclean_lsns ( self ) :
if not self . options . bdbro :
return
self . log . info ( " Test that a legacy wallet that has not been compacted is not dumped by bdbro " )
self . start_node ( 0 , extra_args = [ " -flushwallet=0 " ] )
self . nodes [ 0 ] . createwallet ( " unclean_lsn " )
wallet = self . nodes [ 0 ] . get_wallet_rpc ( " unclean_lsn " )
# First unload and load normally to make sure everything is written
wallet . unloadwallet ( )
self . nodes [ 0 ] . loadwallet ( " unclean_lsn " )
# Next cause a bunch of writes by filling the keypool
wallet . keypoolrefill ( wallet . getwalletinfo ( ) [ " keypoolsize " ] + 100 )
# Lastly kill bitcoind so that the LSNs don't get reset
self . nodes [ 0 ] . process . kill ( )
self . nodes [ 0 ] . wait_until_stopped ( expected_ret_code = 1 if platform . system ( ) == " Windows " else - 9 )
assert self . nodes [ 0 ] . is_node_stopped ( )
wallet_dump = self . nodes [ 0 ] . datadir_path / " unclean_lsn.dump "
self . assert_raises_tool_error ( " LSNs are not reset, this database is not completely flushed. Please reopen then close the database with a version that has BDB support " , " -wallet=unclean_lsn " , f " -dumpfile= { wallet_dump } " , " dump " )
# File can be dumped after reload it normally
self . start_node ( 0 )
self . nodes [ 0 ] . loadwallet ( " unclean_lsn " )
self . stop_node ( 0 )
self . assert_tool_output ( " The dumpfile may contain private keys. To ensure the safety of your Bitcoin, do not share the dumpfile. \n " , " -wallet=unclean_lsn " , f " -dumpfile= { wallet_dump } " , " dump " )
2019-06-27 09:22:17 -04:00
def run_test ( self ) :
2023-09-02 01:09:43 -04:00
self . wallet_path = self . nodes [ 0 ] . wallets_path / self . default_wallet_name / self . wallet_data_filename
2019-06-27 09:22:17 -04:00
self . test_invalid_tool_commands_and_args ( )
# Warning: The following tests are order-dependent.
self . test_tool_wallet_info ( )
self . test_tool_wallet_info_after_transaction ( )
2020-11-13 07:52:28 -03:00
self . test_tool_wallet_create_on_existing_wallet ( )
self . test_getwalletinfo_on_different_wallet ( )
2020-10-27 15:01:12 -03:00
if not self . options . descriptors :
# Salvage is a legacy wallet only thing
self . test_salvage ( )
2024-04-22 15:39:07 -04:00
self . test_dump_endianness ( )
2024-04-22 16:42:44 -04:00
self . test_dump_unclean_lsns ( )
2020-06-01 18:22:56 -04:00
self . test_dump_createfromdump ( )
2023-09-26 22:23:18 -03:00
self . test_chainless_conflicts ( )
2024-04-22 16:06:29 -04:00
self . test_dump_very_large_records ( )
2019-06-27 09:22:17 -04:00
2018-09-20 21:23:42 -03:00
if __name__ == ' __main__ ' :
2024-07-16 17:05:14 -04:00
ToolWalletTest ( __file__ ) . main ( )