From 38e2428f12af69bbd9af145d969eaca6c6ade99f Mon Sep 17 00:00:00 2001 From: niftynei Date: Wed, 5 Oct 2022 16:02:52 -0500 Subject: [PATCH] funder: use utxopsbt to build psbt for RBFs We use the saved previous outputs (plus maybe some new ones?) to build a psbt for an RBF request. RBFs utxo reuse is now working so we can unfail the test (and update it to reflect that the lease sticks around through an RBF cycle). Changelog-Fixed: Plugins: `funder` now honors lease requests across RBFs --- plugins/funder.c | 101 ++++++++++++++++++++++++++++++++++++------ tests/test_opening.py | 5 +-- 2 files changed, 89 insertions(+), 17 deletions(-) diff --git a/plugins/funder.c b/plugins/funder.c index 5be686186..69d0bfd87 100644 --- a/plugins/funder.c +++ b/plugins/funder.c @@ -486,18 +486,66 @@ static bool previously_reserved(struct bitcoin_outpoint **prev_outs, return false; } +struct funder_utxo { + struct bitcoin_outpoint out; + struct amount_sat val; +}; + +static struct out_req * +build_utxopsbt_request(struct command *cmd, + struct open_info *info, + struct amount_sat requested_funds, + struct amount_sat committed_funds, + struct funder_utxo **avail_utxos) +{ + struct out_req *req; + + req = jsonrpc_request_start(cmd->plugin, cmd, + "utxopsbt", + &psbt_funded, + &psbt_fund_failed, + info); + /* Add every prev_out */ + json_array_start(req->js, "utxos"); + for (size_t i = 0; i < tal_count(info->prev_outs); i++) + json_add_outpoint(req->js, NULL, info->prev_outs[i]); + + /* Next add available utxos until we surpass the + * requested funds goal */ + /* FIXME: Update `utxopsbt` to automatically add more inputs? */ + for (size_t i = 0; i < tal_count(avail_utxos); i++) { + /* If we've already hit our goal, break */ + if (amount_sat_greater_eq(committed_funds, requested_funds)) + break; + + /* Add this output to the UTXO */ + json_add_outpoint(req->js, NULL, &avail_utxos[i]->out); + + /* Account for it */ + if (!amount_sat_add(&committed_funds, committed_funds, + avail_utxos[i]->val)) + /* This should really never happen */ + plugin_err(cmd->plugin, "overflow adding committed"); + } + json_array_end(req->js); + return req; +} + static struct command_result * listfunds_success(struct command *cmd, const char *buf, const jsmntok_t *result, struct open_info *info) { - struct amount_sat available_funds, est_fee; + struct amount_sat available_funds, committed_funds, est_fee; const jsmntok_t *outputs_tok, *tok; struct out_req *req; size_t i; const char *funding_err; + /* We only use this for RBFs, when there's a prev_outs list */ + struct funder_utxo **avail_utxos = tal_arr(cmd, struct funder_utxo *, 0); + outputs_tok = json_get_member(buf, result, "outputs"); if (!outputs_tok) plugin_err(cmd->plugin, @@ -506,24 +554,25 @@ listfunds_success(struct command *cmd, json_tok_full(buf, result)); available_funds = AMOUNT_SAT(0); + committed_funds = AMOUNT_SAT(0); json_for_each_arr(i, tok, outputs_tok) { - struct amount_sat val; - struct bitcoin_outpoint out; + struct funder_utxo *utxo; bool is_reserved, is_p2sh; char *status; const char *err; + utxo = tal(cmd, struct funder_utxo); err = json_scan(tmpctx, buf, tok, "{amount_msat:%" ",status:%" ",reserved:%" ",txid:%" ",output:%}", - JSON_SCAN(json_to_msat_as_sats, &val), + JSON_SCAN(json_to_msat_as_sats, &utxo->val), JSON_SCAN_TAL(cmd, json_strdup, &status), JSON_SCAN(json_to_bool, &is_reserved), - JSON_SCAN(json_to_txid, &out.txid), - JSON_SCAN(json_to_number, &out.n)); + JSON_SCAN(json_to_txid, &utxo->out.txid), + JSON_SCAN(json_to_number, &utxo->out.n)); if (err) plugin_err(cmd->plugin, "`listfunds` payload did not scan. %s: %*.s", @@ -542,7 +591,8 @@ listfunds_success(struct command *cmd, /* we skip reserved funds that aren't in our previous * inputs list! */ - if (is_reserved && !previously_reserved(info->prev_outs, &out)) + if (is_reserved && + !previously_reserved(info->prev_outs, &utxo->out)) continue; /* we skip unconfirmed+spent funds */ @@ -551,12 +601,25 @@ listfunds_success(struct command *cmd, /* Don't include outputs that can't cover their weight; * subtract the fee for this utxo out of the utxo */ - if (!amount_sat_sub(&val, val, est_fee)) + if (!amount_sat_sub(&utxo->val, utxo->val, est_fee)) continue; - if (!amount_sat_add(&available_funds, available_funds, val)) + if (!amount_sat_add(&available_funds, available_funds, + utxo->val)) plugin_err(cmd->plugin, "`listfunds` overflowed output values"); + + /* If this is an RBF, we keep track of available utxos */ + if (info->prev_outs) { + /* if not previously reserved, it's committed */ + if (!previously_reserved(info->prev_outs, &utxo->out)) + tal_arr_expand(&avail_utxos, utxo); + else if (!amount_sat_add(&committed_funds, + committed_funds, utxo->val)) + plugin_err(cmd->plugin, + "`listfunds` overflowed" + " committed output values"); + } } funding_err = calculate_our_funding(current_policy, @@ -585,11 +648,20 @@ listfunds_success(struct command *cmd, type_to_string(tmpctx, struct amount_sat, &info->their_funding)); - req = jsonrpc_request_start(cmd->plugin, cmd, - "fundpsbt", - &psbt_funded, - &psbt_fund_failed, - info); + /* If there's prevouts, we compose a psbt with those first, + * then add more funds for anything missing */ + if (info->prev_outs) { + req = build_utxopsbt_request(cmd, info, + info->our_funding, + committed_funds, + avail_utxos); + json_add_bool(req->js, "reservedok", true); + } else + req = jsonrpc_request_start(cmd->plugin, cmd, + "fundpsbt", + &psbt_funded, + &psbt_fund_failed, + info); json_add_string(req->js, "satoshi", type_to_string(tmpctx, struct amount_sat, &info->our_funding)); @@ -597,6 +669,7 @@ listfunds_success(struct command *cmd, tal_fmt(tmpctx, "%"PRIu64"%s", info->funding_feerate_perkw, feerate_style_name(FEERATE_PER_KSIPA))); + /* Our startweight is zero because we're freeriding on their open * transaction ! */ json_add_num(req->js, "startweight", 0); diff --git a/tests/test_opening.py b/tests/test_opening.py index c1b280694..bdf4caae1 100644 --- a/tests/test_opening.py +++ b/tests/test_opening.py @@ -343,7 +343,6 @@ def test_v2_rbf_single(node_factory, bitcoind, chainparams): @unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') @pytest.mark.openchannel('v2') -@pytest.mark.xfail def test_v2_rbf_liquidity_ad(node_factory, bitcoind, chainparams): opts = {'funder-policy': 'match', 'funder-policy-mod': 100, @@ -409,8 +408,8 @@ def test_v2_rbf_liquidity_ad(node_factory, bitcoind, chainparams): # This should be the accepter's amount fundings = only_one(only_one(l1.rpc.listpeers()['peers'])['channels'])['funding'] - # FIXME: The lease goes away :( - assert Millisatoshi(0) == Millisatoshi(fundings['remote_funds_msat']) + # The lease is still there! + assert Millisatoshi(amount * 1000) == fundings['remote_funds_msat'] wait_for(lambda: [c['active'] for c in l1.rpc.listchannels(l1.get_channel_scid(l2))['channels']] == [True, True])