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
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)
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'
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
#!/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'])
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)
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
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')
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')
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)
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
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)
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)
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)
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
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)
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
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))
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)
def set_accept_funding_max(plugin, max_sats, **kwargs): plugin.max_fund = Millisatoshi(max_sats) return {'accepter_max_funding': plugin.max_fund}
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
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
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))
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
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.'})
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
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) })
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'}
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()