Ejemplo n.º 1
0
def test_reserveinputs(node_factory, bitcoind, chainparams):
    """
    Reserve inputs is basically the same as txprepare, with the
    slight exception that 'reserveinputs' doesn't keep the
    unsent transaction around
    """
    amount = 1000000
    total_outs = 12
    l1 = node_factory.get_node(feerates=(7500, 7500, 7500, 7500))
    addr = chainparams['example_addr']

    # Add a medley of funds to withdraw later, bech32 + p2sh-p2wpkh
    for i in range(total_outs // 2):
        bitcoind.rpc.sendtoaddress(l1.rpc.newaddr()['bech32'], amount / 10**8)
        bitcoind.rpc.sendtoaddress(
            l1.rpc.newaddr('p2sh-segwit')['p2sh-segwit'], amount / 10**8)

    bitcoind.generate_block(1)
    wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == total_outs)

    utxo_count = 8
    sent = Decimal('0.01') * (utxo_count - 1)
    reserved = l1.rpc.reserveinputs(
        outputs=[{
            addr: Millisatoshi(amount * (utxo_count - 1) * 1000)
        }])
    assert reserved['feerate_per_kw'] == 7500
    psbt = bitcoind.rpc.decodepsbt(reserved['psbt'])
    out_found = False

    assert len(psbt['inputs']) == utxo_count
    outputs = l1.rpc.listfunds()['outputs']
    assert len([x for x in outputs
                if not x['reserved']]) == total_outs - utxo_count
    assert len([x for x in outputs if x['reserved']]) == utxo_count
    total_outs -= utxo_count
    saved_input = psbt['tx']['vin'][0]

    # We should have two outputs
    for vout in psbt['tx']['vout']:
        if vout['scriptPubKey']['addresses'][0] == addr:
            assert vout['value'] == sent
            out_found = True
    assert out_found

    # Do it again, but for too many inputs
    utxo_count = 12 - utxo_count + 1
    sent = Decimal('0.01') * (utxo_count - 1)
    with pytest.raises(RpcError, match=r"Cannot afford transaction"):
        reserved = l1.rpc.reserveinputs(
            outputs=[{
                addr: Millisatoshi(amount * (utxo_count - 1) * 1000)
            }])

    utxo_count -= 1
    sent = Decimal('0.01') * (utxo_count - 1)
    reserved = l1.rpc.reserveinputs(outputs=[{
        addr:
        Millisatoshi(amount * (utxo_count - 1) * 1000)
    }],
                                    feerate='10000perkw')

    assert reserved['feerate_per_kw'] == 10000
    psbt = bitcoind.rpc.decodepsbt(reserved['psbt'])

    assert len(psbt['inputs']) == utxo_count
    outputs = l1.rpc.listfunds()['outputs']
    assert len([x for x in outputs if not x['reserved']
                ]) == total_outs - utxo_count == 0
    assert len([x for x in outputs if x['reserved']]) == 12

    # No more available
    with pytest.raises(RpcError, match=r"Cannot afford transaction"):
        reserved = l1.rpc.reserveinputs(outputs=[{
            addr: Millisatoshi(amount * 1)
        }],
                                        feerate='253perkw')

    # Unreserve three, from different psbts
    unreserve_utxos = [{
        'txid': saved_input['txid'],
        'vout': saved_input['vout'],
        'sequence': saved_input['sequence']
    }, {
        'txid': psbt['tx']['vin'][0]['txid'],
        'vout': psbt['tx']['vin'][0]['vout'],
        'sequence': psbt['tx']['vin'][0]['sequence']
    }, {
        'txid': psbt['tx']['vin'][1]['txid'],
        'vout': psbt['tx']['vin'][1]['vout'],
        'sequence': psbt['tx']['vin'][1]['sequence']
    }]
    unreserve_psbt = bitcoind.rpc.createpsbt(unreserve_utxos, [])

    unreserved = l1.rpc.unreserveinputs(unreserve_psbt)
    assert all([x['unreserved'] for x in unreserved['outputs']])
    outputs = l1.rpc.listfunds()['outputs']
    assert len([x for x in outputs
                if not x['reserved']]) == len(unreserved['outputs'])
    for i in range(len(unreserved['outputs'])):
        un = unreserved['outputs'][i]
        u_utxo = unreserve_utxos[i]
        assert un['txid'] == u_utxo['txid'] and un['vout'] == u_utxo[
            'vout'] and un['unreserved']

    # Try unreserving the same utxos again, plus one that's not included
    # We expect this to be a no-op.
    unreserve_utxos.append({'txid': 'b' * 64, 'vout': 0, 'sequence': 0})
    unreserve_psbt = bitcoind.rpc.createpsbt(unreserve_utxos, [])
    unreserved = l1.rpc.unreserveinputs(unreserve_psbt)
    assert not any([x['unreserved'] for x in unreserved['outputs']])
    for un in unreserved['outputs']:
        assert not un['unreserved']
    assert len([x for x in l1.rpc.listfunds()['outputs']
                if not x['reserved']]) == 3

    # passing in an empty string should fail
    with pytest.raises(RpcError, match=r"should be a PSBT, not "):
        l1.rpc.unreserveinputs('')

    # reserve one of the utxos that we just unreserved
    utxos = []
    utxos.append(saved_input['txid'] + ":" + str(saved_input['vout']))
    reserved = l1.rpc.reserveinputs([{
        addr: Millisatoshi(amount * 0.5 * 1000)
    }],
                                    feerate='253perkw',
                                    utxos=utxos)
    assert len([x for x in l1.rpc.listfunds()['outputs']
                if not x['reserved']]) == 2
    psbt = bitcoind.rpc.decodepsbt(reserved['psbt'])
    assert len(psbt['inputs']) == 1
    vin = psbt['tx']['vin'][0]
    assert vin['txid'] == saved_input['txid'] and vin['vout'] == saved_input[
        'vout']

    # reserve them all!
    reserved = l1.rpc.reserveinputs([{addr: 'all'}])
    outputs = l1.rpc.listfunds()['outputs']
    assert len([x for x in outputs if not x['reserved']]) == 0
    assert len([x for x in outputs if x['reserved']]) == 12

    # FIXME: restart the node, nothing will remain reserved
    l1.restart()
    assert len(l1.rpc.listfunds()['outputs']) == 12
Ejemplo n.º 2
0
def execute(payload: dict):
    peer_id = peer_from_scid(plugin, payload)
    get_channel(plugin, payload, peer_id)  # ensures or raises error
    test_or_set_chunks(plugin, payload)
    plugin.log("%s  %s  %d%%  %d chunks" % (payload['command'], payload['scid'], payload['percentage'], payload['chunks']))

    # iterate of chunks, default just one
    for chunk in range(payload['chunks']):
        # we discover remaining capacities for each chunk,
        # as fees from previous chunks affect reserves
        spendable, receivable = spendable_from_scid(plugin, payload)
        total = spendable + receivable
        amount = Millisatoshi(int(int(total) * (0.01 * payload['percentage'] / payload['chunks'])))
        if amount == Millisatoshi(0):
            raise RpcError(payload['command'], payload, {'message': 'Cannot process chunk. Amount would be 0msat.'})

        # if capacity exceeds, limit amount to full or empty channel
        if payload['command'] == "drain" and amount > spendable:
            amount = spendable
        if payload['command'] == "fill" and amount > receivable:
            amount = receivable

        result = False
        try:
            # we need to try with different HTLC_FEE values
            # until we dont get capacity error on first hop
            htlc_fee = HTLC_FEE_NUL
            htlc_stp = HTLC_FEE_STP

            while htlc_fee < HTLC_FEE_MAX and result is False:
                # When getting close to 100% we need to account for HTLC commitment fee
                if payload['command'] == 'drain' and spendable - amount <= htlc_fee:
                    if amount < htlc_fee:
                        raise RpcError(payload['command'], payload, {'message': 'channel too low to cover fees'})
                    amount -= htlc_fee
                plugin.log("Trying... chunk:%s/%s  spendable:%s  receivable:%s  htlc_fee:%s =>  amount:%s" % (chunk + 1, payload['chunks'], spendable, receivable, htlc_fee, amount))

                try:
                    result = try_for_htlc_fee(plugin, payload, peer_id, amount, chunk, spendable)
                except Exception as err:
                    if "htlc_fee unknown" in str(err):
                        if htlc_fee == HTLC_FEE_NUL:
                            htlc_fee = HTLC_FEE_MIN - HTLC_FEE_STP
                        htlc_fee += htlc_stp
                        htlc_stp *= 1.1  # exponential increase steps
                        plugin.log("Retrying with additional HTLC onchain fees: %s" % htlc_fee)
                        continue
                    if "htlc_fee is" in str(err):
                        htlc_fee = Millisatoshi(str(err)[12:])
                        plugin.log("Retrying with exact HTLC onchain fees: %s" % htlc_fee)
                        continue
                    raise err

            # If result is still false, we tried allowed htlc_fee range unsuccessfully
            if result is False:
                raise RpcError(payload['command'], payload, {'message': 'Cannot determine required htlc commitment fees.'})

        except Exception as e:
            return cleanup(plugin, payload, e)

    return cleanup(plugin, payload)
Ejemplo n.º 3
0
def test_txprepare(node_factory, bitcoind, chainparams):
    amount = 1000000
    l1 = node_factory.get_node(random_hsm=True)
    addr = chainparams['example_addr']

    # Add some funds to withdraw later: both bech32 and p2sh
    for i in range(5):
        bitcoind.rpc.sendtoaddress(l1.rpc.newaddr()['bech32'], amount / 10**8)
        bitcoind.rpc.sendtoaddress(
            l1.rpc.newaddr('p2sh-segwit')['p2sh-segwit'], amount / 10**8)

    bitcoind.generate_block(1)
    wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 10)

    prep = l1.rpc.txprepare(outputs=[{addr: Millisatoshi(amount * 3 * 1000)}])
    decode = bitcoind.rpc.decoderawtransaction(prep['unsigned_tx'])
    assert decode['txid'] == prep['txid']
    # 4 inputs, 2 outputs (3 if we have a fee output).
    assert len(decode['vin']) == 4
    assert len(decode['vout']) == 2 if not chainparams['feeoutput'] else 3

    # One output will be correct.
    outnum = [
        i for i, o in enumerate(decode['vout'])
        if o['value'] == Decimal(amount * 3) / 10**8
    ][0]

    for i, o in enumerate(decode['vout']):
        if i == outnum:
            assert o['scriptPubKey']['type'] == 'witness_v0_keyhash'
            assert o['scriptPubKey']['addresses'] == [addr]
        else:
            assert o['scriptPubKey']['type'] in ['witness_v0_keyhash', 'fee']

    # Now prepare one with no change.
    prep2 = l1.rpc.txprepare([{addr: 'all'}])
    decode = bitcoind.rpc.decoderawtransaction(prep2['unsigned_tx'])
    assert decode['txid'] == prep2['txid']
    # 6 inputs, 1 outputs.
    assert len(decode['vin']) == 6
    assert len(decode['vout']) == 1 if not chainparams['feeoutput'] else 2

    # Some fees will be paid.
    assert decode['vout'][0]['value'] < Decimal(amount * 6) / 10**8
    assert decode['vout'][0]['value'] > Decimal(
        amount * 6) / 10**8 - Decimal(0.0002)
    assert decode['vout'][0]['scriptPubKey']['type'] == 'witness_v0_keyhash'
    assert decode['vout'][0]['scriptPubKey']['addresses'] == [addr]

    # If I cancel the first one, I can get those first 4 outputs.
    discard = l1.rpc.txdiscard(prep['txid'])
    assert discard['txid'] == prep['txid']
    assert discard['unsigned_tx'] == prep['unsigned_tx']

    prep3 = l1.rpc.txprepare([{addr: 'all'}])
    decode = bitcoind.rpc.decoderawtransaction(prep3['unsigned_tx'])
    assert decode['txid'] == prep3['txid']
    # 4 inputs, 1 outputs.
    assert len(decode['vin']) == 4
    assert len(decode['vout']) == 1 if not chainparams['feeoutput'] else 2

    # Some fees will be taken
    assert decode['vout'][0]['value'] < Decimal(amount * 4) / 10**8
    assert decode['vout'][0]['value'] > Decimal(
        amount * 4) / 10**8 - Decimal(0.0002)
    assert decode['vout'][0]['scriptPubKey']['type'] == 'witness_v0_keyhash'
    assert decode['vout'][0]['scriptPubKey']['addresses'] == [addr]

    # Cannot discard twice.
    with pytest.raises(RpcError, match=r'not an unreleased txid'):
        l1.rpc.txdiscard(prep['txid'])

    # Discard everything, we should now spend all inputs.
    l1.rpc.txdiscard(prep2['txid'])
    l1.rpc.txdiscard(prep3['txid'])
    prep4 = l1.rpc.txprepare([{addr: 'all'}])
    decode = bitcoind.rpc.decoderawtransaction(prep4['unsigned_tx'])
    assert decode['txid'] == prep4['txid']
    # 10 inputs, 1 outputs.
    assert len(decode['vin']) == 10
    assert len(decode['vout']) == 1 if not chainparams['feeoutput'] else 2

    # Some fees will be taken
    assert decode['vout'][0]['value'] < Decimal(amount * 10) / 10**8
    assert decode['vout'][0]['value'] > Decimal(
        amount * 10) / 10**8 - Decimal(0.0003)
    assert decode['vout'][0]['scriptPubKey']['type'] == 'witness_v0_keyhash'
    assert decode['vout'][0]['scriptPubKey']['addresses'] == [addr]
    l1.rpc.txdiscard(prep4['txid'])

    # Try passing in a utxo set
    utxos = [
        utxo["txid"] + ":" + str(utxo["output"])
        for utxo in l1.rpc.listfunds()["outputs"]
    ][:4]
    prep5 = l1.rpc.txprepare([{
        addr: Millisatoshi(amount * 3.5 * 1000)
    }],
                             utxos=utxos)

    # Try passing unconfirmed utxos
    unconfirmed_utxo = l1.rpc.withdraw(l1.rpc.newaddr()["bech32"], 10**5)
    uutxos = [unconfirmed_utxo["txid"] + ":0"]
    with pytest.raises(RpcError,
                       match=r"Cannot afford transaction .* use "
                       "confirmed utxos."):
        l1.rpc.txprepare([{
            addr: Millisatoshi(amount * 3.5 * 1000)
        }],
                         utxos=uutxos)

    decode = bitcoind.rpc.decoderawtransaction(prep5['unsigned_tx'])
    assert decode['txid'] == prep5['txid']

    # Check that correct utxos are included
    assert len(decode['vin']) == 4
    vins = ["{}:{}".format(v['txid'], v['vout']) for v in decode['vin']]
    for utxo in utxos:
        assert utxo in vins

    # We should have a change output, so this is exact
    assert len(decode['vout']) == 3 if chainparams['feeoutput'] else 2
    assert decode['vout'][1]['value'] == Decimal(amount * 3.5) / 10**8
    assert decode['vout'][1]['scriptPubKey']['type'] == 'witness_v0_keyhash'
    assert decode['vout'][1]['scriptPubKey']['addresses'] == [addr]

    # Discard prep4 and get all funds again
    l1.rpc.txdiscard(prep5['txid'])
    with pytest.raises(
            RpcError,
            match=
            r'this destination wants all satoshi. The count of outputs can\'t be more than 1'
    ):
        prep5 = l1.rpc.txprepare([{
            addr: Millisatoshi(amount * 3 * 1000)
        }, {
            addr: 'all'
        }])
    prep5 = l1.rpc.txprepare([{
        addr: Millisatoshi(amount * 3 * 500 + 100000)
    }, {
        addr: Millisatoshi(amount * 3 * 500 - 100000)
    }])
    decode = bitcoind.rpc.decoderawtransaction(prep5['unsigned_tx'])
    assert decode['txid'] == prep5['txid']
    # 4 inputs, 3 outputs(include change).
    assert len(decode['vin']) == 4
    assert len(decode['vout']) == 4 if chainparams['feeoutput'] else 3

    # One output will be correct.
    for i in range(3 + chainparams['feeoutput']):
        if decode['vout'][i - 1]['value'] == Decimal('0.01500100'):
            outnum1 = i - 1
        elif decode['vout'][i - 1]['value'] == Decimal('0.01499900'):
            outnum2 = i - 1
        else:
            changenum = i - 1

    assert decode['vout'][outnum1]['scriptPubKey'][
        'type'] == 'witness_v0_keyhash'
    assert decode['vout'][outnum1]['scriptPubKey']['addresses'] == [addr]

    assert decode['vout'][outnum2]['scriptPubKey'][
        'type'] == 'witness_v0_keyhash'
    assert decode['vout'][outnum2]['scriptPubKey']['addresses'] == [addr]

    assert decode['vout'][changenum]['scriptPubKey'][
        'type'] == 'witness_v0_keyhash'
Ejemplo n.º 4
0
def summary(plugin, exclude=''):
    """Gets summary information about this node."""

    reply = {}
    info = plugin.rpc.getinfo()
    funds = plugin.rpc.listfunds()
    peers = plugin.rpc.listpeers()

    # Make it stand out if we're not on mainnet.
    if info['network'] != 'bitcoin':
        reply['network'] = info['network'].upper()

    if hasattr(plugin, 'my_address') and plugin.my_address:
        reply['my_address'] = plugin.my_address
    else:
        reply['warning_no_address'] = "NO PUBLIC ADDRESSES"

    utxos = [
        int(f['amount_msat']) for f in funds['outputs']
        if f['status'] == 'confirmed'
    ]
    reply['num_utxos'] = len(utxos)
    utxo_amount = Millisatoshi(sum(utxos))
    reply['utxo_amount'] = utxo_amount.to_btc_str()

    avail_out = Millisatoshi(0)
    avail_in = Millisatoshi(0)
    chans = []
    reply['num_channels'] = 0
    reply['num_connected'] = 0
    reply['num_gossipers'] = 0
    for p in peers['peers']:
        pid = p['id']
        addpeer(plugin, p)
        active_channel = False
        for c in p['channels']:
            if c['state'] != 'CHANNELD_NORMAL':
                continue
            active_channel = True
            if c['short_channel_id'] in exclude:
                continue
            if p['connected']:
                reply['num_connected'] += 1
            if c['our_reserve_msat'] < c['to_us_msat']:
                to_us = c['to_us_msat'] - c['our_reserve_msat']
            else:
                to_us = Millisatoshi(0)
            avail_out += to_us

            # We have to derive amount to them
            to_them = c['total_msat'] - c['to_us_msat']
            if c['their_reserve_msat'] < to_them:
                to_them = to_them - c['their_reserve_msat']
            else:
                to_them = Millisatoshi(0)
            avail_in += to_them
            reply['num_channels'] += 1
            chans.append(
                Channel(
                    c['total_msat'],
                    to_us,
                    to_them,
                    pid,
                    c['private'],
                    p['connected'],
                    c['short_channel_id'],
                    plugin.persist['peerstate'][pid]['avail'],
                    c['fee_base_msat'],
                    c['fee_proportional_millionths'],
                ))

        if not active_channel and p['connected']:
            reply['num_gossipers'] += 1

    reply['avail_out'] = avail_out.to_btc_str()
    reply['avail_in'] = avail_in.to_btc_str()
    reply['fees_collected'] = info['fees_collected_msat'].to_btc_str()

    if plugin.fiat_per_btc > 0:
        reply['utxo_amount'] += ' ({})'.format(to_fiatstr(utxo_amount))
        reply['avail_out'] += ' ({})'.format(to_fiatstr(avail_out))
        reply['avail_in'] += ' ({})'.format(to_fiatstr(avail_in))

    if chans != []:
        reply['channels_flags'] = 'P:private O:offline'
        reply['channels'] = ["\n"]
        biggest = max(max(int(c.ours), int(c.theirs)) for c in chans)
        append_header(reply['channels'], biggest)
        for c in chans:
            # Create simple line graph, 47 chars wide.
            our_len = int(round(int(c.ours) / biggest * 23))
            their_len = int(round(int(c.theirs) / biggest * 23))

            # We put midpoint in the middle.
            mid = draw.mid
            if our_len == 0:
                left = "{:>23}".format('')
                mid = draw.double_left
            else:
                left = "{:>23}".format(draw.left + draw.bar * (our_len - 1))

            if their_len == 0:
                right = "{:23}".format('')
                # Both 0 is a special case.
                if our_len == 0:
                    mid = draw.empty
                else:
                    mid = draw.double_right
            else:
                right = "{:23}".format(draw.bar * (their_len - 1) + draw.right)

            s = left + mid + right

            # output short channel id, so things can be copyNpasted easily
            s += " {:14} ".format(c.scid)

            extra = ''
            if c.private:
                extra += 'P'
            else:
                extra += '_'
            if not c.connected:
                extra += 'O'
            else:
                extra += '_'
            s += '[{}] '.format(extra)

            # append fees
            s += ' {:5}'.format(c.base.millisatoshis)
            s += ' {:6}  '.format(c.permil)

            # append 24hr availability
            s += '{:4.0%}  '.format(c.avail)

            # append alias or id
            node = plugin.rpc.listnodes(c.pid)['nodes']
            if len(node) != 0 and 'alias' in node[0]:
                s += node[0]['alias']
            else:
                s += c.pid[0:32]
            reply['channels'].append(s)

    # Make modern lightning-cli format this human-readble by default!
    reply['format-hint'] = 'simple'
    return reply
Ejemplo n.º 5
0
#!/usr/bin/env python3
from pyln.client import Plugin, Millisatoshi, RpcError
from utils import get_ours, wait_ours
import re
import time
import uuid

plugin = Plugin()


# When draining 100% we must account (not pay) for an additional HTLC fee.
# Currently there is no way of getting the exact number before the fact,
# so we try and error until it is high enough, or take the exception text.
HTLC_FEE_NUL = Millisatoshi('0sat')
HTLC_FEE_STP = Millisatoshi('10sat')
HTLC_FEE_MIN = Millisatoshi('100sat')
HTLC_FEE_MAX = Millisatoshi('100000sat')
HTLC_FEE_EST = Millisatoshi('3000sat')
HTLC_FEE_PAT = re.compile("^.* HTLC fee: ([0-9]+sat).*$")


def setup_routing_fees(plugin, payload, route, amount, substractfees: bool = False):
    delay = int(plugin.get_option('cltv-final'))

    amount_iter = amount
    for r in reversed(route):
        r['msatoshi'] = amount_iter.millisatoshis
        r['amount_msat'] = amount_iter
        r['delay'] = delay
        channels = plugin.rpc.listchannels(r['channel'])
        ch = next(c for c in channels.get('channels') if c['destination'] == r['id'])
Ejemplo n.º 6
0
def rebalance(plugin,
              outgoing_scid,
              incoming_scid,
              msatoshi: Millisatoshi = None,
              retry_for: int = 60,
              maxfeepercent: float = 0.5,
              exemptfee: Millisatoshi = Millisatoshi(5000),
              getroute_method=None):
    """Rebalancing channel liquidity with circular payments.

    This tool helps to move some msatoshis between your channels.
    """
    if msatoshi:
        msatoshi = Millisatoshi(msatoshi)
    retry_for = int(retry_for)
    maxfeepercent = float(maxfeepercent)
    if getroute_method is None:
        getroute = plugin.getroute
    else:
        getroute = getroute_switch(getroute_method)
    exemptfee = Millisatoshi(exemptfee)
    payload = {
        "outgoing_scid": outgoing_scid,
        "incoming_scid": incoming_scid,
        "msatoshi": msatoshi,
        "retry_for": retry_for,
        "maxfeepercent": maxfeepercent,
        "exemptfee": exemptfee
    }
    my_node_id = plugin.rpc.getinfo().get('id')
    outgoing_node_id = peer_from_scid(plugin, outgoing_scid, my_node_id,
                                      payload)
    incoming_node_id = peer_from_scid(plugin, incoming_scid, my_node_id,
                                      payload)
    get_channel(plugin, payload, outgoing_node_id, outgoing_scid, True)
    get_channel(plugin, payload, incoming_node_id, incoming_scid, True)
    out_ours, out_total = amounts_from_scid(plugin, outgoing_scid)
    in_ours, in_total = amounts_from_scid(plugin, incoming_scid)

    # If amount was not given, calculate a suitable 50/50 rebalance amount
    if msatoshi is None:
        msatoshi = calc_optimal_amount(out_ours, out_total, in_ours, in_total,
                                       payload)
        plugin.log("Estimating optimal amount %s" % msatoshi)

    # Check requested amounts are selected channels
    if msatoshi > out_ours or msatoshi > in_total - in_ours:
        raise RpcError("rebalance", payload,
                       {'message': 'Channel capacities too low'})

    plugin.log(
        f"starting rebalance out_scid:{outgoing_scid} in_scid:{incoming_scid} amount:{msatoshi}",
        'debug')

    route_out = {
        'id': outgoing_node_id,
        'channel': outgoing_scid,
        'direction': int(not my_node_id < outgoing_node_id)
    }
    route_in = {
        'id': my_node_id,
        'channel': incoming_scid,
        'direction': int(not incoming_node_id < my_node_id)
    }
    start_ts = int(time.time())
    label = "Rebalance-" + str(uuid.uuid4())
    description = "%s to %s" % (outgoing_scid, incoming_scid)
    invoice = plugin.rpc.invoice(msatoshi, label, description, retry_for + 60)
    payment_hash = invoice['payment_hash']
    # The requirement for payment_secret coincided with its addition to the invoice output.
    payment_secret = invoice.get('payment_secret')

    rpc_result = None
    excludes = [my_node_id]  # excude all own channels to prevent shortcuts
    nodes = {}  # here we store erring node counts
    plugin.maxhopidx = 1  # start with short routes and increase
    plugin.msatfactoridx = plugin.msatfactor  # start with high capacity factor
    # and decrease to reduce WIRE_TEMPORARY failures because of imbalances

    # 'disable' maxhops filter if set to <= 0
    # I know this is ugly, but we don't ruin the rest of the code this way
    if plugin.maxhops <= 0:
        plugin.maxhopidx = 20

    # trace stats
    count = 0
    count_sendpay = 0
    time_getroute = 0
    time_sendpay = 0

    try:
        while int(time.time()
                  ) - start_ts < retry_for and not plugin.rebalance_stop:
            count += 1
            try:
                time_start = time.time()
                r = getroute(plugin,
                             targetid=incoming_node_id,
                             fromid=outgoing_node_id,
                             excludes=excludes,
                             msatoshi=msatoshi)
                time_getroute += time.time() - time_start
            except NoRouteException:
                # no more chance for a successful getroute
                rpc_result = {
                    'status': 'error',
                    'message': 'No suitable routes found'
                }
                return cleanup(plugin, label, payload, rpc_result)
            except RpcError as e:
                # getroute can be successful next time with different parameters
                if e.method == "getroute" and e.error.get('code') == 205:
                    continue
                else:
                    raise e

            route_mid = r['route']
            route = [route_out] + route_mid + [route_in]
            setup_routing_fees(plugin, route, msatoshi)
            fees = route[0]['amount_msat'] - msatoshi

            # check fee and exclude worst channel the next time
            # NOTE: the int(msat) casts are just a workaround for outdated pylightning versions
            if fees > exemptfee and int(
                    fees) > int(msatoshi) * maxfeepercent / 100:
                worst_channel = find_worst_channel(route)
                if worst_channel is None:
                    raise RpcError("rebalance", payload,
                                   {'message': 'Insufficient fee'})
                excludes.append(worst_channel['channel'] + '/' +
                                str(worst_channel['direction']))
                continue

            rpc_result = {
                "sent":
                msatoshi + fees,
                "received":
                msatoshi,
                "fee":
                fees,
                "hops":
                len(route),
                "outgoing_scid":
                outgoing_scid,
                "incoming_scid":
                incoming_scid,
                "status":
                "complete",
                "message":
                f"{msatoshi + fees} sent over {len(route)} hops to rebalance {msatoshi}",
            }
            midroute_str = reduce(
                lambda x, y: x + " -> " + y,
                map(lambda r: get_node_alias(r['id']), route_mid))
            full_route_str = "%s -> %s -> %s -> %s" % (
                get_node_alias(my_node_id), get_node_alias(outgoing_node_id),
                midroute_str, get_node_alias(my_node_id))
            plugin.log("%d hops and %s fees for %s along route: %s" %
                       (len(route), fees.to_satoshi_str(),
                        msatoshi.to_satoshi_str(), full_route_str))
            for r in route:
                plugin.log(
                    "    - %s  %14s  %s" %
                    (r['id'], r['channel'], r['amount_msat']), 'debug')

            time_start = time.time()
            count_sendpay += 1
            try:
                plugin.rpc.sendpay(route,
                                   payment_hash,
                                   payment_secret=payment_secret)
                running_for = int(time.time()) - start_ts
                result = plugin.rpc.waitsendpay(
                    payment_hash, max(retry_for - running_for, 0))
                time_sendpay += time.time() - time_start
                if result.get('status') == "complete":
                    rpc_result[
                        "stats"] = f"running_for:{int(time.time()) - start_ts}  count_getroute:{count}  time_getroute:{time_getroute}  time_getroute_avg:{time_getroute / count}  count_sendpay:{count_sendpay}  time_sendpay:{time_sendpay}  time_sendpay_avg:{time_sendpay / count_sendpay}"
                    return cleanup(plugin, label, payload, rpc_result)

            except RpcError as e:
                time_sendpay += time.time() - time_start
                plugin.log(
                    f"maxhops:{plugin.maxhopidx}  msatfactor:{plugin.msatfactoridx}  running_for:{int(time.time()) - start_ts}  count_getroute:{count}  time_getroute:{time_getroute}  time_getroute_avg:{time_getroute / count}  count_sendpay:{count_sendpay}  time_sendpay:{time_sendpay}  time_sendpay_avg:{time_sendpay / count_sendpay}",
                    'debug')
                # plugin.log(f"RpcError: {str(e)}", 'debug')
                # check if we ran into the `rpc.waitsendpay` timeout
                if e.method == "waitsendpay" and e.error.get('code') == 200:
                    raise RpcError("rebalance", payload,
                                   {'message': 'Timeout reached'})
                # check if we have problems with our own channels
                erring_node = e.error.get('data', {}).get('erring_node')
                erring_channel = e.error.get('data', {}).get('erring_channel')
                erring_direction = e.error.get('data',
                                               {}).get('erring_direction')
                if erring_channel == incoming_scid:
                    raise RpcError("rebalance", payload,
                                   {'message': 'Error with incoming channel'})
                if erring_channel == outgoing_scid:
                    raise RpcError("rebalance", payload,
                                   {'message': 'Error with outgoing channel'})
                # exclude other erroring channels
                if erring_channel is not None and erring_direction is not None:
                    excludes.append(erring_channel + '/' +
                                    str(erring_direction))
                # count and exclude nodes that produce a lot of errors
                if erring_node and plugin.erringnodes > 0:
                    if nodes.get(erring_node) is None:
                        nodes[erring_node] = 0
                    nodes[erring_node] += 1
                    if nodes[erring_node] >= plugin.erringnodes:
                        excludes.append(erring_node)

    except Exception as e:
        return cleanup(plugin, label, payload, rpc_result, e)
    rpc_result = {'status': 'error', 'message': 'Timeout reached'}
    return cleanup(plugin, label, payload, rpc_result)
Ejemplo n.º 7
0
def amounts_from_scid(plugin, scid):
    channels = plugin.rpc.listfunds().get('channels')
    channel = next(c for c in channels if c.get('short_channel_id') == scid)
    our_msat = Millisatoshi(channel['our_amount_msat'])
    total_msat = Millisatoshi(channel['amount_msat'])
    return our_msat, total_msat
Ejemplo n.º 8
0
def test_funder_options(node_factory, bitcoind):
    l1, l2, l3 = node_factory.get_nodes(3)
    l1.fundwallet(10**7)

    # Check the default options
    funder_opts = l1.rpc.call('funderupdate')

    assert funder_opts['policy'] == 'fixed'
    assert funder_opts['policy_mod'] == 0
    assert funder_opts['min_their_funding_msat'] == Millisatoshi('10000000msat')
    assert funder_opts['max_their_funding_msat'] == Millisatoshi('4294967295000msat')
    assert funder_opts['per_channel_min_msat'] == Millisatoshi('10000000msat')
    assert funder_opts['per_channel_max_msat'] == Millisatoshi('4294967295000msat')
    assert funder_opts['reserve_tank_msat'] == Millisatoshi('0msat')
    assert funder_opts['fuzz_percent'] == 0
    assert funder_opts['fund_probability'] == 100
    assert funder_opts['leases_only']

    # l2 funds a chanenl with us. We don't contribute
    l2.rpc.connect(l1.info['id'], 'localhost', l1.port)
    l2.fundchannel(l1, 10**6)
    chan_info = only_one(only_one(l2.rpc.listpeers(l1.info['id'])['peers'])['channels'])
    # l1 contributed nothing
    assert chan_info['funding']['remote_msat'] == Millisatoshi('0msat')
    assert chan_info['funding']['local_msat'] != Millisatoshi('0msat')

    # Change all the options
    funder_opts = l1.rpc.call('funderupdate',
                              {'policy': 'available',
                               'policy_mod': 100,
                               'min_their_funding_msat': '100000msat',
                               'max_their_funding_msat': '2000000000msat',
                               'per_channel_min_msat': '8000000msat',
                               'per_channel_max_msat': '10000000000msat',
                               'reserve_tank_msat': '3000000msat',
                               'fund_probability': 99,
                               'fuzz_percent': 0,
                               'leases_only': False})

    assert funder_opts['policy'] == 'available'
    assert funder_opts['policy_mod'] == 100
    assert funder_opts['min_their_funding_msat'] == Millisatoshi('100000msat')
    assert funder_opts['max_their_funding_msat'] == Millisatoshi('2000000000msat')
    assert funder_opts['per_channel_min_msat'] == Millisatoshi('8000000msat')
    assert funder_opts['per_channel_max_msat'] == Millisatoshi('10000000000msat')
    assert funder_opts['reserve_tank_msat'] == Millisatoshi('3000000msat')
    assert funder_opts['fuzz_percent'] == 0
    assert funder_opts['fund_probability'] == 99

    # Set the fund probability back up to 100.
    funder_opts = l1.rpc.call('funderupdate',
                              {'fund_probability': 100})
    l3.rpc.connect(l1.info['id'], 'localhost', l1.port)
    l3.fundchannel(l1, 10**6)
    chan_info = only_one(only_one(l3.rpc.listpeers(l1.info['id'])['peers'])['channels'])
    # l1 contributed all its funds!
    assert chan_info['funding']['remote_msat'] == Millisatoshi('9994255000msat')
    assert chan_info['funding']['local_msat'] == Millisatoshi('1000000000msat')
Ejemplo n.º 9
0
def test_v2_rbf_liquidity_ad(node_factory, bitcoind, chainparams):

    opts = {'funder-policy': 'match', 'funder-policy-mod': 100,
            'lease-fee-base-msat': '100sat', 'lease-fee-basis': 100,
            'may_reconnect': True}
    l1, l2 = node_factory.get_nodes(2, opts=opts)

    # what happens when we RBF?
    feerate = 2000
    amount = 500000
    l1.fundwallet(20000000)
    l2.fundwallet(20000000)

    # l1 leases a channel from l2
    l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
    rates = l1.rpc.dev_queryrates(l2.info['id'], amount, amount)
    l1.daemon.wait_for_log('disconnect')
    l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
    chan_id = l1.rpc.fundchannel(l2.info['id'], amount, request_amt=amount,
                                 feerate='{}perkw'.format(feerate),
                                 compact_lease=rates['compact_lease'])['channel_id']

    vins = [x for x in l1.rpc.listfunds()['outputs'] if x['reserved']]
    assert only_one(vins)
    prev_utxos = ["{}:{}".format(vins[0]['txid'], vins[0]['output'])]

    # Check that we're waiting for lockin
    l1.daemon.wait_for_log(' to DUALOPEND_AWAITING_LOCKIN')

    est_fees = calc_lease_fee(amount, feerate, rates)

    # This should be the accepter's amount
    fundings = only_one(only_one(l1.rpc.listpeers()['peers'])['channels'])['funding']
    assert Millisatoshi(est_fees + amount * 1000) == Millisatoshi(fundings['remote_msat'])

    # rbf the lease with a higher amount
    rate = int(find_next_feerate(l1, l2)[:-5])
    # We 4x the feerate to beat the min-relay fee
    next_feerate = '{}perkw'.format(rate * 4)

    # Initiate an RBF
    startweight = 42 + 172  # base weight, funding output
    initpsbt = l1.rpc.utxopsbt(amount, next_feerate, startweight,
                               prev_utxos, reservedok=True,
                               min_witness_weight=110,
                               excess_as_change=True)['psbt']

    # do the bump
    bump = l1.rpc.openchannel_bump(chan_id, amount, initpsbt,
                                   funding_feerate=next_feerate)
    update = l1.rpc.openchannel_update(chan_id, bump['psbt'])
    assert update['commitments_secured']
    # Sign our inputs, and continue
    signed_psbt = l1.rpc.signpsbt(update['psbt'])['signed_psbt']
    l1.rpc.openchannel_signed(chan_id, signed_psbt)

    # what happens when the channel opens?
    bitcoind.generate_block(6)
    l1.daemon.wait_for_log('to CHANNELD_NORMAL')

    # This should be the accepter's amount
    fundings = only_one(only_one(l1.rpc.listpeers()['peers'])['channels'])['funding']
    # FIXME: The lease goes away :(
    assert Millisatoshi(0) == Millisatoshi(fundings['remote_msat'])

    wait_for(lambda: [c['active'] for c in l1.rpc.listchannels(l1.get_channel_scid(l2))['channels']] == [True, True])

    # send some payments, mine a block or two
    inv = l2.rpc.invoice(10**4, '1', 'no_1')
    l1.rpc.pay(inv['bolt11'])

    # l2 attempts to close a channel that it leased, should succeed
    # (channel isnt leased)
    l2.rpc.close(l1.get_channel_scid(l2))
    l1.daemon.wait_for_log('State changed from CLOSINGD_SIGEXCHANGE to CLOSINGD_COMPLETE')
Ejemplo n.º 10
0
def test_sign_and_send_psbt(node_factory, bitcoind, chainparams):
    """
    Tests for the sign + send psbt RPCs
    """
    amount = 1000000
    total_outs = 12
    coin_mvt_plugin = os.path.join(os.getcwd(), 'tests/plugins/coin_movements.py')
    l1 = node_factory.get_node(options={'plugin': coin_mvt_plugin},
                               feerates=(7500, 7500, 7500, 7500))
    l2 = node_factory.get_node()
    addr = chainparams['example_addr']
    out_total = Millisatoshi(amount * 3 * 1000)

    # Add a medley of funds to withdraw later, bech32 + p2sh-p2wpkh
    for i in range(total_outs // 2):
        bitcoind.rpc.sendtoaddress(l1.rpc.newaddr()['bech32'],
                                   amount / 10**8)
        bitcoind.rpc.sendtoaddress(l1.rpc.newaddr('p2sh-segwit')['p2sh-segwit'],
                                   amount / 10**8)
    bitcoind.generate_block(1)
    wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == total_outs)

    # Make a PSBT out of our inputs
    funding = l1.rpc.fundpsbt(satoshi=out_total,
                              feerate=7500,
                              startweight=42,
                              reserve=True)
    assert len([x for x in l1.rpc.listfunds()['outputs'] if x['reserved']]) == 4
    psbt = bitcoind.rpc.decodepsbt(funding['psbt'])
    saved_input = psbt['tx']['vin'][0]

    # Go ahead and unreserve the UTXOs, we'll use a smaller
    # set of them to create a second PSBT that we'll attempt to sign
    # and broadcast (to disastrous results)
    l1.rpc.unreserveinputs(funding['psbt'])

    # Re-reserve one of the utxos we just unreserved
    psbt = bitcoind.rpc.createpsbt([{'txid': saved_input['txid'],
                                     'vout': saved_input['vout']}], [])
    l1.rpc.reserveinputs(psbt)

    # We require the utxos be reserved before signing them
    with pytest.raises(RpcError, match=r"Aborting PSBT signing. UTXO .* is not reserved"):
        l1.rpc.signpsbt(funding['psbt'])['signed_psbt']

    # Now we unreserve the singleton, so we can reserve it again
    l1.rpc.unreserveinputs(psbt)

    # Now add an output. Note, we add the 'excess msat' to the output so
    # that our feerate is 'correct'. This is of particular importance to elementsd,
    # who requires that every satoshi be accounted for in a tx.
    out_1_ms = Millisatoshi(funding['excess_msat'])
    output_psbt = bitcoind.rpc.createpsbt([],
                                          [{addr: float((out_total + out_1_ms).to_btc())}])
    fullpsbt = bitcoind.rpc.joinpsbts([funding['psbt'], output_psbt])

    # We re-reserve the first set...
    l1.rpc.reserveinputs(fullpsbt)

    # Sign + send the PSBT we've created
    signed_psbt = l1.rpc.signpsbt(fullpsbt)['signed_psbt']
    broadcast_tx = l1.rpc.sendpsbt(signed_psbt)

    # Check that it was broadcast successfully
    l1.daemon.wait_for_log(r'sendrawtx exit 0 .* sendrawtransaction {}'.format(broadcast_tx['tx']))
    bitcoind.generate_block(1)

    # We didn't add a change output.
    expected_outs = total_outs - 4
    wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == expected_outs)

    # Let's try *sending* a PSBT that can't be finalized (it's unsigned)
    with pytest.raises(RpcError, match=r"PSBT not finalizeable"):
        l1.rpc.sendpsbt(fullpsbt)

    # Now we try signing a PSBT with an output that's already been spent
    with pytest.raises(RpcError, match=r"Aborting PSBT signing. UTXO .* is not reserved"):
        l1.rpc.signpsbt(fullpsbt)

    # Queue up another node, to make some PSBTs for us
    for i in range(total_outs // 2):
        bitcoind.rpc.sendtoaddress(l2.rpc.newaddr()['bech32'],
                                   amount / 10**8)
        bitcoind.rpc.sendtoaddress(l2.rpc.newaddr('p2sh-segwit')['p2sh-segwit'],
                                   amount / 10**8)
    # Create a PSBT using L2
    bitcoind.generate_block(1)
    wait_for(lambda: len(l2.rpc.listfunds()['outputs']) == total_outs)
    l2_funding = l2.rpc.fundpsbt(satoshi=out_total,
                                 feerate=7500,
                                 startweight=42,
                                 reserve=True)

    # Try to get L1 to sign it
    with pytest.raises(RpcError, match=r"No wallet inputs to sign"):
        l1.rpc.signpsbt(l2_funding['psbt'])

    # With signonly it will fail if it can't sign it.
    with pytest.raises(RpcError, match=r"is unknown"):
        l1.rpc.signpsbt(l2_funding['psbt'], signonly=[0])

    # Add some of our own PSBT inputs to it
    l1_funding = l1.rpc.fundpsbt(satoshi=out_total,
                                 feerate=7500,
                                 startweight=42,
                                 reserve=True)
    l1_num_inputs = len(bitcoind.rpc.decodepsbt(l1_funding['psbt'])['tx']['vin'])
    l2_num_inputs = len(bitcoind.rpc.decodepsbt(l2_funding['psbt'])['tx']['vin'])

    # Join and add an output (reorders!)
    out_2_ms = Millisatoshi(l1_funding['excess_msat'])
    out_amt = out_2_ms + Millisatoshi(l2_funding['excess_msat']) + out_total + out_total
    output_psbt = bitcoind.rpc.createpsbt([],
                                          [{addr: float(out_amt.to_btc())}])
    joint_psbt = bitcoind.rpc.joinpsbts([l1_funding['psbt'], l2_funding['psbt'],
                                         output_psbt])

    # Ask it to sign inputs it doesn't know, it will fail.
    with pytest.raises(RpcError, match=r"is unknown"):
        l1.rpc.signpsbt(joint_psbt,
                        signonly=list(range(l1_num_inputs + 1)))

    # Similarly, it can't sign inputs it doesn't know.
    sign_success = []
    for i in range(l1_num_inputs + l2_num_inputs):
        try:
            l1.rpc.signpsbt(joint_psbt, signonly=[i])
        except RpcError:
            continue
        sign_success.append(i)
    assert len(sign_success) == l1_num_inputs

    # But it can sign all the valid ones at once.
    half_signed_psbt = l1.rpc.signpsbt(joint_psbt, signonly=sign_success)['signed_psbt']
    for s in sign_success:
        assert bitcoind.rpc.decodepsbt(half_signed_psbt)['inputs'][s]['partial_signatures'] is not None

    totally_signed = l2.rpc.signpsbt(half_signed_psbt)['signed_psbt']

    broadcast_tx = l1.rpc.sendpsbt(totally_signed)
    l1.daemon.wait_for_log(r'sendrawtx exit 0 .* sendrawtransaction {}'.format(broadcast_tx['tx']))

    # Send a PSBT that's not ours
    l2_funding = l2.rpc.fundpsbt(satoshi=out_total,
                                 feerate=7500,
                                 startweight=42,
                                 reserve=True)
    out_amt = Millisatoshi(l2_funding['excess_msat'])
    output_psbt = bitcoind.rpc.createpsbt([],
                                          [{addr: float((out_total + out_amt).to_btc())}])
    psbt = bitcoind.rpc.joinpsbts([l2_funding['psbt'], output_psbt])
    l2_signed_psbt = l2.rpc.signpsbt(psbt)['signed_psbt']
    l1.rpc.sendpsbt(l2_signed_psbt)

    # Re-try sending the same tx?
    bitcoind.generate_block(1)
    sync_blockheight(bitcoind, [l1])
    # Expect an error here
    with pytest.raises(JSONRPCError, match=r"Transaction already in block chain"):
        bitcoind.rpc.sendrawtransaction(broadcast_tx['tx'])

    # Try an empty PSBT
    with pytest.raises(RpcError, match=r"psbt: Expected a PSBT: invalid token"):
        l1.rpc.signpsbt('')
    with pytest.raises(RpcError, match=r"psbt: Expected a PSBT: invalid token"):
        l1.rpc.sendpsbt('')

    # Try an invalid PSBT string
    invalid_psbt = 'cHNidP8BAM0CAAAABJ9446mTRp/ml8OxSLC1hEvrcxG1L02AG7YZ4syHon2sAQAAAAD9////JFJH/NjKwjwrP9myuU68G7t8Q4VIChH0KUkZ5hSAyqcAAAAAAP3///8Uhrj0XDRhGRno8V7qEe4hHvZcmEjt3LQSIXWc+QU2tAEAAAAA/f///wstLikuBrgZJI83VPaY8aM7aPe5U6TMb06+jvGYzQLEAQAAAAD9////AcDGLQAAAAAAFgAUyQltQ/QI6lJgICYsza18hRa5KoEAAAAAAAEBH0BCDwAAAAAAFgAUqc1Qh7Q5kY1noDksmj7cJmHaIbQAAQEfQEIPAAAAAAAWABS3bdYeQbXvBSryHNoyYIiMBwu5rwABASBAQg8AAAAAABepFD1r0NuqAA+R7zDiXrlP7J+/PcNZhwEEFgAUKvGgVL/ThjWE/P1oORVXh/ObucYAAQEgQEIPAAAAAAAXqRRsrE5ugA1VJnAith5+msRMUTwl8ocBBBYAFMrfGCiLi0ZnOCY83ERKJ1sLYMY8A='
    with pytest.raises(RpcError, match=r"psbt: Expected a PSBT: invalid token"):
        l1.rpc.signpsbt(invalid_psbt)

    wallet_coin_mvts = [
        {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tag': 'deposit'},
        {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tag': 'deposit'},
        {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tag': 'deposit'},
        {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tag': 'deposit'},
        {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tag': 'deposit'},
        {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tag': 'deposit'},
        {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tag': 'deposit'},
        {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tag': 'deposit'},
        {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tag': 'deposit'},
        {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tag': 'deposit'},
        {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tag': 'deposit'},
        {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tag': 'deposit'},
        {'type': 'chain_mvt', 'credit': 0, 'debit': 0, 'tag': 'spend_track'},
        {'type': 'chain_mvt', 'credit': 0, 'debit': 0, 'tag': 'spend_track'},
        {'type': 'chain_mvt', 'credit': 0, 'debit': 0, 'tag': 'spend_track'},
        {'type': 'chain_mvt', 'credit': 0, 'debit': 0, 'tag': 'spend_track'},
        {'type': 'chain_mvt', 'credit': 0, 'debit': 3000000000 + int(out_1_ms), 'tag': 'withdrawal'},
        {'type': 'chain_mvt', 'credit': 0, 'debit': 1000000000 - int(out_1_ms), 'tag': 'chain_fees'},
        {'type': 'chain_mvt', 'credit': 0, 'debit': 0, 'tag': 'spend_track'},
        {'type': 'chain_mvt', 'credit': 0, 'debit': 0, 'tag': 'spend_track'},
        {'type': 'chain_mvt', 'credit': 0, 'debit': 0, 'tag': 'spend_track'},
        {'type': 'chain_mvt', 'credit': 0, 'debit': 0, 'tag': 'spend_track'},
        {'type': 'chain_mvt', 'credit': 0, 'debit': 4000000000, 'tag': 'withdrawal'},
        {'type': 'chain_mvt', 'credit': 0, 'debit': 0, 'tag': 'chain_fees'},
    ]

    check_coin_moves(l1, 'wallet', wallet_coin_mvts, chainparams)
Ejemplo n.º 11
0
def test_rebalance_all(node_factory, bitcoind):
    l1, l2, l3 = node_factory.line_graph(3, opts=plugin_opt)
    nodes = [l1, l2, l3]

    # check we get an error if theres just one channel
    result = l1.rpc.rebalanceall()
    assert result[
        'message'] == 'Error: Not enough open channels to rebalance anything'

    # now we add another 100% outgoing liquidity to l1 which does not help
    l4 = node_factory.get_node()
    l1.connect(l4)
    l1.fundchannel(l4)

    # test this is still not possible
    result = l1.rpc.rebalanceall()
    assert result[
        'message'] == 'Error: Not enough liquidity to rebalance anything'

    # remove l4 it does not distort further testing
    l1.rpc.close(l1.get_channel_scid(l4))

    # now we form a circle so we can do actually rebalanceall
    l3.connect(l1)
    l3.fundchannel(l1)

    # get scids
    scid12 = l1.get_channel_scid(l2)
    scid23 = l2.get_channel_scid(l3)
    scid31 = l3.get_channel_scid(l1)
    scids = [scid12, scid23, scid31]

    # wait for each others gossip
    bitcoind.generate_block(6)
    wait_for_all_active(nodes, scids)

    # check that theres nothing to stop when theres nothing to stop
    result = l1.rpc.rebalancestop()
    assert result['message'] == "No rebalance is running, nothing to stop."

    # check the rebalanceall starts
    result = l1.rpc.rebalanceall(feeratio=5.0)  # we need high fees to work
    assert result['message'].startswith('Rebalance started')
    l1.daemon.wait_for_logs([
        f"Try to rebalance: {scid12} -> {scid31}",
        f"Automatic rebalance finished"
    ])

    # check additional calls to stop return 'nothing to stop' + last message
    result = l1.rpc.rebalancestop()['message']
    assert result.startswith(
        "No rebalance is running, nothing to stop. "
        "Last 'rebalanceall' gave: Automatic rebalance finished")

    # wait until listpeers is up2date
    wait_for_all_htlcs(nodes)

    # check that channels are now balanced
    c12 = l1.rpc.listpeers(l2.info['id'])['peers'][0]['channels'][0]
    c13 = l1.rpc.listpeers(l3.info['id'])['peers'][0]['channels'][0]
    assert abs(0.5 - (Millisatoshi(c12['to_us_msat']) /
                      Millisatoshi(c12['total_msat']))) < 0.01
    assert abs(0.5 - (Millisatoshi(c13['to_us_msat']) /
                      Millisatoshi(c13['total_msat']))) < 0.01

    # briefly check rebalancereport works
    report = l1.rpc.rebalancereport()
    assert report.get('rebalanceall_is_running') == False
    assert report.get('total_successful_rebalances') == 2
Ejemplo n.º 12
0
def test_utxopsbt(node_factory, bitcoind, chainparams):
    amount = 1000000
    l1 = node_factory.get_node()

    outputs = []
    # Add a medley of funds to withdraw later, bech32 + p2sh-p2wpkh
    txid = bitcoind.rpc.sendtoaddress(l1.rpc.newaddr()['bech32'],
                                      amount / 10**8)
    outputs.append((txid, bitcoind.rpc.gettransaction(txid)['details'][0]['vout']))
    txid = bitcoind.rpc.sendtoaddress(l1.rpc.newaddr('p2sh-segwit')['p2sh-segwit'],
                                      amount / 10**8)
    outputs.append((txid, bitcoind.rpc.gettransaction(txid)['details'][0]['vout']))

    bitcoind.generate_block(1)
    wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == len(outputs))

    fee_val = 7500
    feerate = '{}perkw'.format(fee_val)

    # Explicitly spend the first output above.
    funding = l1.rpc.utxopsbt(amount // 2, feerate, 0,
                              ['{}:{}'.format(outputs[0][0], outputs[0][1])],
                              reserve=False)
    psbt = bitcoind.rpc.decodepsbt(funding['psbt'])
    # We can fuzz this up to 99 blocks back.
    assert psbt['tx']['locktime'] > bitcoind.rpc.getblockcount() - 100
    assert psbt['tx']['locktime'] <= bitcoind.rpc.getblockcount()
    assert len(psbt['tx']['vin']) == 1
    assert funding['excess_msat'] > Millisatoshi(0)
    assert funding['excess_msat'] < Millisatoshi(amount // 2 * 1000)
    assert funding['feerate_per_kw'] == 7500
    assert 'estimated_final_weight' in funding
    assert 'reservations' not in funding

    # This should add 99 to the weight, but otherwise be identical except for locktime.
    start_weight = 99
    funding2 = l1.rpc.utxopsbt(amount // 2, feerate, start_weight,
                               ['{}:{}'.format(outputs[0][0], outputs[0][1])],
                               reserve=False, locktime=bitcoind.rpc.getblockcount() + 1)
    psbt2 = bitcoind.rpc.decodepsbt(funding2['psbt'])
    assert psbt2['tx']['locktime'] == bitcoind.rpc.getblockcount() + 1
    assert psbt2['tx']['vin'] == psbt['tx']['vin']
    if chainparams['elements']:
        # elements includes the fee as an output
        addl_fee = Millisatoshi((fee_val * start_weight + 999) // 1000 * 1000)
        assert psbt2['tx']['vout'][0]['value'] == psbt['tx']['vout'][0]['value'] + addl_fee.to_btc()
    else:
        assert psbt2['tx']['vout'] == psbt['tx']['vout']
    assert funding2['excess_msat'] < funding['excess_msat']
    assert funding2['feerate_per_kw'] == 7500
    assert funding2['estimated_final_weight'] == funding['estimated_final_weight'] + 99
    assert 'reservations' not in funding2

    # Cannot afford this one (too much)
    with pytest.raises(RpcError, match=r"not afford"):
        l1.rpc.utxopsbt(amount, feerate, 0,
                        ['{}:{}'.format(outputs[0][0], outputs[0][1])])

    # Nor this (even with both)
    with pytest.raises(RpcError, match=r"not afford"):
        l1.rpc.utxopsbt(amount * 2, feerate, 0,
                        ['{}:{}'.format(outputs[0][0], outputs[0][1]),
                         '{}:{}'.format(outputs[1][0], outputs[1][1])])

    # Should get two inputs (and reserve!)
    funding = l1.rpc.utxopsbt(amount, feerate, 0,
                              ['{}:{}'.format(outputs[0][0], outputs[0][1]),
                               '{}:{}'.format(outputs[1][0], outputs[1][1])])
    psbt = bitcoind.rpc.decodepsbt(funding['psbt'])
    assert len(psbt['tx']['vin']) == 2
    assert len(funding['reservations']) == 2
    assert funding['reservations'][0]['txid'] == outputs[0][0]
    assert funding['reservations'][0]['vout'] == outputs[0][1]
    assert funding['reservations'][0]['was_reserved'] is False
    assert funding['reservations'][0]['reserved'] is True
    assert funding['reservations'][1]['txid'] == outputs[1][0]
    assert funding['reservations'][1]['vout'] == outputs[1][1]
    assert funding['reservations'][1]['was_reserved'] is False
    assert funding['reservations'][1]['reserved'] is True

    # Should refuse to use reserved outputs.
    with pytest.raises(RpcError, match=r"already reserved"):
        l1.rpc.utxopsbt(amount, feerate, 0,
                        ['{}:{}'.format(outputs[0][0], outputs[0][1]),
                         '{}:{}'.format(outputs[1][0], outputs[1][1])])

    # Unless we tell it that's ok.
    l1.rpc.utxopsbt(amount, feerate, 0,
                    ['{}:{}'.format(outputs[0][0], outputs[0][1]),
                     '{}:{}'.format(outputs[1][0], outputs[1][1])],
                    reservedok=True)
Ejemplo n.º 13
0
def test_fundpsbt(node_factory, bitcoind, chainparams):
    amount = 1000000
    total_outs = 4
    l1 = node_factory.get_node()

    outputs = []
    # Add a medley of funds to withdraw later, bech32 + p2sh-p2wpkh
    for i in range(total_outs // 2):
        txid = bitcoind.rpc.sendtoaddress(l1.rpc.newaddr()['bech32'],
                                          amount / 10**8)
        outputs.append((txid, bitcoind.rpc.gettransaction(txid)['details'][0]['vout']))
        txid = bitcoind.rpc.sendtoaddress(l1.rpc.newaddr('p2sh-segwit')['p2sh-segwit'],
                                          amount / 10**8)
        outputs.append((txid, bitcoind.rpc.gettransaction(txid)['details'][0]['vout']))

    bitcoind.generate_block(1)
    wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == total_outs)

    feerate = '7500perkw'

    # Should get one input, plus some excess
    funding = l1.rpc.fundpsbt(amount // 2, feerate, 0, reserve=False)
    psbt = bitcoind.rpc.decodepsbt(funding['psbt'])
    # We can fuzz this up to 99 blocks back.
    assert psbt['tx']['locktime'] > bitcoind.rpc.getblockcount() - 100
    assert psbt['tx']['locktime'] <= bitcoind.rpc.getblockcount()
    assert len(psbt['tx']['vin']) == 1
    assert funding['excess_msat'] > Millisatoshi(0)
    assert funding['excess_msat'] < Millisatoshi(amount // 2 * 1000)
    assert funding['feerate_per_kw'] == 7500
    assert 'estimated_final_weight' in funding
    assert 'reservations' not in funding

    # This should add 99 to the weight, but otherwise be identical (might choose different inputs though!) except for locktime.
    funding2 = l1.rpc.fundpsbt(amount // 2, feerate, 99, reserve=False, locktime=bitcoind.rpc.getblockcount() + 1)
    psbt2 = bitcoind.rpc.decodepsbt(funding2['psbt'])
    assert psbt2['tx']['locktime'] == bitcoind.rpc.getblockcount() + 1
    assert len(psbt2['tx']['vin']) == 1
    assert funding2['excess_msat'] < funding['excess_msat']
    assert funding2['feerate_per_kw'] == 7500
    # Naively you'd expect this to be +99, but it might have selected a non-p2sh output...
    assert funding2['estimated_final_weight'] > funding['estimated_final_weight']

    # Cannot afford this one (too much)
    with pytest.raises(RpcError, match=r"not afford"):
        l1.rpc.fundpsbt(amount * total_outs, feerate, 0)

    # Nor this (depth insufficient)
    with pytest.raises(RpcError, match=r"not afford"):
        l1.rpc.fundpsbt(amount // 2, feerate, 0, minconf=2)

    # Should get two inputs.
    psbt = bitcoind.rpc.decodepsbt(l1.rpc.fundpsbt(amount, feerate, 0, reserve=False)['psbt'])
    assert len(psbt['tx']['vin']) == 2

    # Should not use reserved outputs.
    psbt = bitcoind.rpc.createpsbt([{'txid': out[0], 'vout': out[1]} for out in outputs], [])
    l1.rpc.reserveinputs(psbt)
    with pytest.raises(RpcError, match=r"not afford"):
        l1.rpc.fundpsbt(amount // 2, feerate, 0)

    # Will use first one if unreserved.
    l1.rpc.unreserveinputs(bitcoind.rpc.createpsbt([{'txid': outputs[0][0], 'vout': outputs[0][1]}], []))
    psbt = l1.rpc.fundpsbt(amount // 2, feerate, 0)['psbt']

    # Should have passed to reserveinputs.
    with pytest.raises(RpcError, match=r"already reserved"):
        l1.rpc.reserveinputs(psbt)

    # And now we can't afford any more.
    with pytest.raises(RpcError, match=r"not afford"):
        l1.rpc.fundpsbt(amount // 2, feerate, 0)
Ejemplo n.º 14
0
def test_sign_and_send_psbt(node_factory, bitcoind, chainparams):
    """
    Tests for the sign + send psbt RPCs
    """
    amount = 1000000
    total_outs = 12
    coin_mvt_plugin = os.path.join(os.getcwd(),
                                   'tests/plugins/coin_movements.py')
    l1 = node_factory.get_node(options={'plugin': coin_mvt_plugin},
                               feerates=(7500, 7500, 7500, 7500))
    l2 = node_factory.get_node()
    addr = chainparams['example_addr']

    # Add a medley of funds to withdraw later, bech32 + p2sh-p2wpkh
    for i in range(total_outs // 2):
        bitcoind.rpc.sendtoaddress(l1.rpc.newaddr()['bech32'], amount / 10**8)
        bitcoind.rpc.sendtoaddress(
            l1.rpc.newaddr('p2sh-segwit')['p2sh-segwit'], amount / 10**8)
    bitcoind.generate_block(1)
    wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == total_outs)

    # Make a PSBT out of our inputs
    reserved = l1.rpc.reserveinputs(
        outputs=[{
            addr: Millisatoshi(3 * amount * 1000)
        }])
    assert len([x for x in l1.rpc.listfunds()['outputs']
                if x['reserved']]) == 4
    psbt = bitcoind.rpc.decodepsbt(reserved['psbt'])
    saved_input = psbt['tx']['vin'][0]

    # Go ahead and unreserve the UTXOs, we'll use a smaller
    # set of them to create a second PSBT that we'll attempt to sign
    # and broadcast (to disastrous results)
    l1.rpc.unreserveinputs(reserved['psbt'])

    # Re-reserve one of the utxos we just unreserved
    utxos = []
    utxos.append(saved_input['txid'] + ":" + str(saved_input['vout']))
    second_reservation = l1.rpc.reserveinputs(
        [{
            addr: Millisatoshi(amount * 0.5 * 1000)
        }],
        feerate='253perkw',
        utxos=utxos)

    # We require the utxos be reserved before signing them
    with pytest.raises(
            RpcError, match=r"Aborting PSBT signing. UTXO .* is not reserved"):
        l1.rpc.signpsbt(reserved['psbt'])['signed_psbt']

    # Now we unreserve the singleton, so we can reserve it again
    l1.rpc.unreserveinputs(second_reservation['psbt'])

    # We re-reserve the first set...
    utxos = []
    for vin in psbt['tx']['vin']:
        utxos.append(vin['txid'] + ':' + str(vin['vout']))
    reserved = l1.rpc.reserveinputs(outputs=[{
        addr:
        Millisatoshi(3 * amount * 1000)
    }],
                                    utxos=utxos)
    # Sign + send the PSBT we've created
    signed_psbt = l1.rpc.signpsbt(reserved['psbt'])['signed_psbt']
    broadcast_tx = l1.rpc.sendpsbt(signed_psbt)

    # Check that it was broadcast successfully
    l1.daemon.wait_for_log(r'sendrawtx exit 0 .* sendrawtransaction {}'.format(
        broadcast_tx['tx']))
    bitcoind.generate_block(1)

    # We expect a change output to be added to the wallet
    expected_outs = total_outs - 4 + 1
    wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == expected_outs)

    # Let's try *sending* a PSBT that can't be finalized (it's unsigned)
    with pytest.raises(RpcError, match=r"PSBT not finalizeable"):
        l1.rpc.sendpsbt(second_reservation['psbt'])

    # Now we try signing a PSBT with an output that's already been spent
    with pytest.raises(
            RpcError,
            match=r"Aborting PSBT signing. UTXO {} is not reserved".format(
                utxos[0])):
        l1.rpc.signpsbt(second_reservation['psbt'])

    # Queue up another node, to make some PSBTs for us
    for i in range(total_outs // 2):
        bitcoind.rpc.sendtoaddress(l2.rpc.newaddr()['bech32'], amount / 10**8)
        bitcoind.rpc.sendtoaddress(
            l2.rpc.newaddr('p2sh-segwit')['p2sh-segwit'], amount / 10**8)
    # Create a PSBT using L2
    bitcoind.generate_block(1)
    wait_for(lambda: len(l2.rpc.listfunds()['outputs']) == total_outs)
    l2_reserved = l2.rpc.reserveinputs(
        outputs=[{
            addr: Millisatoshi(3 * amount * 1000)
        }])

    # Try to get L1 to sign it
    with pytest.raises(RpcError, match=r"No wallet inputs to sign"):
        l1.rpc.signpsbt(l2_reserved['psbt'])

    # Add some of our own PSBT inputs to it
    l1_reserved = l1.rpc.reserveinputs(
        outputs=[{
            addr: Millisatoshi(3 * amount * 1000)
        }])
    joint_psbt = bitcoind.rpc.joinpsbts(
        [l1_reserved['psbt'], l2_reserved['psbt']])

    half_signed_psbt = l1.rpc.signpsbt(joint_psbt)['signed_psbt']
    totally_signed = l2.rpc.signpsbt(half_signed_psbt)['signed_psbt']

    broadcast_tx = l1.rpc.sendpsbt(totally_signed)
    l1.daemon.wait_for_log(r'sendrawtx exit 0 .* sendrawtransaction {}'.format(
        broadcast_tx['tx']))

    # Send a PSBT that's not ours
    l2_reserved = l2.rpc.reserveinputs(
        outputs=[{
            addr: Millisatoshi(3 * amount * 1000)
        }])
    l2_signed_psbt = l2.rpc.signpsbt(l2_reserved['psbt'])['signed_psbt']
    l1.rpc.sendpsbt(l2_signed_psbt)

    # Re-try sending the same tx?
    bitcoind.generate_block(1)
    sync_blockheight(bitcoind, [l1])
    # Expect an error here
    with pytest.raises(JSONRPCError,
                       match=r"Transaction already in block chain"):
        bitcoind.rpc.sendrawtransaction(broadcast_tx['tx'])

    # Try an empty PSBT
    with pytest.raises(RpcError, match=r"should be a PSBT, not"):
        l1.rpc.signpsbt('')
    with pytest.raises(RpcError, match=r"should be a PSBT, not"):
        l1.rpc.sendpsbt('')

    # Try a modified (invalid) PSBT string
    modded_psbt = l2_reserved['psbt'][:-3] + 'A' + l2_reserved['psbt'][-3:]
    with pytest.raises(RpcError, match=r"should be a PSBT, not"):
        l1.rpc.signpsbt(modded_psbt)

    wallet_coin_mvts = [
        {
            'type': 'chain_mvt',
            'credit': 1000000000,
            'debit': 0,
            'tag': 'deposit'
        },
        {
            'type': 'chain_mvt',
            'credit': 1000000000,
            'debit': 0,
            'tag': 'deposit'
        },
        {
            'type': 'chain_mvt',
            'credit': 1000000000,
            'debit': 0,
            'tag': 'deposit'
        },
        {
            'type': 'chain_mvt',
            'credit': 1000000000,
            'debit': 0,
            'tag': 'deposit'
        },
        {
            'type': 'chain_mvt',
            'credit': 1000000000,
            'debit': 0,
            'tag': 'deposit'
        },
        {
            'type': 'chain_mvt',
            'credit': 1000000000,
            'debit': 0,
            'tag': 'deposit'
        },
        {
            'type': 'chain_mvt',
            'credit': 1000000000,
            'debit': 0,
            'tag': 'deposit'
        },
        {
            'type': 'chain_mvt',
            'credit': 1000000000,
            'debit': 0,
            'tag': 'deposit'
        },
        {
            'type': 'chain_mvt',
            'credit': 1000000000,
            'debit': 0,
            'tag': 'deposit'
        },
        {
            'type': 'chain_mvt',
            'credit': 1000000000,
            'debit': 0,
            'tag': 'deposit'
        },
        {
            'type': 'chain_mvt',
            'credit': 1000000000,
            'debit': 0,
            'tag': 'deposit'
        },
        {
            'type': 'chain_mvt',
            'credit': 1000000000,
            'debit': 0,
            'tag': 'deposit'
        },
        {
            'type': 'chain_mvt',
            'credit': 0,
            'debit': 0,
            'tag': 'spend_track'
        },
        {
            'type': 'chain_mvt',
            'credit': 0,
            'debit': 0,
            'tag': 'spend_track'
        },
        {
            'type': 'chain_mvt',
            'credit': 0,
            'debit': 0,
            'tag': 'spend_track'
        },
        {
            'type': 'chain_mvt',
            'credit': 0,
            'debit': 0,
            'tag': 'spend_track'
        },
        # Nicely splits out withdrawals and chain fees, because it's all our tx
        {
            'type': 'chain_mvt',
            'credit': 0,
            'debit': 988255000,
            'tag': 'withdrawal'
        },
        {
            'type': 'chain_mvt',
            'credit': 0,
            'debit': 3000000000,
            'tag': 'withdrawal'
        },
        {
            'type': 'chain_mvt',
            'credit': 0,
            'debit': 11745000,
            'tag': 'chain_fees'
        },
        {
            'type': 'chain_mvt',
            'credit': 988255000,
            'debit': 0,
            'tag': 'deposit'
        },
        {
            'type': 'chain_mvt',
            'credit': 0,
            'debit': 0,
            'tag': 'spend_track'
        },
        {
            'type': 'chain_mvt',
            'credit': 0,
            'debit': 0,
            'tag': 'spend_track'
        },
        {
            'type': 'chain_mvt',
            'credit': 0,
            'debit': 0,
            'tag': 'spend_track'
        },
        {
            'type': 'chain_mvt',
            'credit': 0,
            'debit': 0,
            'tag': 'spend_track'
        },
        # Note that this is technically wrong since we paid 11745sat in fees
        # but since it includes inputs / outputs from a second node, we can't
        # do proper acccounting for it.
        {
            'type': 'chain_mvt',
            'credit': 0,
            'debit': 4000000000,
            'tag': 'withdrawal'
        },
        {
            'type': 'chain_mvt',
            'credit': 0,
            'debit': 0,
            'tag': 'chain_fees'
        },
        {
            'type': 'chain_mvt',
            'credit': 988255000,
            'debit': 0,
            'tag': 'deposit'
        },
    ]
    check_coin_moves(l1, 'wallet', wallet_coin_mvts)
Ejemplo n.º 15
0
def test_zero():
    # zero amounts are of course valid
    amount = Millisatoshi("0btc")
    assert int(amount) == 0
    amount = Millisatoshi("0sat")
    assert int(amount) == 0
    amount = Millisatoshi("0msat")
    assert int(amount) == 0

    # zero floating amount as well
    amount = Millisatoshi("0.0btc")
    assert int(amount) == 0
    amount = Millisatoshi("0.0sat")
    assert int(amount) == 0
    amount = Millisatoshi("0.0msat")
    assert int(amount) == 0

    # also anything multiplied by zero
    amount = Millisatoshi("1btc") * 0
    assert int(amount) == 0
    amount = Millisatoshi("1sat") * 0
    assert int(amount) == 0
    amount = Millisatoshi("1msat") * 0
    assert int(amount) == 0

    # and multiplied by a floating zero
    amount = Millisatoshi("1btc") * 0.0
    assert int(amount) == 0
    amount = Millisatoshi("1sat") * 0.0
    assert int(amount) == 0
    amount = Millisatoshi("1msat") * 0.0
    assert int(amount) == 0
Ejemplo n.º 16
0
def sendinvoiceless(plugin,
                    nodeid,
                    msatoshi: Millisatoshi,
                    maxfeepercent: float = 0.5,
                    retry_for: int = 60,
                    exemptfee: Millisatoshi = Millisatoshi(5000)):
    """Send invoiceless payments with circular routes.
    This tool sends some msatoshis without needing to have an invoice from the receiving node.
    """
    msatoshi = Millisatoshi(msatoshi)
    maxfeepercent = float(maxfeepercent)
    retry_for = int(retry_for)
    exemptfee = Millisatoshi(exemptfee)
    payload = {
        "nodeid": nodeid,
        "msatoshi": msatoshi,
        "maxfeepercent": maxfeepercent,
        "retry_for": retry_for,
        "exemptfee": exemptfee
    }
    myid = plugin.rpc.getinfo().get('id')
    label = "InvoicelessChange-" + str(uuid.uuid4())
    description = "Sending %s to %s" % (msatoshi, nodeid)
    change = Millisatoshi(1000)
    invoice = plugin.rpc.invoice(change, label, description, retry_for + 60)
    payment_hash = invoice['payment_hash']
    plugin.log("Invoice payment_hash: %s" % payment_hash)
    success_msg = ""
    try:
        excludes = []
        start_ts = int(time.time())
        while int(time.time()) - start_ts < retry_for:
            forth = plugin.rpc.getroute(nodeid,
                                        msatoshi + change,
                                        riskfactor=10,
                                        exclude=excludes)
            back = plugin.rpc.getroute(myid,
                                       change,
                                       riskfactor=10,
                                       fromid=nodeid,
                                       exclude=excludes)
            route = forth['route'] + back['route']
            setup_routing_fees(plugin, route, change, payload)
            fees = route[0]['amount_msat'] - route[-1]['amount_msat'] - msatoshi

            # check fee and exclude worst channel the next time
            # NOTE: the int(msat) casts are just a workaround for outdated pylightning versions
            if fees > exemptfee and int(
                    fees) > int(msatoshi) * maxfeepercent / 100:
                worst_channel = find_worst_channel(route, nodeid)
                if worst_channel is None:
                    raise RpcError("sendinvoiceless", payload,
                                   {'message': 'Insufficient fee'})
                excludes.append(worst_channel)
                continue

            success_msg = "%d msat delivered with %d msat fee over %d hops" % (
                msatoshi, fees, len(route))
            plugin.log("Sending %s over %d hops to send %s and return %s" %
                       (route[0]['msatoshi'], len(route), msatoshi, change))
            for r in route:
                plugin.log("    - %s  %14s  %s" %
                           (r['id'], r['channel'], r['amount_msat']))

            try:
                plugin.rpc.sendpay(route, payment_hash)
                plugin.rpc.waitsendpay(payment_hash,
                                       retry_for + start_ts - int(time.time()))
                return success_msg

            except RpcError as e:
                plugin.log("RpcError: " + str(e))
                erring_channel = e.error.get('data', {}).get('erring_channel')
                erring_direction = e.error.get('data',
                                               {}).get('erring_direction')
                if erring_channel is not None and erring_direction is not None:
                    excludes.append(erring_channel + '/' +
                                    str(erring_direction))

    except Exception as e:
        plugin.log("Exception: " + str(e))
        return cleanup(plugin, label, payload, success_msg, e)
    return cleanup(plugin, label, payload, success_msg)
Ejemplo n.º 17
0
def test_round_zero():
    # everything below 1msat should round down to zero
    amount = Millisatoshi("1msat") * 0.9
    assert int(amount) == 0
    amount = Millisatoshi("10msat") * 0.09
    assert int(amount) == 0
    amount = Millisatoshi("100msat") * 0.009
    assert int(amount) == 0
    amount = Millisatoshi("1000msat") * 0.0009
    assert int(amount) == 0

    amount = Millisatoshi("1sat") * 0.0009
    assert int(amount) == 0
    amount = Millisatoshi("0.1sat") * 0.009
    assert int(amount) == 0
    amount = Millisatoshi("0.01sat") * 0.09
    assert int(amount) == 0
    amount = Millisatoshi("0.001sat") * 0.9
    assert int(amount) == 0

    amount = Millisatoshi("10sat") * 0.00009
    assert int(amount) == 0
    amount = Millisatoshi("100sat") * 0.000009
    assert int(amount) == 0
    amount = Millisatoshi("1000sat") * 0.0000009
    assert int(amount) == 0
    amount = Millisatoshi("10000sat") * 0.00000009
    assert int(amount) == 0
    amount = Millisatoshi("10000sat") * 0.00000009
    assert int(amount) == 0

    amount = Millisatoshi("1btc") * 0.000000000009
    assert int(amount) == 0
    amount = Millisatoshi("0.1btc") * 0.00000000009
    assert int(amount) == 0
    amount = Millisatoshi("0.01btc") * 0.0000000009
    assert int(amount) == 0
    amount = Millisatoshi("0.001btc") * 0.000000009
    assert int(amount) == 0
    amount = Millisatoshi("0.0001btc") * 0.00000009
    assert int(amount) == 0
    amount = Millisatoshi("0.00001btc") * 0.0000009
    assert int(amount) == 0
    amount = Millisatoshi("0.000001btc") * 0.000009
    assert int(amount) == 0
    amount = Millisatoshi("0.0000001btc") * 0.00009
    assert int(amount) == 0
    amount = Millisatoshi("0.00000001btc") * 0.0009
    assert int(amount) == 0
    amount = Millisatoshi("0.000000001btc") * 0.009
    assert int(amount) == 0
    amount = Millisatoshi("0.0000000001btc") * 0.09
    assert int(amount) == 0
    amount = Millisatoshi("0.00000000001btc") * 0.9
    assert int(amount) == 0
Ejemplo n.º 18
0
def init(configuration, options, plugin):
    # this is the max channel size, pre-wumbo
    plugin.max_fund = Millisatoshi((2**24 - 1) * 1000)
    plugin.inflight = {}
    plugin.log('max funding set to {}'.format(plugin.max_fund))
Ejemplo n.º 19
0
def a_minus_b(a: Millisatoshi, b: Millisatoshi):
    # a minus b, but Millisatoshi cannot be negative
    return a - b if a > b else Millisatoshi(0)
Ejemplo n.º 20
0
def set_accept_funding_max(plugin, max_sats, **kwargs):
    plugin.max_fund = Millisatoshi(max_sats)

    return {'accepter_max_funding': plugin.max_fund}
Ejemplo n.º 21
0
def maybe_rebalance_pairs(plugin: Plugin, ch1, ch2, failed_channels: list):
    scid1 = ch1["short_channel_id"]
    scid2 = ch2["short_channel_id"]
    result = {"success": False, "fee_spent": Millisatoshi(0)}
    if scid1 + ":" + scid2 in failed_channels:
        return result
    # check if HTLCs are settled
    if not wait_for_htlcs(plugin, failed_channels, [scid1, scid2]):
        return result
    i = 0
    while not plugin.rebalance_stop:
        liquidity1 = liquidity_info(ch1, plugin.enough_liquidity,
                                    plugin.ideal_ratio)
        liquidity2 = liquidity_info(ch2, plugin.enough_liquidity,
                                    plugin.ideal_ratio)
        amount1 = min(must_send(liquidity1), could_receive(liquidity2))
        amount2 = min(should_send(liquidity1), should_receive(liquidity2))
        amount3 = min(could_send(liquidity1), must_receive(liquidity2))
        amount = max(amount1, amount2, amount3)
        if amount < plugin.min_amount:
            return result
        amount = min(amount, get_max_amount(i, plugin))
        maxfee = get_max_fee(plugin, amount)
        plugin.log(
            f"Try to rebalance: {scid1} -> {scid2}; amount={amount}; maxfee={maxfee}"
        )
        start_ts = time.time()
        try:
            res = rebalance(plugin,
                            outgoing_scid=scid1,
                            incoming_scid=scid2,
                            msatoshi=amount,
                            retry_for=1200,
                            maxfeepercent=0,
                            exemptfee=maxfee)
            if not res.get('status') == 'complete':
                raise Exception  # fall into exception handler below
        except Exception:
            failed_channels.append(scid1 + ":" + scid2)
            # rebalance failed, let's try with a smaller amount
            while (get_max_amount(i, plugin) >= amount and
                   get_max_amount(i, plugin) != get_max_amount(i + 1, plugin)):
                i += 1
            if amount > get_max_amount(i, plugin):
                continue
            return result
        result["success"] = True
        result["fee_spent"] += res["fee"]
        htlc_start_ts = time.time()
        # wait for settlement
        htlc_success = wait_for_htlcs(plugin, failed_channels, [scid1, scid2])
        current_ts = time.time()
        res["elapsed_time"] = str(timedelta(seconds=current_ts -
                                            start_ts))[:-3]
        res["htlc_time"] = str(timedelta(seconds=current_ts -
                                         htlc_start_ts))[:-3]
        plugin.log(f"Rebalance succeeded: {res}")
        if not htlc_success:
            return result
        ch1 = get_chan(plugin, scid1)
        assert ch1 is not None
        ch2 = get_chan(plugin, scid2)
        assert ch2 is not None
    return result
Ejemplo n.º 22
0
def test_round_down():
    # sub msat significatns should be floored
    amount = Millisatoshi("2msat") * 0.9
    assert int(amount) == 1
    amount = Millisatoshi("20msat") * 0.09
    assert int(amount) == 1
    amount = Millisatoshi("200msat") * 0.009
    assert int(amount) == 1
    amount = Millisatoshi("2000msat") * 0.0009
    assert int(amount) == 1

    amount = Millisatoshi("2sat") * 0.0009
    assert int(amount) == 1
    amount = Millisatoshi("0.2sat") * 0.009
    assert int(amount) == 1
    amount = Millisatoshi("0.02sat") * 0.09
    assert int(amount) == 1
    amount = Millisatoshi("0.002sat") * 0.9
    assert int(amount) == 1

    amount = Millisatoshi("20sat") * 0.00009
    assert int(amount) == 1
    amount = Millisatoshi("200sat") * 0.000009
    assert int(amount) == 1
    amount = Millisatoshi("2000sat") * 0.0000009
    assert int(amount) == 1
    amount = Millisatoshi("20000sat") * 0.00000009
    assert int(amount) == 1
    amount = Millisatoshi("20000sat") * 0.00000009
    assert int(amount) == 1

    amount = Millisatoshi("2btc") * 0.000000000009
    assert int(amount) == 1
    amount = Millisatoshi("0.2btc") * 0.00000000009
    assert int(amount) == 1
    amount = Millisatoshi("0.02btc") * 0.0000000009
    assert int(amount) == 1
    amount = Millisatoshi("0.002btc") * 0.000000009
    assert int(amount) == 1
    amount = Millisatoshi("0.0002btc") * 0.00000009
    assert int(amount) == 1
    amount = Millisatoshi("0.00002btc") * 0.0000009
    assert int(amount) == 1
    amount = Millisatoshi("0.000002btc") * 0.000009
    assert int(amount) == 1
    amount = Millisatoshi("0.0000002btc") * 0.00009
    assert int(amount) == 1
    amount = Millisatoshi("0.00000002btc") * 0.0009
    assert int(amount) == 1
    amount = Millisatoshi("0.000000002btc") * 0.009
    assert int(amount) == 1
    amount = Millisatoshi("0.0000000002btc") * 0.09
    assert int(amount) == 1
    amount = Millisatoshi("0.00000000002btc") * 0.9
    assert int(amount) == 1
Ejemplo n.º 23
0
def append_header(table, max_msat):
    short_str = Millisatoshi(max_msat).to_approx_str()
    table.append(
        "%c%-13sOUT/OURS %c IN/THEIRS%12s%c SCID           FLAG   BASE PERMIL AVAIL  ALIAS"
        % (draw.left, short_str, draw.mid, short_str, draw.right))
Ejemplo n.º 24
0
def test_nonegative():
    with pytest.raises(ValueError, match='Millisatoshi must be >= 0'):
        Millisatoshi("-1btc")
    with pytest.raises(ValueError, match='Millisatoshi must be >= 0'):
        Millisatoshi("-1.0btc")
    with pytest.raises(ValueError, match='Millisatoshi must be >= 0'):
        Millisatoshi("-0.1btc")
    with pytest.raises(ValueError, match='Millisatoshi must be >= 0'):
        Millisatoshi("-.1btc")
    with pytest.raises(ValueError, match='Millisatoshi must be >= 0'):
        Millisatoshi("-1sat")
    with pytest.raises(ValueError, match='Millisatoshi must be >= 0'):
        Millisatoshi("-1.0sat")
    with pytest.raises(ValueError, match='Millisatoshi must be >= 0'):
        Millisatoshi("-0.1sat")
    with pytest.raises(ValueError, match='Millisatoshi must be >= 0'):
        Millisatoshi("-.1sat")
    with pytest.raises(ValueError, match='Millisatoshi must be >= 0'):
        Millisatoshi("-1msat")
    with pytest.raises(ValueError, match='Millisatoshi must be >= 0'):
        Millisatoshi("-1.0msat")

    with pytest.raises(ValueError, match='Millisatoshi must be >= 0'):
        Millisatoshi("1msat") * -1
    with pytest.raises(ValueError, match='Millisatoshi must be >= 0'):
        Millisatoshi("1msat") * -42
    with pytest.raises(ValueError, match='Millisatoshi must be >= 0'):
        Millisatoshi("1sat") * -1
    with pytest.raises(ValueError, match='Millisatoshi must be >= 0'):
        Millisatoshi("1btc") * -1

    with pytest.raises(ValueError, match='Millisatoshi must be >= 0'):
        Millisatoshi("1msat") / -1
    with pytest.raises(ValueError, match='Millisatoshi must be >= 0'):
        Millisatoshi("1msat") / -0.5
    with pytest.raises(ValueError, match='Millisatoshi must be >= 0'):
        Millisatoshi("1sat") / -1
    with pytest.raises(ValueError, match='Millisatoshi must be >= 0'):
        Millisatoshi("1btc") / -1

    with pytest.raises(ValueError, match='Millisatoshi must be >= 0'):
        Millisatoshi("1msat") // -1
    with pytest.raises(ValueError, match='Millisatoshi must be >= 0'):
        Millisatoshi("1sat") // -1
    with pytest.raises(ValueError, match='Millisatoshi must be >= 0'):
        Millisatoshi("1btc") // -1
Ejemplo n.º 25
0
def test_or_set_chunks(plugin, payload):
    scid = payload['scid']
    cmd = payload['command']
    spendable, receivable = spendable_from_scid(plugin, payload)
    total = spendable + receivable
    amount = Millisatoshi(int(int(total) * (0.01 * payload['percentage'])))

    # if capacity exceeds, limit amount to full or empty channel
    if cmd == "drain" and amount > spendable:
        amount = spendable
    if cmd == "fill" and amount > receivable:
        amount = receivable
    if amount == Millisatoshi(0):
        raise RpcError(payload['command'], payload, {'message': 'Cannot detect required chunks to perform operation. Amount would be 0msat.'})

    # get all spendable/receivables for our channels
    channels = {}
    for channel in plugin.rpc.listchannels(source=payload['my_id']).get('channels'):
        if channel['short_channel_id'] == scid:
            continue
        try:
            spend, recv = spendable_from_scid(plugin, payload, channel['short_channel_id'], True)
        except RpcError:
            continue
        channels[channel['short_channel_id']] = {
            'spendable': spend,
            'receivable': recv,
        }
    if len(channels) == 0:
        raise RpcError(payload['command'], payload, {'message': 'Not enough usable channels to perform cyclic routing.'})

    # test if selected chunks fit into other channel capacities
    chunks = payload['chunks']
    if chunks > 0:
        chunksize = amount / chunks
        fit = 0
        for i in channels:
            channel = channels[i]
            if cmd == "drain":
                fit += int(channel['receivable']) // int(chunksize)
            if cmd == "fill":
                fit += int(channel['spendable']) // int(chunksize)
        if fit >= chunks:
            return
        if cmd == "drain":
            raise RpcError(payload['command'], payload, {'message': 'Selected chunks (%d) will not fit incoming channel capacities.' % chunks})
        if cmd == "fill":
            raise RpcError(payload['command'], payload, {'message': 'Selected chunks (%d) will not fit outgoing channel capacities.' % chunks})

    # if chunks is 0 -> auto detect from 1 to 16 (max) chunks until amounts fit
    else:
        chunks = 0
        while chunks < 16:
            chunks += 1
            chunksize = amount / chunks
            fit = 0
            for i in channels:
                channel = channels[i]
                if cmd == "drain" and int(channel['receivable']) > 0:
                    fit += int(channel['receivable']) // int(chunksize)
                if cmd == "fill" and int(channel['spendable']) > 0:
                    fit += int(channel['spendable']) // int(chunksize)
                if fit >= chunks:
                    payload['chunks'] = chunks
                    return

        if cmd == "drain":
            raise RpcError(payload['command'], payload, {'message': 'Cannot detect required chunks to perform operation. Incoming capacity problem.'})
        if cmd == "fill":
            raise RpcError(payload['command'], payload, {'message': 'Cannot detect required chunks to perform operation. Outgoing capacity problem.'})
Ejemplo n.º 26
0
def test_div():
    # msat / num := msat
    amount = Millisatoshi(42) / 2
    assert isinstance(amount, Millisatoshi)
    assert amount == Millisatoshi(21)
    amount = Millisatoshi(42) / 2.6
    assert amount == Millisatoshi(16)

    # msat / msat := num   (float ratio)
    amount = Millisatoshi(42) / Millisatoshi(2)
    assert isinstance(amount, float)
    assert amount == 21.0
    amount = Millisatoshi(8) / Millisatoshi(5)
    assert amount == 1.6

    # msat // num := msat
    amount = Millisatoshi(42) // 2
    assert isinstance(amount, Millisatoshi)
    assert amount == Millisatoshi(21)

    # msat // msat := num
    amount = Millisatoshi(42) // Millisatoshi(3)
    assert isinstance(amount, int)
    assert amount == 14
    amount = Millisatoshi(42) // Millisatoshi(3)
    assert amount == 14
    amount = Millisatoshi(42) // Millisatoshi(4)
    assert amount == 10
Ejemplo n.º 27
0
def test_deprecated_txprepare(node_factory, bitcoind):
    """Test the deprecated old-style:
       txprepare {destination} {satoshi} {feerate} {minconf}
    """
    amount = 10**4
    l1 = node_factory.get_node(options={'allow-deprecated-apis': True})
    addr = l1.rpc.newaddr()['bech32']

    for i in range(7):
        l1.fundwallet(10**8)

    bitcoind.generate_block(1)
    sync_blockheight(bitcoind, [l1])

    wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 7)

    # Array type
    with pytest.raises(
            RpcError,
            match=r'.* should be an amount in satoshis or all, not .*'):
        l1.rpc.call('txprepare', [addr, 'slow'])

    with pytest.raises(RpcError, match=r'Need set \'satoshi\' field.'):
        l1.rpc.call('txprepare', [addr])

    with pytest.raises(RpcError,
                       match=r'Could not parse destination address.*'):
        l1.rpc.call('txprepare', [Millisatoshi(amount * 100), 'slow', 1])

    l1.rpc.call('txprepare', [addr, Millisatoshi(amount * 100), 'slow', 1])
    l1.rpc.call('txprepare', [addr, Millisatoshi(amount * 100), 'normal'])
    l1.rpc.call('txprepare', [addr, Millisatoshi(amount * 100), None, 1])
    l1.rpc.call('txprepare', [addr, Millisatoshi(amount * 100)])

    # Object type
    with pytest.raises(RpcError, match=r'Need set \'outputs\' field.'):
        l1.rpc.call('txprepare', {'destination': addr, 'feerate': 'slow'})

    with pytest.raises(RpcError, match=r'Need set \'outputs\' field.'):
        l1.rpc.call(
            'txprepare', {
                'satoshi': Millisatoshi(amount * 100),
                'feerate': '10perkw',
                'minconf': 2
            })

    l1.rpc.call(
        'txprepare', {
            'destination': addr,
            'satoshi': Millisatoshi(amount * 100),
            'feerate': '2000perkw',
            'minconf': 1
        })
    l1.rpc.call(
        'txprepare', {
            'destination': addr,
            'satoshi': Millisatoshi(amount * 100),
            'feerate': '2000perkw'
        })
    l1.rpc.call('txprepare', {
        'destination': addr,
        'satoshi': Millisatoshi(amount * 100)
    })
Ejemplo n.º 28
0
def test_to_approx_str():
    amount = Millisatoshi('10000000sat')
    assert amount.to_approx_str() == "0.1btc"
    amount = Millisatoshi('1000000sat')
    assert amount.to_approx_str() == "0.01btc"
    amount = Millisatoshi('100000sat')
    assert amount.to_approx_str() == "0.001btc"
    amount = Millisatoshi('10000sat')
    assert amount.to_approx_str() == "10000sat"
    amount = Millisatoshi('1000sat')
    assert amount.to_approx_str() == "1000sat"
    amount = Millisatoshi('100msat')
    assert amount.to_approx_str() == "0.1sat"

    # also test significant rounding
    amount = Millisatoshi('10001234sat')
    assert amount.to_approx_str() == "0.1btc"
    amount = Millisatoshi('1234sat')
    assert amount.to_approx_str(3) == "1234sat"  # note: no rounding
    amount = Millisatoshi('1234sat')
    assert amount.to_approx_str(2) == "1234sat"  # note: no rounding
    amount = Millisatoshi('1230sat')
    assert amount.to_approx_str(2) == "1230sat"  # note: no rounding
    amount = Millisatoshi('12345678sat')
    assert amount.to_approx_str() == "0.123btc"
    amount = Millisatoshi('12345678sat')
    assert amount.to_approx_str(1) == "0.1btc"
    amount = Millisatoshi('15345678sat')
    assert amount.to_approx_str(1) == "0.2btc"
    amount = Millisatoshi('1200000000sat')
    assert amount.to_approx_str() == "12btc"
    amount = Millisatoshi('1200000000sat')
    assert amount.to_approx_str(1) == "12btc"  # note: no rounding
def run_check(funding_amt_str):
    if Millisatoshi(funding_amt_str).to_satoshi() % 2 == 1:
        return {'result': 'reject', 'error_message': "I don't like odd amounts"}

    return {'result': 'continue'}
Ejemplo n.º 30
0
def on_htlc_accepted(htlc, onion, plugin, request, **kwargs):
    plugin.log("Got an incoming HTLC htlc={}".format(htlc))

    # The HTLC might be a rebalance we ourselves initiated, better check
    # against the list of pending ones.
    rebalance = plugin.rebalances.get(htlc['payment_hash'], None)
    if rebalance is not None:
        # Settle the rebalance, before settling the request that initiated the
        # rebalance.
        request.set_result({
            "result": "resolve",
            "payment_key": rebalance['payment_key']
        })

        # Now wait for it to settle correctly
        plugin.rpc.waitsendpay(htlc['payment_hash'])

        rebalance['request'].set_result({"result": "continue"})

        # Clean up our stash of active rebalancings.
        del plugin.rebalances[htlc['payment_hash']]
        return

    # Check to see if the next channel has sufficient capacity
    scid = onion['short_channel_id'] if 'short_channel_id' in onion else '0x0x0'

    # Are we the destination? Then there's nothing to do. Continue.
    if scid == '0x0x0':
        request.set_result({"result": "continue"})
        return

    # Locate the channel + direction that would be the next in the path
    peers = plugin.rpc.listpeers()['peers']

    chan = None
    peer = None
    for p in peers:
        for c in p['channels']:
            if 'short_channel_id' in c and c['short_channel_id'] == scid:
                chan = c
                peer = p

    # Check if the channel is active and routable, otherwise there's little
    # point in even trying
    if not peer['connected'] or chan['state'] != "CHANNELD_NORMAL":
        request.set_result({"result": "continue"})
        return

    # Need to consider who the funder is, since they are paying the fees.
    # TODO If we are the funder we need to take the cost of an HTLC into
    # account as well.
    #funder = chan['msatoshi_to_us_max'] == chan['msatoshi_total']
    forward_amt = Millisatoshi(onion['forward_amount'])

    # If we have enough capacity just let it through now. Otherwise the
    # Millisatoshi raises an error for negative amounts in the calculation
    # below.
    if forward_amt < chan['spendable_msat']:
        request.set_result({"result": "continue"})
        return

    # Compute the amount we need to rebalance, give us a bit of breathing room
    # while we're at it (25% more rebalancing than strictly necessary) so we
    # don't end up with a completely unbalanced channel right away again, and
    # to account for a bit of fuzziness when it comes to dipping into the
    # reserve.
    amt = ceil(int(forward_amt - chan['spendable_msat']) * 1.25)

    # If we have a higher balance than is required we don't need to rebalance,
    # just stop here.
    if amt <= 0:
        request.set_result({"result": "continue"})
        return

    t = threading.Thread(target=try_rebalance,
                         args=(scid, chan, amt, peer, request))
    t.daemon = True
    t.start()