You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
207 lines
9.9 KiB
207 lines
9.9 KiB
#!/usr/bin/env python3 |
|
|
|
description="""This tool is to be used in a case where |
|
a user has a BIP39 seedphrase but has no wallet file and no |
|
backup of imported keys, and they had earlier used SNICKER. |
|
|
|
This will usually not be needed as you should keep a backup |
|
of your *.jmdat joinmarket wallet file, which contains all |
|
this information. |
|
|
|
Before using this tool, you need to do: |
|
`python wallet-tool.py recover` to recover the wallet from |
|
seed, and then: |
|
`bitcoin-cli rescanblockchain ...` |
|
for an appropriate range of blocks in order for Bitcoin Core |
|
to get a record of the transactions that happened with your |
|
HD addresses. |
|
|
|
Then, you can run this script to find all the SNICKER-generated |
|
imported addresses that either did have, or still do have, keys |
|
and have them imported back into the wallet. |
|
(Note that this of course won't find any other non-SNICKER imported |
|
keys, so as a reminder, *always* back up either jmdat wallet files, |
|
or at least, the imported keys themselves.) |
|
""" |
|
|
|
import sys |
|
from optparse import OptionParser |
|
from jmbase import bintohex, EXIT_ARGERROR, jmprint |
|
import jmbitcoin as btc |
|
from jmclient import (add_base_options, load_program_config, |
|
check_regtest, get_wallet_path, open_test_wallet_maybe, |
|
WalletService) |
|
from jmclient.configure import get_log |
|
|
|
log = get_log() |
|
|
|
def get_pubs_and_indices_of_inputs(tx, wallet_service, ours): |
|
""" Returns a list of items (pubkey, index), |
|
one per input at index index, in transaction |
|
tx, spending pubkey pubkey, if the input is ours |
|
if ours is True, else returns the complementary list. |
|
""" |
|
our_ins = [] |
|
not_our_ins = [] |
|
for i in range(len(tx.vin)): |
|
pub, msg = btc.extract_pubkey_from_witness(tx, i) |
|
if not pub: |
|
continue |
|
if not wallet_service.is_known_script( |
|
wallet_service.pubkey_to_script(pub)): |
|
not_our_ins.append((pub, i)) |
|
else: |
|
our_ins.append((pub, i)) |
|
if ours: |
|
return our_ins |
|
else: |
|
return not_our_ins |
|
|
|
def get_pubs_and_indices_of_ancestor_inputs(txin, wallet_service, ours): |
|
""" For a transaction input txin, retrieve the spent transaction, |
|
and iterate over its inputs, returning a list of items |
|
(pubkey, index) all of which belong to us if ours is True, |
|
or else the complementary set. |
|
Note: the ancestor transactions must be in the dict txlist, which is |
|
keyed by txid and values are CTransaction; if not, |
|
an error occurs. This is assumed to be the case because all ancestors |
|
must be either in the set returned by wallet_sync, or else in the set |
|
of SNICKER transactions found so far. |
|
""" |
|
tx = wallet_service.get_transaction(txin.prevout.hash[::-1]) |
|
return get_pubs_and_indices_of_inputs(tx, wallet_service, ours=ours) |
|
|
|
def main(): |
|
parser = OptionParser( |
|
usage= |
|
'usage: %prog [options] walletname', |
|
description=description |
|
) |
|
parser.add_option('-m', '--mixdepth', action='store', type='int', |
|
dest='mixdepth', default=0, |
|
help="mixdepth to source coins from") |
|
parser.add_option('-a', |
|
'--amtmixdepths', |
|
action='store', |
|
type='int', |
|
dest='amtmixdepths', |
|
help='number of mixdepths in wallet, default 5', |
|
default=5) |
|
parser.add_option('-g', |
|
'--gap-limit', |
|
type="int", |
|
action='store', |
|
dest='gaplimit', |
|
help='gap limit for wallet, default=6', |
|
default=6) |
|
add_base_options(parser) |
|
(options, args) = parser.parse_args() |
|
load_program_config(config_path=options.datadir) |
|
check_regtest() |
|
if len(args) != 1: |
|
log.error("Invalid arguments, see --help") |
|
sys.exit(EXIT_ARGERROR) |
|
wallet_name = args[0] |
|
wallet_path = get_wallet_path(wallet_name, None) |
|
max_mix_depth = max([options.mixdepth, options.amtmixdepths - 1]) |
|
wallet = open_test_wallet_maybe( |
|
wallet_path, wallet_name, max_mix_depth, |
|
wallet_password_stdin=options.wallet_password_stdin, |
|
gap_limit=options.gaplimit) |
|
wallet_service = WalletService(wallet) |
|
|
|
# step 1: do a full recovery style sync. this will pick up |
|
# all addresses that we expect to match transactions against, |
|
# from a blank slate Core wallet that originally had no imports. |
|
if not options.recoversync: |
|
jmprint("Recovery sync was not set, but using it anyway.") |
|
while not wallet_service.synced: |
|
wallet_service.sync_wallet(fast=False) |
|
# Note that the user may be interrupted above by the rescan |
|
# request; this is as for normal scripts; after the rescan is done |
|
# (usually, only once, but, this *IS* needed here, unlike a normal |
|
# wallet generation event), we just try again. |
|
|
|
# Now all address from HD are imported, we need to grab |
|
# all the transactions for those addresses; this includes txs |
|
# that *spend* as well as receive our coins, so will include |
|
# "first-out" SNICKER txs as well as ordinary spends and JM coinjoins. |
|
seed_transactions = wallet_service.get_all_transactions() |
|
|
|
# Search for SNICKER txs and add them if they match. |
|
# We proceed recursively; we find all one-out matches, then |
|
# all 2-out matches, until we find no new ones and stop. |
|
|
|
if len(seed_transactions) == 0: |
|
jmprint("No transactions were found for this wallet. Did you rescan?") |
|
return False |
|
|
|
new_txs = [] |
|
current_block_heights = set() |
|
for tx in seed_transactions: |
|
if btc.is_snicker_tx(tx): |
|
jmprint("Found a snicker tx: {}".format(bintohex(tx.GetTxid()[::-1]))) |
|
equal_outs = btc.get_equal_outs(tx) |
|
if not equal_outs: |
|
continue |
|
if all([wallet_service.is_known_script( |
|
x.scriptPubKey) == False for x in [a[1] for a in equal_outs]]): |
|
# it is now *very* likely that one of the two equal |
|
# outputs is our SNICKER custom output |
|
# script; notice that in this case, the transaction *must* |
|
# have spent our inputs, since it didn't recognize ownership |
|
# of either coinjoin output (and if it did recognize the change, |
|
# it would have recognized the cj output also). |
|
# We try to regenerate one of the outputs, but warn if |
|
# we can't. |
|
my_indices = get_pubs_and_indices_of_inputs(tx, wallet_service, ours=True) |
|
for mypub, mi in my_indices: |
|
for eo in equal_outs: |
|
for (other_pub, i) in get_pubs_and_indices_of_inputs(tx, wallet_service, ours=False): |
|
for (our_pub, j) in get_pubs_and_indices_of_ancestor_inputs(tx.vin[mi], wallet_service, ours=True): |
|
our_spk = wallet_service.pubkey_to_script(our_pub) |
|
our_priv = wallet_service.get_key_from_addr( |
|
wallet_service.script_to_addr(our_spk)) |
|
tweak_bytes = btc.ecdh(our_priv[:-1], other_pub) |
|
tweaked_pub = btc.snicker_pubkey_tweak(our_pub, tweak_bytes) |
|
tweaked_spk = wallet_service.pubkey_to_script(tweaked_pub) |
|
if tweaked_spk == eo[1].scriptPubKey: |
|
# TODO wallet.script_to_addr has a dubious assertion, that's why |
|
# we use btc method directly: |
|
address_found = str(btc.CCoinAddress.from_scriptPubKey(btc.CScript(tweaked_spk))) |
|
#address_found = wallet_service.script_to_addr(tweaked_spk) |
|
jmprint("Found a new SNICKER output belonging to us.") |
|
jmprint("Output address {} in the following transaction:".format( |
|
address_found)) |
|
jmprint(btc.human_readable_transaction(tx)) |
|
jmprint("Importing the address into the joinmarket wallet...") |
|
# NB for a recovery we accept putting any imported keys all into |
|
# the same mixdepth (0); TODO investigate correcting this, it will |
|
# be a little complicated. |
|
success, msg = wallet_service.check_tweak_matches_and_import(wallet_service.script_to_addr(our_spk), |
|
tweak_bytes, tweaked_pub, wallet_service.mixdepth) |
|
if not success: |
|
jmprint("Failed to import SNICKER key: {}".format(msg), "error") |
|
return False |
|
else: |
|
jmprint("... success.") |
|
# we want the blockheight to track where the next-round rescan |
|
# must start from |
|
current_block_heights.add(wallet_service.get_transaction_block_height(tx)) |
|
# add this transaction to the next round. |
|
new_txs.append(tx) |
|
if len(new_txs) == 0: |
|
return True |
|
seed_transactions.extend(new_txs) |
|
earliest_new_blockheight = min(current_block_heights) |
|
jmprint("New SNICKER addresses were imported to the Core wallet; " |
|
"do rescanblockchain again, starting from block {}, before " |
|
"restarting this script.".format(earliest_new_blockheight)) |
|
return False |
|
|
|
if __name__ == "__main__": |
|
res = main() |
|
if not res: |
|
jmprint("Script finished, recovery is NOT complete.", level="warning") |
|
else: |
|
jmprint("Script finished, recovery is complete.")
|
|
|