def test_set_nlocktime(mock_bitcoincore): """Test that newly created recovery transactions have nlocktime = current blockheight + 1""" mock_bitcoincore.return_value = AuthServiceProxy('testnet_txs') estimate = { 'blocks': 3, 'feerate': 1, } mock_bitcoincore.return_value.estimatesmartfee.return_value = estimate current_blockheight = 123 mock_bitcoincore.return_value.getblockcount.return_value = current_blockheight destination_address = 'mynHfTyTWyGGB76NBFbfUrTnn8YWQkTJVs' args = [ '--mnemonic-file={}'.format(datafile('mnemonic_6.txt')), '--rpcuser=abc', '--rpcpassword=abc', '2of3', '--recovery-mnemonic-file={}'.format(datafile('mnemonic_7.txt')), '--rescan', '--key-search-depth={}'.format(key_depth), '--search-subaccounts={}'.format(sub_depth), '--destination-address={}'.format(destination_address), ] output = get_output(args).strip() tx = txutil.from_hex(output) assert wally.tx_get_locktime(tx) == current_blockheight
def test_recover_2of3(mock_bitcoincore): """Test 2of3 happy path""" mock_bitcoincore.return_value = AuthServiceProxy('testnet_txs') estimate = {'blocks': 3, 'feerate': 1, } mock_bitcoincore.return_value.estimatesmartfee.return_value = estimate destination_address = 'mynHfTyTWyGGB76NBFbfUrTnn8YWQkTJVs' args = [ '--mnemonic-file={}'.format(datafile('mnemonic_6.txt')), '--rpcuser=abc', '--rpcpassword=abc', '2of3', '--recovery-mnemonic-file={}'.format(datafile('mnemonic_7.txt')), '--rescan', '--key-search-depth={}'.format(key_depth), '--search-subaccounts={}'.format(sub_depth), '--destination-address={}'.format(destination_address), ] # Raw tx output = get_output(args).strip() assert output == open(datafile("signed_2of3_5")).read().strip() # Check replace by fee is set tx = txutil.from_hex(output) assert wally.tx_get_num_inputs(tx) == 1 assert wally.tx_get_input_sequence(tx, 0) == int(32*'1', 2) - 2 # Summary args = ['--show-summary', ] + args output = get_output(args) summary = parse_summary(output) assert len(summary) == 1 assert summary[0]['destination address'] == destination_address
def verify_txs(txs, utxos, expect_witness): txs = [txutil.from_hex(tx) for tx in txs] for tx in txs: assert wally.tx_get_num_inputs(tx) == 1 assert wally.tx_get_num_outputs(tx) >= 1 if expect_witness: assert wally.tx_get_witness_count(tx) == 1 else: assert wally.tx_get_witness_count(tx) == 0 wally.tx_get_total_output_satoshi(tx) # Throws if total overflows assert len(utxos) > 0 for idx, utxo in enumerate(utxos): tx = txs[idx] spending_tx = txutil.from_hex(''.join(utxo.split()))
def __init__(self, txfile): self.tx_by_id = {} self.txout_by_address = {} for line in open(datafile(txfile)).readlines(): tx = txutil.from_hex(line.strip()) self.tx_by_id[txutil.get_txhash_bin(tx)] = tx for i in range(wally.tx_get_num_outputs(tx)): addr = txutil.get_output_address(tx, i, gaconstants.ADDR_VERSIONS_TESTNET) self.txout_by_address[addr] = (tx, i) self.imported = {} # This is something of a workaround because all the existing tests are based on generating # txs where nlocktime was fixed as 0. The code changed to use current blockheight, so by # fudging this value to 0 the existing tests don't notice the difference self.getblockcount.return_value = 0
def fixup_old_nlocktimes(self): """Fixup data from old format nlocktimes files Older nlocktimes files do not contain explicit prevout_signatures, prevout_scripts or prevout_script_types. Detect this and extract them from the raw transaction to make the txdata look consistent to the rest of the code. Note that segwit is not being handled here because old style nlocktimes predate segwit """ for txdata in self.txdata: if 'prevout_signatures' not in txdata: tx = txutil.from_hex(txdata['tx']) txdata['prevout_script_types'] = [] txdata['prevout_signatures'] = [] txdata['prevout_scripts'] = [] for i in range(wally.tx_get_num_inputs(tx)): inp = wally.tx_get_input_script(tx, i) ga_signature = wally.hex_from_bytes(inp[2:inp[1]+2]) redeem_script = wally.hex_from_bytes(inp[-71:]) txdata['prevout_signatures'].append(ga_signature) txdata['prevout_scripts'].append(redeem_script) txdata['prevout_script_types'].append(gaconstants.P2SH_FORTIFIED_OUT)
def get_transactions(self): # Get a list of utxos by scanning the blockchain self.utxos = self.rescan(clargs.args.key_search_depth, clargs.args.search_subaccounts or 0) return [(txutil.from_hex(tx), None) for tx in self.sign_utxos()]
def scan_blockchain(self, keysets): # Blockchain scanning is delegated to core via bitcoinrpc logging.debug("Connecting to bitcoinrpc to scan blockchain") core = bitcoincore.Connection(clargs.args) version = core.getnetworkinfo()["version"] if version >= 170000 and version <= 170100 and clargs.args.ignore_mempool: logging.warning('Mempool transactions are being ignored') # If the node is running version 0.17.0 or 0.17.1 and # the user does not want to scan the mempool, then use # scantxoutset, otherwise fall back to importmulti + listunspent # FIXME: check for format changes in 0.17.2 scanobjects = [] for keyset in keysets: for witness in keyset.witnesses.values(): scanobjects.append('addr({})'.format(witness.address)) # By using the descriptor "addr(<address>)" we do not fully exploit # the potential of output descriptors (we could delegate the HD # derivation to core). However, as long as the RPC will be marked as # experimental, it is better to keep its usage simple. logging.info('Scanning UTXO set for {} derived addresses'.format( len(scanobjects))) all_utxos = core.scantxoutset("start", scanobjects)["unspents"] logging.debug('Unspents: {}'.format(all_utxos)) elif not clargs.args.ignore_mempool: logging.info("Scanning from '{}'".format(clargs.args.scan_from)) logging.warning('This step may take 10 minutes or more') # Need to import our keysets into core so that it will recognise the # utxos we are looking for addresses = [] requests = [] for keyset in keysets: for witness in keyset.witnesses.values(): addresses.append(witness.address) requests.append({ 'scriptPubKey': { "address": witness.address }, 'timestamp': clargs.args.scan_from, 'watchonly': True, }) logging.info('Importing {} derived addresses into bitcoind'.format( len(requests))) result = core.importmulti(requests) expected_result = [{'success': True}] * len(requests) if result != expected_result: logging.warning('Unexpected result from importmulti') logging.warning('Expected: {}'.format(expected_result)) logging.warning('Actual: {}'.format(result)) raise exceptions.ImportMultiError( 'Unexpected result from importmulti') logging.info('Successfully imported {} derived addresses'.format( len(result))) # Scan the blockchain for any utxos with addresses that match the derived keysets logging.info('Getting unspent transactions...') all_utxos = core.listunspent(0, 9999999, addresses) logging.debug('all utxos = {}'.format(all_utxos)) logging.info('There are {} unspent transactions'.format( len(all_utxos))) else: # The flag --ingore-mempool is not intended to ignore the mempool, but just to # make the user aware that `scantxoutset` does not look at mempool transactions. msg = '--ignore-mempool cannot be specified if you run an old version of ' \ 'Bitcoin Core (without scantxoutset)' raise exceptions.BitcoinCoreConnectionError(msg) # Now need to match the returned utxos with the keysets that unlock them # This is a rather unfortunate loop because there is no other way to correlate the # results from listunspent with the requests to importmulti, or infer the order # of the outputs from scantxoutset utxos = [] tx_matches = [(tx['txid'], keyset, witness, tx['vout']) for tx in all_utxos for keyset in keysets for witness in keyset.witnesses.values() if tx['scriptPubKey'] == witness.scriptPubKey] raw_txs = core.batch_([["getrawtransaction", tx[0]] for tx in tx_matches]) dest_address = self.get_destination_address() for txid_match, raw_tx in zip(tx_matches, raw_txs): txid, keyset, witness, txvout = txid_match logging.info( 'Found recoverable transaction, ' 'subaccount={}, pointer={}, txid={}, witness type={}'.format( keyset.subaccount, keyset.pointer, txid, witness.type_)) logging.debug("found raw={}".format(raw_tx)) utxo = UTXO( keyset, witness.type_, txvout, txutil.from_hex(raw_tx), dest_address, ) utxos.append(utxo) return utxos
def expect_feerate(fee_satoshi_byte, args=None, is_segwit=False, amount=None, too_big=False): """Expect the given feerate Callers typically mock estimatesmartfee before calling this """ if args is None: destination_address = 'mynHfTyTWyGGB76NBFbfUrTnn8YWQkTJVs' args = [ '--mnemonic-file={}'.format(datafile('mnemonic_2.txt')), '--rpcuser=abc', '--rpcpassword=abc', '2of3', '--recovery-mnemonic-file={}'.format(datafile('mnemonic_3.txt')), '--rescan', '--search-subaccounts={}'.format(sub_depth), '--destination-address={}'.format(destination_address), '--key-search-depth=150', '--default-feerate={}'.format(default_feerate), ] # Raw tx if too_big: stdout, ofiles = get_output_ex(args) summary = parse_summary(stdout) assert len(summary) == 1 assert summary[0]['total out'] == '0.0 BTC' assert summary[0]['coin value'] == '0.0 BTC' csv = parse_csv(ofiles[garecovery.clargs.DEFAULT_OFILE]) csv = [row for row in csv] assert len(csv) == 1 assert csv[0]['raw tx'] == '** dust **' return output = get_output(args).strip() output_tx = txutil.from_hex(output) assert wally.tx_get_num_outputs(output_tx) == 1 if is_segwit: assert wally.tx_get_witness_count(output_tx) > 0 else: assert wally.tx_get_witness_count(output_tx) == 0 # Calculate the expected fee expected_fee = decimal.Decimal(fee_satoshi_byte * wally.tx_get_vsize(output_tx)) # The amount of our test tx is a well known value if amount is None: amount = decimal.Decimal(111110000) expected_amount = amount - expected_fee actual_amount = wally.tx_get_output_satoshi(output_tx, 0) if expected_amount <= 0: # If expected amount is negative then the fee exceeds the amount # In this case the amount should be small, but not zero assert actual_amount > 0 assert actual_amount < 10 else: # Expect the resultant tx to have a single output with expected amount # Calculating the fee is not exact so allow a tolerance tolerance = decimal.Decimal(0.001) assert actual_amount < (expected_amount * (1 + tolerance)) assert actual_amount > (expected_amount * (1 - tolerance))
def scan_blockchain(self, keysets): # Blockchain scanning is delegated to core via bitcoinrpc logging.debug("Connecting to bitcoinrpc to scan blockchain") core = bitcoincore.Connection(clargs.args) logging.info("Scanning from '{}'".format(clargs.args.scan_from)) logging.warning('This step may take 10 minutes or more') # Need to import our keysets into core so that it will recognise the # utxos we are looking for addresses = [] requests = [] for keyset in keysets: for witness in keyset.witnesses.values(): addresses.append(witness.address) requests.append({ 'scriptPubKey': { "address": witness.address }, 'timestamp': clargs.args.scan_from, 'watchonly': True, }) logging.info('Importing {} derived addresses into bitcoind'.format( len(requests))) result = core.importmulti(requests) expected_result = [{'success': True}] * len(requests) if result != expected_result: logging.warning('Unexpected result from importmulti') logging.warning('Expected: {}'.format(expected_result)) logging.warning('Actual: {}'.format(result)) raise exceptions.ImportMultiError( 'Unexpected result from importmulti') logging.info('Successfully imported {} derived addresses'.format( len(result))) # Scan the blockchain for any utxos with addresses that match the derived keysets logging.info('Getting unspent transactions...') all_utxos = core.listunspent(0, 9999999, addresses) logging.debug('all utxos = {}'.format(all_utxos)) logging.info('There are {} unspent transactions'.format( len(all_utxos))) # Now need to match the returned utxos with the keysets that unlock them # This is a rather unfortunate loop because there is no other way to correlate the # results from listunspent with the requests to importmulti utxos = [] tx_matches = [(tx['txid'], keyset, witness, tx['vout']) for tx in all_utxos for keyset in keysets for witness in keyset.witnesses.values() if tx['scriptPubKey'] == witness.scriptPubKey] raw_txs = core.batch_([["getrawtransaction", tx[0]] for tx in tx_matches]) dest_address = self.get_destination_address() for txid_match, raw_tx in zip(tx_matches, raw_txs): txid, keyset, witness, txvout = txid_match logging.info( 'Found recoverable transaction, ' 'subaccount={}, pointer={}, txid={}, witness type={}'.format( keyset.subaccount, keyset.pointer, txid, witness.type_)) logging.debug("found raw={}".format(raw_tx)) utxo = UTXO( keyset, witness.type_, txvout, txutil.from_hex(raw_tx), dest_address, ) utxos.append(utxo) return utxos
def test_recover_2of2_csv(mock_bitcoincore): """Test 2of2-csv happy path""" mock_bitcoincore.return_value = AuthServiceProxy('testnet_txs') estimate = { 'blocks': 3, 'feerate': decimal.Decimal('0.00001'), } mock_bitcoincore.return_value.estimatesmartfee.return_value = estimate mock_bitcoincore.return_value.getnetworkinfo = mock.Mock( return_value={'version': 190100}) mock_bitcoincore.return_value.getblockcount.return_value = 144 args = [ '--mnemonic-file={}'.format(datafile('mnemonic_1.txt')), '--rpcuser=abc', '--rpcpassword=abc', '2of2-csv', '--network=testnet', '--key-search-depth={}'.format(key_depth), '--search-subaccounts={}'.format(sub_depth), ] # Raw tx output = get_output(args).strip() assert output == open(datafile("signed_2of2_csv_1")).read().strip() tx = txutil.from_hex(output) assert wally.tx_get_num_inputs(tx) == 1 # Summary args = [ '--show-summary', ] + args output = get_output(args) summary = parse_summary(output) assert len(summary) == 1 # Use scantxoutset instead of importmulti + listunspent scantxoutset_result = { 'success': True, 'unspents': [{ 'txid': '0ab5d70ef25a601de455155fdcb8c492d21a9b3063211dc8a969568d9d0fe15b', 'vout': 0, 'scriptPubKey': 'a91458ce12e1773dd078940a9dc855b94c3c9a343b8587', 'desc': 'addr(2N1LnKRLTCWr8H9UdwoREazuFDXHMEgZj9g)#ztm9gzsm', 'amount': 0.001, 'height': 0, }], } mock_bitcoincore.return_value.scantxoutset = mock.Mock( return_value=scantxoutset_result) # output not expired yet mock_bitcoincore.return_value.getblockcount.return_value = 143 args = [ '--mnemonic-file={}'.format(datafile('mnemonic_1.txt')), '--rpcuser=abc', '--rpcpassword=abc', '2of2-csv', '--network=testnet', '--key-search-depth={}'.format(key_depth), '--search-subaccounts={}'.format(sub_depth), '--ignore-mempool', ] # Raw tx raw_tx = get_output(args).strip() assert raw_tx == '' # output expired mock_bitcoincore.return_value.getblockcount.return_value = 144 # Raw tx output = get_output(args).strip() assert output == open(datafile("signed_2of2_csv_1")).read().strip() # Check replace by fee is set tx = txutil.from_hex(output) assert wally.tx_get_num_inputs(tx) == 1 # Summary args = [ '--show-summary', ] + args output = get_output(args) summary = parse_summary(output) assert len(summary) == 1