Exemple #1
0
def calc_optimal_amount(out_ours, out_total, in_ours, in_total, payload):
    out_ours, out_total = int(out_ours), int(out_total)
    in_ours, in_total = int(in_ours), int(in_total)

    in_theirs = in_total - in_ours
    vo = int(out_ours - (out_total/2))
    vi = int((in_total/2) - in_ours)

    # cases where one option can be eliminated because it exceeds other capacity
    if vo > in_theirs and vi > 0 and vi < out_ours:
        return Millisatoshi(vi)
    if vi > out_ours and vo > 0 and vo < in_theirs:
        return Millisatoshi(vo)

    # cases where one channel is still capable to bring other to balance
    if vo < 0 and vi > 0 and vi < out_ours:
        return Millisatoshi(vi)
    if vi < 0 and vo > 0 and vo < in_theirs:
        return Millisatoshi(vo)

    # when both options are possible take the one with least effort
    if vo > 0 and vo < in_theirs and vi > 0 and vi < out_ours:
        return Millisatoshi(min(vi, vo))

    raise RpcError("rebalance", payload, {'message': 'rebalancing these channels will make things worse'})
Exemple #2
0
def check_channel_open(openchannel, plugin: Plugin) -> str:
    push_amount = Millisatoshi(openchannel["push_msat"]).millisatoshis

    if push_amount != 0:
        return "push amount not 0"

    channel_creation = plugin.channel_creation
    capacity = Millisatoshi(openchannel["funding_satoshis"]).millisatoshis

    expected_capacity = math.floor(
        channel_creation.invoice_amount /
        (1 - (channel_creation.inbound_percentage / 100)))

    if expected_capacity > capacity:
        return "minimum capacity requirement of {}msat not met".format(
            expected_capacity)

    channel_flags = openchannel["channel_flags"]

    # A private channel has "channel_flags" 0
    if channel_creation.private and channel_flags == 1:
        return "channel is not private"
    elif not channel_creation.private and channel_flags == 0:
        return "channel is not public"

    return ""
Exemple #3
0
def openchannel_v2(plugin, node_id, amount):
    change_output_weight = (9 + 22) * 4
    funding_output_weight = (9 + 34) * 4
    core_weight = 44
    feerate_val = 2000
    feerate = '{}perkw'.format(feerate_val)

    funding = plugin.rpc.fundpsbt(amount, feerate, funding_output_weight + core_weight)
    psbt_obj = psbt_from_base64(funding['psbt'])

    excess = Millisatoshi(funding['excess_msat'])
    # FIXME: convert feerate ?!
    change_cost = Millisatoshi(change_output_weight * feerate_val // 1000 * 1000)
    dust_limit = Millisatoshi(feerate_val * 1000)
    if excess > (dust_limit + change_cost):
        addr = plugin.rpc.newaddr()['bech32']
        change = excess - change_cost
        output = tx_output_init(int(change.to_satoshi()), get_script(addr))
        psbt_add_output_at(psbt_obj, 0, 0, output)

    resp = plugin.rpc.openchannel_init(node_id, amount,
                                       psbt_to_base64(psbt_obj, 0),
                                       commitment_feerate=feerate,
                                       funding_feerate=feerate)

    cid = resp['channel_id']
    # We don't have an updates, so we send update until our peer is also
    # finished
    while not resp['commitments_secured']:
        resp = plugin.rpc.openchannel_update(cid, resp['psbt'])

    # fixme: pass in array of our input indexes to signonly
    signed = plugin.rpc.signpsbt(resp['psbt'])
    return plugin.rpc.openchannel_signed(cid, signed['signed_psbt'])
Exemple #4
0
def on_openchannel(openchannel2, plugin, **kwargs):
    # We mirror what the peer does, wrt to funding amount
    amount = openchannel2['their_funding']
    feerate = openchannel2['feerate_per_kw_funding']
    locktime = openchannel2['locktime']

    funding = plugin.rpc.fundpsbt(amount,
                                  ''.join([str(feerate), 'perkw']),
                                  0,
                                  reserve=True,
                                  locktime=locktime)
    psbt_obj = psbt_from_base64(funding['psbt'])

    excess = Millisatoshi(funding['excess_msat'])
    change_cost = Millisatoshi(164 * feerate // 1000 * 1000)
    dust_limit = Millisatoshi(253 * 1000)
    if excess > (dust_limit + change_cost):
        addr = plugin.rpc.newaddr()['bech32']
        change = excess - change_cost
        output = tx_output_init(int(change.to_satoshi()), get_script(addr))
        psbt_add_output_at(psbt_obj, 0, 0, output)

    return {
        'result': 'continue',
        'psbt': psbt_to_base64(psbt_obj, 0),
        'accepter_funding_msat': amount
    }
Exemple #5
0
def rebalanceall(plugin: Plugin, min_amount: Millisatoshi = Millisatoshi("50000sat"), feeratio: float = 0.5):
    """Rebalance all unbalanced channels if possible for a very low fee.
    Default minimum rebalancable amount is 50000sat. Default feeratio = 0.5, half of our node's default fee.
    To be economical, it tries to fix the liquidity cheaper than it can be ruined by transaction forwards.
    It may run for a long time (hours) in the background, but can be stopped with the rebalancestop method.
    """
    # some early checks before we start the async thread
    if plugin.mutex.locked():
        return {"message": "Rebalance is already running, this may take a while. To stop it use the cli method 'rebalancestop'."}
    channels = get_open_channels(plugin)
    if len(channels) <= 1:
        return {"message": "Error: Not enough open channels to rebalance anything"}
    our = sum(ch["to_us_msat"] for ch in channels)
    total = sum(ch["total_msat"] for ch in channels)
    min_amount = Millisatoshi(min_amount)
    if total - our < min_amount or our < min_amount:
        return {"message": "Error: Not enough liquidity to rebalance anything"}

    # param parsing ensure correct type
    plugin.feeratio = float(feeratio)
    plugin.min_amount = min_amount

    # run the job
    t = Thread(target=rebalanceall_thread, args=(plugin, ))
    t.start()
    return {"message": f"Rebalance started with min rebalancable amount: {plugin.min_amount}, feeratio: {plugin.feeratio}"}
Exemple #6
0
def test_multiwithdraw_simple(node_factory, bitcoind):
    """
    Test simple multiwithdraw usage.
    """
    l1, l2, l3 = node_factory.get_nodes(3)
    l1.fundwallet(10**8)

    addr2 = l2.rpc.newaddr()['bech32']
    amount2 = Millisatoshi(2222 * 1000)
    addr3 = l3.rpc.newaddr()['bech32']
    amount3 = Millisatoshi(3333 * 1000)

    # Multiwithdraw!
    txid = l1.rpc.multiwithdraw([{addr2: amount2}, {addr3: amount3}])["txid"]
    bitcoind.generate_block(1)
    sync_blockheight(bitcoind, [l1, l2, l3])

    # l2 shoulda gotten money.
    funds2 = l2.rpc.listfunds()['outputs']
    assert only_one(funds2)["txid"] == txid
    assert only_one(funds2)["address"] == addr2
    assert only_one(funds2)["status"] == "confirmed"
    assert only_one(funds2)["amount_msat"] == amount2

    # l3 shoulda gotten money.
    funds3 = l3.rpc.listfunds()['outputs']
    assert only_one(funds3)["txid"] == txid
    assert only_one(funds3)["address"] == addr3
    assert only_one(funds3)["status"] == "confirmed"
    assert only_one(funds3)["amount_msat"] == amount3
Exemple #7
0
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'])
        fee = Millisatoshi(ch['base_fee_millisatoshi'])
        # BOLT #7 requires fee >= fee_base_msat + ( amount_to_forward * fee_proportional_millionths / 1000000 )
        fee += (amount_iter * ch['fee_per_millionth'] + 10**6 - 1) // 10**6    # integer math trick to round up
        amount_iter += fee
        delay += ch['delay']

    # amounts have to be calculated the other way when being fee substracted
    # we took the upper loop as well for the delay parameter
    if substractfees:
        amount_iter = amount
        first = True
        for r in route:
            channels = plugin.rpc.listchannels(r['channel'])
            ch = next(c for c in channels.get('channels') if c['destination'] == r['id'])
            if not first:
                fee = Millisatoshi(ch['base_fee_millisatoshi'])
                # BOLT #7 requires fee >= fee_base_msat + ( amount_to_forward * fee_proportional_millionths / 1000000 )
                fee += (amount_iter * ch['fee_per_millionth'] + 10**6 - 1) // 10**6    # integer math trick to round up
                if fee > amount_iter:
                    raise RpcError(payload['command'], payload, {'message': 'Cannot cover fees to %s %s' % (payload['command'], amount)})
                amount_iter -= fee
            first = False
            r['msatoshi'] = amount_iter.millisatoshis
            r['amount_msat'] = amount_iter
Exemple #8
0
def test_queryrates(node_factory, bitcoind):
    l1, l2 = node_factory.get_nodes(2)

    amount = 10 ** 6

    l1.fundwallet(amount * 10)
    l2.fundwallet(amount * 10)

    l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
    with pytest.raises(RpcError, match=r'not advertising liquidity'):
        l1.rpc.dev_queryrates(l2.info['id'], amount, amount * 10)

    l2.rpc.call('funderupdate', {'policy': 'match',
                                 'policy_mod': 100,
                                 'per_channel_max_msat': '1btc',
                                 'fuzz_percent': 0,
                                 'lease_fee_base_msat': '2sat',
                                 'funding_weight': 1000,
                                 'lease_fee_basis': 140,
                                 'channel_fee_max_base_msat': '3sat',
                                 'channel_fee_max_proportional_thousandths': 101})

    l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
    result = l1.rpc.dev_queryrates(l2.info['id'], amount, amount)
    assert result['our_funding_msat'] == Millisatoshi(amount * 1000)
    assert result['their_funding_msat'] == Millisatoshi(amount * 1000)
    assert result['funding_weight'] == 1000
    assert result['lease_fee_base_msat'] == Millisatoshi(2000)
    assert result['lease_fee_basis'] == 140
    assert result['channel_fee_max_base_msat'] == Millisatoshi(3000)
    assert result['channel_fee_max_proportional_thousandths'] == 101
Exemple #9
0
def amounts_from_scid(plugin, scid):
    channels = plugin.rpc.listfunds().get('channels')
    channel = next(
        c for c in channels
        if 'short_channel_id' in c and c['short_channel_id'] == scid)
    our_msat = Millisatoshi(channel['our_amount_msat'])
    total_msat = Millisatoshi(channel['amount_msat'])
    return our_msat, total_msat
Exemple #10
0
def check_balance_snaps(n, expected_bals):
    snaps = n.rpc.listsnapshots()['balance_snapshots']
    for snap, exp in zip(snaps, expected_bals):
        assert snap['blockheight'] == exp['blockheight']
        for acct, exp_acct in zip(snap['accounts'], exp['accounts']):
            # FIXME: also check 'account_id's (these change every run)
            for item in ['balance_msat']:
                assert Millisatoshi(acct[item]) == Millisatoshi(exp_acct[item])
Exemple #11
0
def rebalancereport(plugin: Plugin):
    """Show information about rebalance
    """
    res = {}
    res["rebalanceall_is_running"] = plugin.mutex.locked()
    res["getroute_method"] = plugin.getroute.__name__
    res["maxhops_threshold"] = plugin.maxhops
    res["msatfactor"] = plugin.msatfactor
    res["erringnodes_threshold"] = plugin.erringnodes
    channels = get_open_channels(plugin)
    health_percent = 0.0
    if len(channels) > 1:
        enough_liquidity = get_enough_liquidity_threshold(channels)
        ideal_ratio = get_ideal_ratio(channels, enough_liquidity)
        res["enough_liquidity_threshold"] = enough_liquidity
        res["ideal_liquidity_ratio"] = f"{ideal_ratio * 100:.2f}%"
        for ch in channels:
            liquidity = liquidity_info(ch, enough_liquidity, ideal_ratio)
            health_percent += health_score(liquidity) * int(ch["total_msat"])
        health_percent /= int(sum(ch["total_msat"] for ch in channels))
    else:
        res["enough_liquidity_threshold"] = Millisatoshi(0)
        res["ideal_liquidity_ratio"] = "0%"
    res["liquidity_health"] = f"{health_percent:.2f}%"
    invoices = plugin.rpc.listinvoices()['invoices']
    rebalances = [
        i for i in invoices
        if i.get('status') == 'paid' and i.get('label').startswith("Rebalance")
    ]
    total_fee = Millisatoshi(0)
    total_amount = Millisatoshi(0)
    res["total_successful_rebalances"] = len(rebalances)
    # pyln-client does not support the 'status' argument as yet
    # pays = plugin.rpc.listpays(status="complete")["pays"]
    pays = plugin.rpc.listpays()["pays"]
    pays = [p for p in pays if p.get('status') == 'complete']
    for r in rebalances:
        try:
            pay = next(p for p in pays
                       if p["payment_hash"] == r["payment_hash"])
            total_amount += pay["amount_msat"]
            total_fee += pay["amount_sent_msat"] - pay["amount_msat"]
        except Exception:
            res["total_successful_rebalances"] -= 1
    res["total_rebalanced_amount"] = total_amount
    res["total_rebalance_fee"] = total_fee
    if total_amount > Millisatoshi(0):
        res["average_rebalance_fee_ppm"] = round(
            total_fee / total_amount * 10**6, 2)
    else:
        res["average_rebalance_fee_ppm"] = 0

    avg_forward_fees = get_avg_forward_fees(plugin, [1, 7, 30])
    res['average_forward_fee_ppm_1d'] = avg_forward_fees[0]
    res['average_forward_fee_ppm_7d'] = avg_forward_fees[1]
    res['average_forward_fee_ppm_30d'] = avg_forward_fees[2]

    return res
Exemple #12
0
def receivedinvoiceless(plugin,
                        min_amount: Millisatoshi = Millisatoshi(10000)):
    """
    List payments received via sendinvoiceless from other nodes.
    """

    mynodeid = plugin.rpc.getinfo()['id']
    mychannels = plugin.rpc.listchannels(source=mynodeid)['channels']
    forwards = plugin.rpc.listforwards()['forwards']
    default_fees = {
        'base': int(plugin.get_option('fee-base')),
        'ppm': int(plugin.get_option('fee-per-satoshi'))
    }

    # build a mapping of mychannel fees
    # <scid -> {base, ppm}>
    myfees = {}
    for channel in mychannels:
        scid = channel['short_channel_id']
        myfees[scid] = {
            'base': channel['base_fee_millisatoshi'],
            'ppm': channel['fee_per_millionth']
        }

    # loop through settled forwards and check for overpaid routings
    result = []
    for forward in forwards:
        if forward['status'] != "settled":
            continue

        # for old channel, we dont know fees anymore, use defaults
        scid = forward['out_channel']
        fees = myfees.get(scid, default_fees)
        fee_paid = forward['fee']
        fee_required = int(forward['out_msatoshi'] * fees['ppm'] * 10**-6 +
                           fees['base'])

        if fee_paid > fee_required:
            amount = Millisatoshi(fee_paid - fee_required)

            # fess can sometimes not be exact when channel fees changed in the past, filter those
            if amount < min_amount:
                continue

            entry = {'amount_msat': amount, 'amount_btc': amount.to_btc_str()}

            # old lightningd versions may not support received_time yet
            if 'resolved_time' in forward:
                time_secs = int(forward['resolved_time'])
                time_str = datetime.utcfromtimestamp(time_secs).strftime(
                    '%Y-%m-%d %H:%M:%S (UTC)')
                entry['resolved_time'] = forward['resolved_time']
                entry['timestamp'] = time_str

            result.append(entry)

    return result
Exemple #13
0
def account_balance(n, account_id):
    moves = dedupe_moves(n.rpc.call('listcoinmoves_plugin')['coin_moves'])
    chan_moves = [m for m in moves if m['account_id'] == account_id]
    assert len(chan_moves) > 0
    m_sum = Millisatoshi(0)
    for m in chan_moves:
        m_sum += Millisatoshi(m['credit_msat'])
        m_sum -= Millisatoshi(m['debit_msat'])
    return m_sum
Exemple #14
0
def init(options: dict, configuration: dict, plugin: Plugin, **kwargs):
    plugin.our_node_id = plugin.rpc.getinfo()["id"]
    plugin.deactivate_fuzz = options.get("feeadjuster-deactivate-fuzz")
    plugin.forward_event_subscription = not options.get(
        "feeadjuster-deactivate-fee-update")
    plugin.update_threshold = float(options.get("feeadjuster-threshold"))
    plugin.update_threshold_abs = Millisatoshi(
        options.get("feeadjuster-threshold-abs"))
    plugin.big_enough_liquidity = Millisatoshi(
        options.get("feeadjuster-enough-liquidity"))
    plugin.imbalance = float(options.get("feeadjuster-imbalance"))
    adjustment_switch = {
        "soft": get_ratio_soft,
        "hard": get_ratio_hard,
        "default": get_ratio
    }
    plugin.get_ratio = adjustment_switch.get(
        options.get("feeadjuster-adjustment-method"), get_ratio)
    fee_strategy_switch = {
        "global": get_fees_global,
        "median": get_fees_median
    }
    plugin.fee_strategy = fee_strategy_switch.get(
        options.get("feeadjuster-feestrategy"), get_fees_global)
    config = plugin.rpc.listconfigs()
    plugin.adj_basefee = config["fee-base"]
    plugin.adj_ppmfee = config["fee-per-satoshi"]

    # normalize the imbalance percentage value to 0%-50%
    if plugin.imbalance < 0 or plugin.imbalance > 1:
        raise ValueError("feeadjuster-imbalance must be between 0 and 1.")
    if plugin.imbalance > 0.5:
        plugin.imbalance = 1 - plugin.imbalance

    # detect if server supports the new listchannels by `destination` (#4614)
    plugin.listchannels_by_dst = False
    rpchelp = plugin.rpc.help().get('help')
    if len([
            c for c in rpchelp if c["command"].startswith("listchannels ")
            and "destination" in c["command"]
    ]) == 1:
        plugin.listchannels_by_dst = True

    plugin.log(
        f"Plugin feeadjuster initialized "
        f"({plugin.adj_basefee} base / {plugin.adj_ppmfee} ppm) with an "
        f"imbalance of {int(100 * plugin.imbalance)}%/{int(100 * ( 1 - plugin.imbalance))}%, "
        f"update_threshold: {int(100 * plugin.update_threshold)}%, "
        f"update_threshold_abs: {plugin.update_threshold_abs}, "
        f"enough_liquidity: {plugin.big_enough_liquidity}, "
        f"deactivate_fuzz: {plugin.deactivate_fuzz}, "
        f"forward_event_subscription: {plugin.forward_event_subscription}, "
        f"adjustment_method: {plugin.get_ratio.__name__}, "
        f"fee_strategy: {plugin.fee_strategy.__name__}, "
        f"listchannels_by_dst: {plugin.listchannels_by_dst}")
    plugin.mutex.release()
    feeadjust(plugin)
Exemple #15
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 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_log(f"Try to rebalance: {scid12} -> {scid31}")
    wait_for(lambda: not l1.rpc.rebalanceall()['message'].startswith(
        "Rebalance is already running"))
    result = l1.rpc.rebalancestop()
    assert result['message'].startswith("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
Exemple #16
0
def check_coin_moves(n, account_id, expected_moves, chainparams):
    moves = n.rpc.call('listcoinmoves_plugin')['coin_moves']
    # moves can lag; wait for a few seconds if we don't have correct number.
    # then move on: we'll get details below.
    expected_count = 0
    for m in enumerate(expected_moves):
        if isinstance(m, list):
            expected_count += len(m)
        else:
            expected_count += 1

    if len(moves) != expected_count:
        time.sleep(5)
        moves = n.rpc.call('listcoinmoves_plugin')['coin_moves']

    node_id = n.info['id']
    acct_moves = [m for m in moves if m['account_id'] == account_id]
    for mv in acct_moves:
        print(
            "{{'type': '{}', 'credit': {}, 'debit': {}, 'tags': '{}' , ['fees'?: '{}']}},"
            .format(mv['type'],
                    Millisatoshi(mv['credit']).millisatoshis,
                    Millisatoshi(mv['debit']).millisatoshis, mv['tags'],
                    mv['fees'] if 'fees' in mv else ''))
        assert mv['version'] == 2
        assert mv['node_id'] == node_id
        assert mv['timestamp'] > 0
        assert mv['coin_type'] == chainparams['bip173_prefix']
        # chain moves should have blockheights
        if mv['type'] == 'chain_mvt' and mv['account_id'] != 'external':
            assert mv['blockheight'] is not None

    for num, m in enumerate(expected_moves):
        # They can group things which are in any order.
        if isinstance(m, list):
            number_moves = len(m)
            for acct_move in acct_moves[:number_moves]:
                found = None
                for i in range(len(m)):
                    if move_matches(m[i], acct_move):
                        found = i
                        break
                if found is None:
                    raise ValueError("Unexpected move {} amongst {}".format(
                        acct_move, m))
                del m[i]
            acct_moves = acct_moves[number_moves:]
        else:
            if not move_matches(m, acct_moves[0]):
                raise ValueError("Unexpected move {}: {} != {}".format(
                    num, acct_moves[0], m))
            acct_moves = acct_moves[1:]

    assert acct_moves == []
def channels_to_rebalance(rpc):
    incoming = []
    outgoing = []
    peers = rpc.listpeers()
    for p in peers['peers']:
        if not p['connected']:
            continue
        for c in p['channels']:
            if c['state'] != 'CHANNELD_NORMAL':
                continue
            if c['short_channel_id'] in exclude:
                continue
            if c['our_reserve_msat'] < c['to_us_msat']:
                to_us = c['to_us_msat'] - c['our_reserve_msat']
            else:
                to_us = Millisatoshi(0)

            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)

            middle = (to_us + to_them) / 2
            to_middle = to_us - middle if to_us > to_them else to_them - middle
            perc = int((to_middle.to_satoshi() /
                        (to_us + to_them).to_satoshi()) * 100)

            chan = {
                'to_us': to_us,
                'to_them': to_them,
                'chan_id': c['short_channel_id'],
                'to_middle': to_middle,
                'percent': perc
            }
            if perc < SKIPPING_PERC:  # consider balanced channel around 10% from middle
                print("skipping %s" % str(chan))
                continue
            if to_us > to_them:
                outgoing.append(chan)
            else:
                incoming.append(chan)

    incoming = sorted(incoming, key=lambda k: k['to_middle'])
    outgoing = sorted(outgoing, key=lambda k: k['to_middle'])

    print()
    print("incoming")
    print_list(incoming)
    print("outgoing")
    print_list(outgoing)

    return incoming, outgoing
Exemple #18
0
def test_rebalance_manual(node_factory, bitcoind):
    l1, l2, l3 = node_factory.line_graph(3, opts=plugin_opt)
    nodes = [l1, l2, l3]

    # form a circle so we can do rebalancing
    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)
    for n in nodes:
        for scid in scids:
            n.wait_channel_active(scid)

    # check we can do an auto amount rebalance
    result = l1.rpc.rebalance(scid12, scid31)
    print(result)
    assert result['status'] == 'complete'
    assert result['outgoing_scid'] == scid12
    assert result['incoming_scid'] == scid31
    assert result['hops'] == 3
    assert result['received'] == '500000000msat'

    # 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

    # check we can do a manual amount rebalance in the other direction
    result = l1.rpc.rebalance(scid31, scid12, '250000000msat')
    assert result['status'] == 'complete'
    assert result['outgoing_scid'] == scid31
    assert result['incoming_scid'] == scid12
    assert result['hops'] == 3
    assert result['received'] == '250000000msat'

    # briefly check rebalancereport works
    report = l1.rpc.rebalancereport()
    assert report.get('rebalanceall_is_running') == False
    assert report.get('total_successful_rebalances') == 2
Exemple #19
0
def rebalanceall(plugin: Plugin, min_amount: Millisatoshi = Millisatoshi("50000sat"), feeratio: float = 0.5):
    """Rebalance all unbalanced channels if possible for a very low fee.
    Default minimum rebalancable amount is 50000sat. Default feeratio = 0.5, half of our node's default fee.
    To be economical, it tries to fix the liquidity cheaper than it can be ruined by transaction forwards.
    It may run for a long time (hours) in the background, but can be stopped with the rebalancestop method.
    """
    if plugin.mutex.locked():
        return {"message": "Rebalance is already running, this may take a while. To stop it use the cli method 'rebalancestop'."}
    plugin.feeratio = float(feeratio)
    plugin.min_amount = Millisatoshi(min_amount)
    t = Thread(target=rebalanceall_thread, args=(plugin, ))
    t.start()
    return {"message": "Rebalance started"}
Exemple #20
0
def get_enough_liquidity_threshold(channels: list):
    low = Millisatoshi(0)
    biggest_channel = max(channels, key=lambda ch: ch["total_msat"])
    high = biggest_channel["total_msat"] / 2
    while True:
        mid = (low + high) / 2
        if high - low < Millisatoshi("1sat"):
            break
        if check_liquidity_threshold(channels, mid):
            low = mid
        else:
            high = mid
    return mid / 2
def test_init():
    # Note: Ongoing Discussion, hence the `with pytest.raises`.
    # https://github.com/ElementsProject/lightning/pull/4273#discussion_r540369093
    #
    # Initialization with a float should be possible:
    # Millisatoshi(5) / 2 currently works, and removes the half msat.
    # So Millisatoshi(5 / 2) should be the same.
    amount = Millisatoshi(5) / 2
    assert amount == Millisatoshi(2)
    with pytest.raises(
            TypeError,
            match="Millisatoshi by float is currently not supported"):
        assert amount == Millisatoshi(5 / 2)

    ratio = Millisatoshi(8) / Millisatoshi(5)
    assert isinstance(ratio, float)
    with pytest.raises(
            TypeError,
            match="Millisatoshi by float is currently not supported"):
        assert Millisatoshi(ratio) == Millisatoshi(8 / 5)

    # Check that init by a round float is allowed.
    # Required by some existing tests: tests/test_wallet.py::test_txprepare
    amount = Millisatoshi(42.0)
    assert amount == 42
Exemple #22
0
def on_openchannel(openchannel, plugin, **kwargs):
    # - a multiple of 11: we send back a valid address (regtest)
    if Millisatoshi(openchannel['funding_satoshis']).to_satoshi() % 11 == 0:
        return {'result': 'continue', 'close_to': 'bcrt1q7gtnxmlaly9vklvmfj06amfdef3rtnrdazdsvw'}

    # - a multiple of 7: we send back an empty address
    if Millisatoshi(openchannel['funding_satoshis']).to_satoshi() % 7 == 0:
        return {'result': 'continue', 'close_to': ''}

    # - a multiple of 5: we send back an address for the wrong chain (mainnet)
    if Millisatoshi(openchannel['funding_satoshis']).to_satoshi() % 5 == 0:
        return {'result': 'continue', 'close_to': 'bc1qlq8srqnz64wgklmqvurv7qnr4rvtq2u96hhfg2'}

    # - otherwise: we don't include the close_to
    return {'result': 'continue'}
Exemple #23
0
def test_millisatoshi_passthrough(node_factory):
    """ Ensure that Millisatoshi arguments and return work.
    """
    plugin_path = os.path.join(os.getcwd(), 'tests/plugins/millisatoshis.py')
    n = node_factory.get_node(options={'plugin': plugin_path, 'log-level': 'io'})

    # By keyword
    ret = n.rpc.call('echo', {'msat': Millisatoshi(17), 'not_an_msat': '22msat'})['echo_msat']
    assert type(ret) == Millisatoshi
    assert ret == Millisatoshi(17)

    # By position
    ret = n.rpc.call('echo', [Millisatoshi(18), '22msat'])['echo_msat']
    assert type(ret) == Millisatoshi
    assert ret == Millisatoshi(18)
 def is_msat_request(checker, instance):
     """msat fields can be raw integers, sats, btc."""
     try:
         Millisatoshi(instance)
         return True
     except TypeError:
         return False
Exemple #25
0
def init(options, configuration, plugin):
    config = plugin.rpc.listconfigs()
    plugin.cltv_final = config.get("cltv-final")
    plugin.fee_base = Millisatoshi(config.get("fee-base"))
    plugin.fee_ppm = config.get("fee-per-satoshi")
    plugin.mutex = Lock()
    plugin.log("Plugin rebalance.py initialized")
Exemple #26
0
def get_currencyrate(plugin, currency, urlformat, replymembers):
    # NOTE: Bitstamp has a DNS/Proxy issues that can return 404
    # Workaround: retry up to 5 times with a delay
    currency_lc = currency.lower()
    url = urlformat.format(currency_lc=currency_lc, currency=currency)
    r = requests_retry_session(retries=5, status_forcelist=(404)).get(
        url, proxies=plugin.proxies)

    if r.status_code != 200:
        plugin.log(level='info',
                   message='{}: bad response {}'.format(url, r.status_code))
        return None

    json = r.json()
    for m in replymembers:
        expanded = m.format(currency_lc=currency_lc, currency=currency)
        if expanded not in json:
            plugin.log(level='debug',
                       message='{}: {} not in {}'.format(url, expanded, json))
            return None
        json = json[expanded]

    try:
        return Millisatoshi(int(10**11 / float(json)))
    except Exception:
        plugin.log(level='info',
                   message='{}: could not convert {} to msat'.format(
                       url, json))
        return None
Exemple #27
0
def currencyconvert(plugin, amount, currency):
    """Converts currency using given APIs."""
    rates = get_rates(plugin, currency.upper())
    if len(rates) == 0:
        raise Exception("No values available for currency {}".format(currency.upper()))
    val = statistics.median([m.millisatoshis for m in rates.values()]) * float(amount)
    return {"msat": Millisatoshi(round(val))}
Exemple #28
0
def rebalanceall_thread(plugin: Plugin):
    if not plugin.mutex.acquire(blocking=False):
        return
    try:
        start_ts = time.time()
        feeadjuster_state = feeadjuster_toggle(plugin, False)
        channels = get_open_channels(plugin)
        plugin.enough_liquidity = get_enough_liquidity_threshold(channels)
        plugin.ideal_ratio = get_ideal_ratio(channels, plugin.enough_liquidity)
        plugin.log(f"Automatic rebalance is running with enough liquidity threshold: {plugin.enough_liquidity}, "
                   f"ideal liquidity ratio: {plugin.ideal_ratio * 100:.2f}%, "
                   f"min rebalancable amount: {plugin.min_amount}, "
                   f"feeratio: {plugin.feeratio}")
        failed_pairs = []
        success = 0
        fee_spent = Millisatoshi(0)
        while not plugin.rebalance_stop:
            result = maybe_rebalance_once(plugin, failed_pairs)
            if not result["success"]:
                break
            success += 1
            fee_spent += result["fee_spent"]
        feeadjust_would_be_nice(plugin)
        feeadjuster_toggle(plugin, feeadjuster_state)
        elapsed_time = timedelta(seconds=time.time() - start_ts)
        plugin.log(f"Automatic rebalance finished: {success} successful rebalance, {fee_spent} fee spent, it took {str(elapsed_time)[:-3]}")
    finally:
        plugin.mutex.release()
Exemple #29
0
    def fundbalancedchannel(self, remote_node, total_capacity, announce=True):
        '''
        Creates a perfectly-balanced channel, as all things should be.
        '''
        if isinstance(total_capacity, Millisatoshi):
            total_capacity = int(total_capacity.to_satoshi())
        else:
            total_capacity = int(total_capacity)

        self.fundwallet(total_capacity + 10000)
        self.rpc.connect(remote_node.info['id'], 'localhost', remote_node.port)

        # Make sure the fundchannel is confirmed.
        num_tx = len(self.bitcoin.rpc.getrawmempool())
        tx = self.rpc.fundchannel(remote_node.info['id'],
                                  total_capacity,
                                  feerate='slow',
                                  minconf=0,
                                  announce=announce,
                                  push_msat=Millisatoshi(total_capacity *
                                                         500))['tx']
        wait_for(lambda: len(self.bitcoin.rpc.getrawmempool()) == num_tx + 1)
        self.bitcoin.generate_block(1)

        # Generate the scid.
        # NOTE This assumes only the coinbase and the fundchannel is
        # confirmed in the block.
        return '{}x1x{}'.format(
            self.bitcoin.rpc.getblockcount(),
            get_tx_p2wsh_outnum(self.bitcoin, tx, total_capacity))
Exemple #30
0
def rebalancereport(plugin: Plugin):
    """Show information about rebalance
    """
    res = {}
    res["rebalanceall_is_running"] = plugin.mutex.locked()
    res["getroute_method"] = plugin.getroute.__name__
    res["maxhops_threshold"] = plugin.maxhops
    res["msatfactor"] = plugin.msatfactor
    res["erringnodes_threshold"] = plugin.erringnodes
    channels = get_open_channels(plugin)
    health_percent = 0.0
    if len(channels) > 1:
        enough_liquidity = get_enough_liquidity_threshold(channels)
        ideal_ratio = get_ideal_ratio(channels, enough_liquidity)
        res["enough_liquidity_threshold"] = enough_liquidity
        res["ideal_liquidity_ratio"] = f"{ideal_ratio * 100:.2f}%"
        for ch in channels:
            liquidity = liquidity_info(ch, enough_liquidity, ideal_ratio)
            health_percent += health_score(liquidity) * int(ch["total_msat"])
        health_percent /= int(sum(ch["total_msat"] for ch in channels))
    else:
        res["enough_liquidity_threshold"] = Millisatoshi(0)
        res["ideal_liquidity_ratio"] = "0%"
    res["liquidity_health"] = f"{health_percent:.2f}%"
    invoices = plugin.rpc.listinvoices()['invoices']
    rebalances = [
        i for i in invoices
        if i.get('status') == 'paid' and i.get('label').startswith("Rebalance")
    ]
    total_fee = Millisatoshi(0)
    total_amount = Millisatoshi(0)
    res["total_successful_rebalances"] = len(rebalances)
    for r in rebalances:
        try:
            pay = plugin.rpc.listpays(r["bolt11"])["pays"][0]
            total_amount += pay["amount_msat"]
            total_fee += pay["amount_sent_msat"] - pay["amount_msat"]
        except Exception:
            res["total_successful_rebalances"] -= 1
    res["total_rebalanced_amount"] = total_amount
    res["total_rebalance_fee"] = total_fee
    if total_amount > Millisatoshi(0):
        res["average_rebalance_fee_ppm"] = round(
            total_fee / total_amount * 10**6, 2)
    else:
        res["average_rebalance_fee_ppm"] = 0
    return res