addpsbtoutput: New onchain command for PSBT output

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.
This commit is contained in:
Dusty Daemon 2023-09-14 14:29:20 -04:00 committed by Vincenzo Palazzo
parent c5d36c4ba9
commit bc9333ac1e
10 changed files with 383 additions and 1 deletions

View File

@ -1523,6 +1523,17 @@ class LightningRpc(UnixDomainSocketRpc):
}
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.

View File

@ -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 \

View File

@ -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>

View File

@ -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)

View File

@ -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;

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -1,4 +1,5 @@
from fixtures import * # noqa: F401,F403
from pyln.client import RpcError
import pytest
import unittest
import time
@ -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

View File

@ -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()

View File

@ -654,6 +654,94 @@ static const struct json_command fundpsbt_command = {
};
AUTODATA(json_command, &fundpsbt_command);
static struct command_result *json_addpsbtoutput(struct command *cmd,
const char *buffer,
const jsmntok_t *obj UNNEEDED,
const jsmntok_t *params)
{
struct json_stream *response;
struct amount_sat *amount;
struct wally_psbt *psbt;
u32 *locktime;
ssize_t outnum;
u32 weight;
struct pubkey pubkey;
s64 keyidx;
u8 *b32script;
if (!param(cmd, buffer, params,
p_req("satoshi", param_sat, &amount),
p_opt("initialpsbt", param_psbt, &psbt),
p_opt("locktime", param_number, &locktime),
NULL))
return command_param_failed();
if (!psbt) {
if (!locktime) {
locktime = tal(cmd, u32);
*locktime = default_locktime(cmd->ld->topology);
}
psbt = create_psbt(cmd, 0, 0, *locktime);
}
else if(locktime) {
return command_fail(cmd, FUNDING_PSBT_INVALID,
"Can't set locktime of an existing {initialpsbt}");
}
if (!validate_psbt(psbt))
return command_fail(cmd,
FUNDING_PSBT_INVALID,
"PSBT failed to validate.");
if (amount_sat_less(*amount, chainparams->dust_limit))
return command_fail(cmd, FUND_OUTPUT_IS_DUST,
"Receive amount is below dust limit (%s)",
type_to_string(tmpctx,
struct amount_sat,
&chainparams->dust_limit));
/* Get a change adddress */
keyidx = wallet_get_newindex(cmd->ld);
if (keyidx < 0)
return command_fail(cmd, LIGHTNINGD,
"Failed to generate change address."
" Keys exhausted.");
if (chainparams->is_elements) {
bip32_pubkey(cmd->ld, &pubkey, keyidx);
b32script = scriptpubkey_p2wpkh(tmpctx, &pubkey);
} else {
b32script = p2tr_for_keyidx(tmpctx, cmd->ld, keyidx);
}
if (!b32script) {
return command_fail(cmd, LIGHTNINGD,
"Failed to generate change address."
" Keys generation failure");
}
txfilter_add_scriptpubkey(cmd->ld->owned_txfilter, b32script);
outnum = psbt->num_outputs;
psbt_append_output(psbt, b32script, *amount);
/* Add additional weight of output */
weight = bitcoin_tx_output_weight(
chainparams->is_elements ? BITCOIN_SCRIPTPUBKEY_P2WPKH_LEN : BITCOIN_SCRIPTPUBKEY_P2TR_LEN);
response = json_stream_success(cmd);
json_add_psbt(response, "psbt", psbt);
json_add_num(response, "estimated_added_weight", weight);
json_add_num(response, "outnum", outnum);
return command_success(cmd, response);
}
static const struct json_command addpsbtoutput_command = {
"addpsbtoutput",
"bitcoin",
json_addpsbtoutput,
"Create a PSBT (or modify existing {initialpsbt}) with an output receiving {satoshi} amount.",
false
};
AUTODATA(json_command, &addpsbtoutput_command);
static struct command_result *param_txout(struct command *cmd,
const char *name,
const char *buffer,