Skip to content

Commit

Permalink
addpsbtoutput: New onchain command for PSBT output
Browse files Browse the repository at this point in the history
Also added splice_out tests that use the new PSBT command.

ChangeLog-Added: New `addpsbtoutput` command for creating a PSBT that can receive funds to the on-chain wallet.
  • Loading branch information
ddustin authored and rustyrussell committed Oct 2, 2023
1 parent 72f914a commit f1dfec4
Show file tree
Hide file tree
Showing 10 changed files with 383 additions and 1 deletion.
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

0 comments on commit f1dfec4

Please sign in to comment.