mirror of
https://github.com/ElementsProject/lightning.git
synced 2024-11-19 01:43:36 +01:00
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:
parent
c5d36c4ba9
commit
bc9333ac1e
@ -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.
|
||||
|
@ -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 \
|
||||
|
@ -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>
|
||||
|
65
doc/lightning-addpsbtoutput.7.md
Normal file
65
doc/lightning-addpsbtoutput.7.md
Normal 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)
|
@ -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;
|
||||
|
||||
|
21
doc/schemas/addpsbtoutput.request.json
Normal file
21
doc/schemas/addpsbtoutput.request.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
25
doc/schemas/addpsbtoutput.schema.json
Normal file
25
doc/schemas/addpsbtoutput.schema.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user