Skip to content
Open
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
159 changes: 90 additions & 69 deletions plugins/spender/splice.c
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,16 @@ static struct command_result *unreserve_get_result(struct command *cmd,
return make_error(cmd, abort_pkg, "unreserve_get_result");
}

static struct command_result *free_abort_pkg_and_forward(struct command *cmd,
const char *methodname,
const char *buf,
const jsmntok_t *result,
struct abort_pkg *abort_pkg)
{
tal_free(abort_pkg);
return forward_error(cmd, methodname, buf, result, NULL);
}

static struct command_result *abort_get_result(struct command *cmd,
const char *methodname,
const char *buf,
Expand All @@ -163,7 +173,8 @@ static struct command_result *abort_get_result(struct command *cmd,
return make_error(cmd, abort_pkg, "abort_get_result");

req = jsonrpc_request_start(cmd, "unreserveinputs",
unreserve_get_result, forward_error,
unreserve_get_result,
free_abort_pkg_and_forward,
abort_pkg);

json_add_psbt(req->js, "psbt", splice_cmd->psbt);
Expand Down Expand Up @@ -193,7 +204,9 @@ static struct command_result *do_fail(struct command *cmd,
abort_pkg->code = code;

req = jsonrpc_request_start(cmd, "abort_channels",
abort_get_result, forward_error, abort_pkg);
abort_get_result,
free_abort_pkg_and_forward,
abort_pkg);

added = 0;
json_array_start(req->js, "channel_ids");
Expand Down Expand Up @@ -1620,6 +1633,77 @@ static struct command_result *handle_fee_and_ppm(struct command *cmd,
return NULL;
}

/* Fund out to bitcoin addresses */
static struct command_result *handle_bitcoin_addrs(struct command *cmd,
struct splice_cmd *splice_cmd)
{
struct splice_script_result *action;
struct splice_cmd_action_state *state;
struct wally_psbt_output *output;
char *bitcoin_address;
u64 serial_id;
u8 *scriptpubkey;

for (size_t i = 0; i < tal_count(splice_cmd->actions); i++) {
action = splice_cmd->actions[i];
state = splice_cmd->states[i];
if (!action->bitcoin_address)
continue;
if (state->state != SPLICE_CMD_NONE)
continue;

if (!amount_sat_is_zero(action->out_sat))
return do_fail(cmd, splice_cmd,
JSONRPC2_INVALID_PARAMS,
"Cannot fund from bitcoin"
" address");
if (!decode_scriptpubkey_from_addr(cmd, chainparams,
action->bitcoin_address,
&scriptpubkey))
return do_fail(cmd, splice_cmd,
JSONRPC2_INVALID_PARAMS,
"Bitcoin address"
" unrecognized");

/* Reencode scriptpubkey to addr for verification */
bitcoin_address = encode_scriptpubkey_to_addr(tmpctx,
chainparams,
scriptpubkey,
tal_bytelen(scriptpubkey));
if (!bitcoin_address)
return do_fail(cmd, splice_cmd,
JSONRPC2_INVALID_PARAMS,
"Bitcoin scriptpubkey failed"
" reencoding for address");

if (0 != strcmp(bitcoin_address, action->bitcoin_address))
return do_fail(cmd, splice_cmd,
JSONRPC2_INVALID_PARAMS,
tal_fmt(tmpctx,
"Bitcoin scriptpubkey"
" failed validation for"
" address. Reencoded"
" address is %s while"
" address from script is"
" %s",
bitcoin_address ?: "NULL",
action->bitcoin_address));

output = psbt_append_output(splice_cmd->psbt,
scriptpubkey,
action->in_sat);

serial_id = psbt_new_output_serial(splice_cmd->psbt,
TX_INITIATOR);
psbt_output_set_serial_id(splice_cmd->psbt, output,
serial_id);

state->state = SPLICE_CMD_DONE;
}

return NULL;
}

static struct command_result *continue_splice(struct command *cmd,
struct splice_cmd *splice_cmd)
{
Expand Down Expand Up @@ -1661,6 +1745,10 @@ static struct command_result *continue_splice(struct command *cmd,
splice_cmd->fee_calculated = true;
}

result = handle_bitcoin_addrs(cmd, splice_cmd);
if (result)
return result;

/* Only after fee calcualtion can we add wallet actions taking funds */
for (size_t i = 0; i < tal_count(splice_cmd->actions); i++) {
action = splice_cmd->actions[i];
Expand Down Expand Up @@ -1762,18 +1850,13 @@ static struct command_result *execute_splice(struct command *cmd,
struct splice_cmd *splice_cmd)
{
struct splice_script_result *action;
struct splice_cmd_action_state *state;
struct wally_psbt_output *output;
u64 serial_id;
int pays_fee;
u8 *scriptpubkey;

/* Basic validation */
pays_fee = 0;
for (size_t i = 0; i < tal_count(splice_cmd->actions); i++) {
int dest_count = 0;
action = splice_cmd->actions[i];
state = splice_cmd->states[i];

if (action->out_ppm && !action->onchain_wallet)
return do_fail(cmd, splice_cmd, JSONRPC2_INVALID_PARAMS,
Expand Down Expand Up @@ -1824,8 +1907,6 @@ static struct command_result *execute_splice(struct command *cmd,

for (size_t i = 0; i < tal_count(splice_cmd->actions); i++) {
action = splice_cmd->actions[i];
state = splice_cmd->states[i];
char *bitcoin_address;

/* `out_ppm` is the percent to take out of the action.
* If it is set to '*' we get a value of UINT32_MAX.
Expand All @@ -1842,61 +1923,11 @@ static struct command_result *execute_splice(struct command *cmd,
" feerate");
splice_cmd->feerate_per_kw = action->feerate_per_kw;
}

/* Fund out to bitcoin address */
if (action->bitcoin_address) {
if (!amount_sat_is_zero(action->in_sat))
return do_fail(cmd, splice_cmd,
JSONRPC2_INVALID_PARAMS,
"Cannot fund from bitcoin"
" address");
if (!decode_scriptpubkey_from_addr(cmd, chainparams,
action->bitcoin_address,
&scriptpubkey))
return do_fail(cmd, splice_cmd,
JSONRPC2_INVALID_PARAMS,
"Bitcoin address"
" unrecognized");

/* Reencode scriptpubkey to addr for verification */
bitcoin_address = encode_scriptpubkey_to_addr(tmpctx,
chainparams,
scriptpubkey,
tal_bytelen(scriptpubkey));
if (!bitcoin_address)
return do_fail(cmd, splice_cmd,
JSONRPC2_INVALID_PARAMS,
"Bitcoin scriptpubkey failed"
" reencoding for address");

if (!strcmp(bitcoin_address, action->bitcoin_address))
return do_fail(cmd, splice_cmd,
JSONRPC2_INVALID_PARAMS,
"Bitcoin scriptpubkey failed"
" validation for address");

output = psbt_append_output(splice_cmd->psbt,
scriptpubkey,
action->in_sat);

/* DTODO: support dynamic address payouts (percent) */

serial_id = psbt_new_output_serial(splice_cmd->psbt,
TX_INITIATOR);
psbt_output_set_serial_id(splice_cmd->psbt, output,
serial_id);

state->state = SPLICE_CMD_DONE;

add_to_debug_log(splice_cmd,
"execute_splice-load_btcaddress");
}
}

/* Set needed funds to the wallet contributions. */
for (size_t i = 0; i < tal_count(splice_cmd->actions); i++) {
action = splice_cmd->actions[i];
state = splice_cmd->states[i];
if (action->onchain_wallet
&& !amount_sat_is_zero(action->out_sat)) {
splice_cmd->needed_funds = action->out_sat;
Expand Down Expand Up @@ -2042,16 +2073,6 @@ validate_splice_cmd(struct splice_cmd *splice_cmd)
" fee");
paying_fee_count++;
}
if (action->bitcoin_address && action->in_ppm)
return command_fail(splice_cmd->cmd,
JSONRPC2_INVALID_PARAMS,
"Dynamic bitcoin address amounts"
" not supported for now");
if (action->bitcoin_address)
return command_fail(splice_cmd->cmd,
JSONRPC2_INVALID_PARAMS,
"Paying out to bitcoin addresses"
" not supported for now.");
}

return NULL;
Expand Down
33 changes: 32 additions & 1 deletion tests/test_splice.py
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,38 @@ def test_easy_splice_out(node_factory, bitcoind, chainparams):
assert initial_wallet_balance + Millisatoshi(spliceamt * 1000) == end_wallet_balance


@pytest.mark.xfail(strict=True)
@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_address(node_factory, bitcoind, chainparams):
fundamt = 1000000

l1, l2 = node_factory.line_graph(2, fundamount=fundamt, wait_for_announce=True,
opts={'experimental-splicing': None})

initial_wallet_balance = Millisatoshi(bkpr_account_balance(l1, 'wallet'))

addr = l1.rpc.newaddr()['p2tr']

# Splice out 100k from first channel, putting result less fees into onchain wallet via addres
spliceamt = 100000
l1.rpc.splice(f"*:? -> {spliceamt}+fee; {spliceamt} -> {addr}")

bitcoind.generate_block(6, wait_for_mempool=1)
l2.daemon.wait_for_log(r'lightningd, splice_locked clearing inflights')

p1 = only_one(l1.rpc.listpeerchannels(peer_id=l2.info['id'])['channels'])
p2 = only_one(l2.rpc.listpeerchannels(l1.info['id'])['channels'])
assert 'inflight' not in p1
assert 'inflight' not in p2

wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 2)
wait_for(lambda: len(l1.rpc.listfunds()['channels']) == 1)

end_wallet_balance = Millisatoshi(bkpr_account_balance(l1, 'wallet'))
assert initial_wallet_balance + Millisatoshi(spliceamt * 1000) == end_wallet_balance


@pytest.mark.openchannel('v1')
@pytest.mark.openchannel('v2')
@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need')
Expand Down
Loading