Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions contrib/pyln-client/pyln/client/lightning.py
Original file line number Diff line number Diff line change
Expand Up @@ -1523,6 +1523,17 @@ def fundpsbt(self, satoshi, feerate, startweight, minconf=None, reserve=None, lo
}
return self.call("fundpsbt", payload)

def addpsbtoutput(self, satoshi, initialpsbt=None, locktime=None):
"""
Create a PSBT with an output of amount satoshi leading to the on-chain wallet
"""
payload = {
"satoshi": satoshi,
"initialpsbt": initialpsbt,
"locktime": locktime,
}
return self.call("addpsbtoutput", payload)

def utxopsbt(self, satoshi, feerate, startweight, utxos, reserve=None, reservedok=False, locktime=None, min_witness_weight=None, excess_as_change=False):
"""
Create a PSBT with given inputs, to give an output of satoshi.
Expand Down
1 change: 1 addition & 0 deletions doc/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ MANPAGES := doc/lightning-cli.1 \
doc/lightning-fundchannel_complete.7 \
doc/lightning-fundchannel_cancel.7 \
doc/lightning-funderupdate.7 \
doc/lightning-addpsbtoutput.7 \
doc/lightning-fundpsbt.7 \
doc/lightning-getroute.7 \
doc/lightning-hsmtool.8 \
Expand Down
1 change: 1 addition & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Core Lightning Documentation

.. block_start manpages
lightning-addgossip <lightning-addgossip.7.md>
lightning-addpsbtoutput <lightning-addpsbtoutput.7.md>
lightning-autoclean-once <lightning-autoclean-once.7.md>
lightning-autoclean-status <lightning-autoclean-status.7.md>
lightning-batching <lightning-batching.7.md>
Expand Down
65 changes: 65 additions & 0 deletions doc/lightning-addpsbtoutput.7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
lightning-addpsbtoutput -- Command to populate PSBT outputs from the wallet
================================================================

SYNOPSIS
--------

**addpsbtoutput** *satoshi* [*initialpsbt*] [*locktime*]

DESCRIPTION
-----------

`addpsbtoutput` is a low-level RPC command which creates or modifies a PSBT
by adding a single output of amount *satoshi*.

This is used to receive funds into the on-chain wallet interactively
using PSBTs.

*satoshi* is the satoshi value of the output. It can
be a whole number, a whole number ending in *sat*, a whole number
ending in *000msat*, or a number with 1 to 8 decimal places ending in
*btc*.

*initialpsbt* is a PSBT to add the output to. If not speciifed, a PSBT
will be created automatically.

*locktime* is an optional locktime: if not set, it is set to a recent
block height (if no initial psbt is specified).

EXAMPLE USAGE
-------------

Here is a command to make a PSBT with a 100,000 sat output that leads
to the on-chain wallet.
```shell
lightning-cli addpsbtoutput 100000sat
```

RETURN VALUE
------------

[comment]: # (GENERATE-FROM-SCHEMA-START)
On success, an object is returned, containing:

- **psbt** (string): Unsigned PSBT which fulfills the parameters given
- **estimated\_added\_weight** (u32): The estimated weight of the added output
- **outnum** (u32): The 0-based number where the output was placed

[comment]: # (GENERATE-FROM-SCHEMA-END)

AUTHOR
------

@dusty\_daemon

SEE ALSO
--------

lightning-fundpsbt(7), lightning-utxopsbt(7)

RESOURCES
---------

Main web site: <https://github.com/ElementsProject/lightning>

[comment]: # ( SHA256STAMP:a0c026276fb8402b20336e6f727774fe102a4c5cb6b93ff0ed65a9c6f79d3a83)
2 changes: 1 addition & 1 deletion doc/lightning-splice_init.7.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ RESULT=$(lightning-cli listpeerchannels);
CHANNEL_ID=$(echo $RESULT| jq -r ".channels[0].channel_id");
echo $RESULT;

RESULT=$(lightning-cli newoutput 100000);
RESULT=$(lightning-cli addpsbtoutput 100000);
INITIALPSBT=$(echo $RESULT | jq -r ".psbt");
echo $RESULT;

Expand Down
21 changes: 21 additions & 0 deletions doc/schemas/addpsbtoutput.request.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"required": [
"satoshi"
],
"added": "v23.11",
"properties": {
"satoshi": {
"type": "msat"
},
"locktime": {
"type": "u32"
},
"initialpsbt": {
"type": "string",
"description": "the (optional) base 64 encoded PSBT to begin with. If not specified, one will be generated automatically"
}
}
}
25 changes: 25 additions & 0 deletions doc/schemas/addpsbtoutput.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"required": [
"psbt",
"estimated_added_weight",
"outnum"
],
"added": "v23.11",
"properties": {
"psbt": {
"type": "string",
"description": "Unsigned PSBT which fulfills the parameters given"
},
"estimated_added_weight": {
"type": "u32",
"description": "The estimated weight of the added output"
},
"outnum": {
"type": "u32",
"description": "The 0-based number where the output was placed"
}
}
}
143 changes: 143 additions & 0 deletions tests/test_splicing.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from fixtures import * # noqa: F401,F403
from pyln.client import RpcError
import pytest
import unittest
import time
Expand Down Expand Up @@ -133,3 +134,145 @@ def test_splice_listnodes(node_factory, bitcoind):

assert len(l1.rpc.listnodes()['nodes']) == 2
assert len(l2.rpc.listnodes()['nodes']) == 2


@pytest.mark.openchannel('v1')
@pytest.mark.openchannel('v2')
@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need')
def test_splice_out(node_factory, bitcoind):
l1, l2 = node_factory.line_graph(2, fundamount=1000000, wait_for_announce=True, opts={'experimental-splicing': None})

chan_id = l1.get_channel_id(l2)

funds_result = l1.rpc.addpsbtoutput(100000)

# Pay with fee by subjtracting 5000 from channel balance
result = l1.rpc.splice_init(chan_id, -105000, funds_result['psbt'])
result = l1.rpc.splice_update(chan_id, result['psbt'])
result = l1.rpc.splice_signed(chan_id, result['psbt'])

l2.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE')
l1.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE')

mempool = bitcoind.rpc.getrawmempool(True)
assert len(list(mempool.keys())) == 1
assert result['txid'] in list(mempool.keys())

bitcoind.generate_block(6, wait_for_mempool=1)

l2.daemon.wait_for_log(r'CHANNELD_AWAITING_SPLICE to CHANNELD_NORMAL')
l1.daemon.wait_for_log(r'CHANNELD_AWAITING_SPLICE to CHANNELD_NORMAL')

inv = l2.rpc.invoice(10**2, '3', 'no_3')
l1.rpc.pay(inv['bolt11'])

# Check that the splice doesn't generate a unilateral close transaction
time.sleep(5)
assert l1.db_query("SELECT count(*) as c FROM channeltxs;")[0]['c'] == 0


@pytest.mark.openchannel('v1')
@pytest.mark.openchannel('v2')
@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need')
def test_invalid_splice(node_factory, bitcoind):
# Here we do a splice but underfund it purposefully
l1, l2 = node_factory.line_graph(2, fundamount=1000000, wait_for_announce=True, opts={'experimental-splicing': None,
'may_reconnect': True,
'allow_warning': True})

chan_id = l1.get_channel_id(l2)

# We claim to add 100000 but in fact add nothing
result = l1.rpc.splice_init(chan_id, 100000)

with pytest.raises(RpcError) as rpc_error:
result = l1.rpc.splice_update(chan_id, result['psbt'])

assert rpc_error.value.error["code"] == 357
assert rpc_error.value.error["message"] == "You provided 1000000000msat but committed to 1100000000msat."

# The splicing inflight should not have been left pending in the DB
assert l1.db_query("SELECT count(*) as c FROM channel_funding_inflights;")[0]['c'] == 0

l1.daemon.wait_for_log(r'Peer has reconnected, state CHANNELD_NORMAL')

assert l1.db_query("SELECT count(*) as c FROM channel_funding_inflights;")[0]['c'] == 0

# Now we do a real splice to confirm everything works after restart
funds_result = l1.rpc.fundpsbt("109000sat", "slow", 166, excess_as_change=True)

result = l1.rpc.splice_init(chan_id, 100000, funds_result['psbt'])
result = l1.rpc.splice_update(chan_id, result['psbt'])
result = l1.rpc.signpsbt(result['psbt'])
result = l1.rpc.splice_signed(chan_id, result['signed_psbt'])

l2.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE')
l1.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE')

mempool = bitcoind.rpc.getrawmempool(True)
assert len(list(mempool.keys())) == 1
assert result['txid'] in list(mempool.keys())

bitcoind.generate_block(6, wait_for_mempool=1)

l2.daemon.wait_for_log(r'CHANNELD_AWAITING_SPLICE to CHANNELD_NORMAL')
l1.daemon.wait_for_log(r'CHANNELD_AWAITING_SPLICE to CHANNELD_NORMAL')

inv = l2.rpc.invoice(10**2, '3', 'no_3')
l1.rpc.pay(inv['bolt11'])

# Check that the splice doesn't generate a unilateral close transaction
time.sleep(5)
assert l1.db_query("SELECT count(*) as c FROM channeltxs;")[0]['c'] == 0


@pytest.mark.openchannel('v1')
@pytest.mark.openchannel('v2')
@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need')
def test_commit_crash_splice(node_factory, bitcoind):
# Here we do a normal splice out but force a restart after commiting.
l1, l2 = node_factory.line_graph(2, fundamount=1000000, wait_for_announce=True, opts={'experimental-splicing': None,
'may_reconnect': True})

chan_id = l1.get_channel_id(l2)

result = l1.rpc.splice_init(chan_id, -105000, l1.rpc.addpsbtoutput(100000)['psbt'])
result = l1.rpc.splice_update(chan_id, result['psbt'])

l1.daemon.wait_for_log(r"Splice initiator: we commit")

l1.restart()

# The splicing inflight should have been left pending in the DB
assert l1.db_query("SELECT count(*) as c FROM channel_funding_inflights;")[0]['c'] == 1

l1.daemon.wait_for_log(r'Peer has reconnected, state CHANNELD_NORMAL')

assert l1.db_query("SELECT count(*) as c FROM channel_funding_inflights;")[0]['c'] == 1

result = l1.rpc.splice_init(chan_id, -105000, l1.rpc.addpsbtoutput(100000)['psbt'])
result = l1.rpc.splice_update(chan_id, result['psbt'])
result = l1.rpc.splice_signed(chan_id, result['psbt'])

l2.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE')
l1.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE')

mempool = bitcoind.rpc.getrawmempool(True)
assert len(list(mempool.keys())) == 1
assert result['txid'] in list(mempool.keys())

bitcoind.generate_block(6, wait_for_mempool=1)

l2.daemon.wait_for_log(r'CHANNELD_AWAITING_SPLICE to CHANNELD_NORMAL')
l1.daemon.wait_for_log(r'CHANNELD_AWAITING_SPLICE to CHANNELD_NORMAL')

time.sleep(1)

assert l1.db_query("SELECT count(*) as c FROM channel_funding_inflights;")[0]['c'] == 0

inv = l2.rpc.invoice(10**2, '3', 'no_3')
l1.rpc.pay(inv['bolt11'])

# Check that the splice doesn't generate a unilateral close transaction
time.sleep(5)
assert l1.db_query("SELECT count(*) as c FROM channeltxs;")[0]['c'] == 0
27 changes: 27 additions & 0 deletions tests/test_wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,33 @@ def test_fundpsbt(node_factory, bitcoind, chainparams):
l1.rpc.fundpsbt(amount // 2, feerate, 0)


@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need')
def test_addpsbtoutput(node_factory, bitcoind, chainparams):
amount1 = 1000000
amount2 = 3333333
locktime = 111
l1 = node_factory.get_node()

result = l1.rpc.addpsbtoutput(amount1, locktime=locktime)
assert result['outnum'] == 0

psbt_info = bitcoind.rpc.decodepsbt(l1.rpc.setpsbtversion(result['psbt'], 0)['psbt'])

assert len(psbt_info['tx']['vout']) == 1
assert psbt_info['tx']['vout'][0]['n'] == result['outnum']
assert psbt_info['tx']['vout'][0]['value'] * 100000000 == amount1
assert psbt_info['tx']['locktime'] == locktime

result = l1.rpc.addpsbtoutput(amount2, result['psbt'])
n = result['outnum']

psbt_info = bitcoind.rpc.decodepsbt(l1.rpc.setpsbtversion(result['psbt'], 0)['psbt'])

assert len(psbt_info['tx']['vout']) == 2
assert psbt_info['tx']['vout'][n]['value'] * 100000000 == amount2
assert psbt_info['tx']['vout'][n]['n'] == result['outnum']


def test_utxopsbt(node_factory, bitcoind, chainparams):
amount = 1000000
l1 = node_factory.get_node()
Expand Down
Loading