Ejemplo n.º 1
0
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
Ejemplo n.º 2
0
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
Ejemplo n.º 3
0
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()))
Ejemplo n.º 4
0
    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
Ejemplo n.º 5
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)
Ejemplo n.º 6
0
    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()]
Ejemplo n.º 7
0
    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
Ejemplo n.º 8
0
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))
Ejemplo n.º 9
0
    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
Ejemplo n.º 10
0
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