lightningd: don't let them fundpsbt below emergency reserve.

This is the simple version which always tries to keep some sats if we
have an anchor channel.  Turns out that we need something more
sophisticated for multifundchannel, so that's next.

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
Changelog-Changed: JSON-RPC: `withdraw` will refuse to spend funds below `min-emergency-msat` if we have any anchor channels (and `all` will be reduced appropriately).
Changelog-Changed: JSON-RPC: `fundpsbt` and `utxopsbt` will refuse to spend funds below `min-emergency-msat` if we have any anchor channels.
This commit is contained in:
Rusty Russell 2023-06-29 09:44:10 +09:30
parent 75aca3cbb6
commit 391da2f440
10 changed files with 221 additions and 15 deletions

View File

@ -966,22 +966,28 @@ size_t bitcoin_tx_2of2_input_witness_weight(void)
);
}
struct amount_sat change_amount(struct amount_sat excess, u32 feerate_perkw,
size_t total_weight)
struct amount_sat change_fee(u32 feerate_perkw, size_t total_weight)
{
size_t outweight;
struct amount_sat change_fee;
struct amount_sat fee;
/* Must be able to pay for its own additional weight */
outweight = bitcoin_tx_output_weight(BITCOIN_SCRIPTPUBKEY_P2WPKH_LEN);
/* Rounding can cause off by one errors, so we do this */
if (!amount_sat_sub(&change_fee,
if (!amount_sat_sub(&fee,
amount_tx_fee(feerate_perkw, outweight + total_weight),
amount_tx_fee(feerate_perkw, total_weight)))
return AMOUNT_SAT(0);
abort();
return fee;
}
if (!amount_sat_sub(&excess, excess, change_fee))
struct amount_sat change_amount(struct amount_sat excess, u32 feerate_perkw,
size_t total_weight)
{
struct amount_sat fee = change_fee(feerate_perkw, total_weight);
if (!amount_sat_sub(&excess, excess, fee))
return AMOUNT_SAT(0);
/* Must be non-dust */

View File

@ -320,17 +320,25 @@ size_t bitcoin_tx_simple_input_weight(bool p2sh);
size_t bitcoin_tx_2of2_input_witness_weight(void);
/**
* change_amount - Is it worth making a P2WPKH change output at this feerate?
* @excess: input amount we have above the tx fee and other outputs.
* change_fee - what's the cost to add a change output to this tx?
* @feerate_perkw: feerate.
*
* If it's not worth (or possible) to make change, returns AMOUNT_SAT(0).
* Otherwise returns the amount of the change output to add (@excess minus
* the additional fee for the change output itself).
* @total_weight: current weight of tx.
*
* We pass in the total_weight of the tx (up until this point) so as
* to avoid any off-by-one errors with rounding the change fee (down)
*/
struct amount_sat change_fee(u32 feerate_perkw, size_t total_weight);
/**
* change_amount - Is it worth making a P2WPKH change output at this feerate?
* @excess: input amount we have above the tx fee and other outputs.
* @feerate_perkw: feerate.
* @total_weight: current weight of tx.
*
* If it's not worth (or possible) to make change, returns AMOUNT_SAT(0).
* Otherwise returns the amount of the change output to add (@excess minus
* the change_fee()).
*/
struct amount_sat change_amount(struct amount_sat excess, u32 feerate_perkw,
size_t total_weight);

View File

@ -63,6 +63,7 @@ enum jsonrpc_errcode {
FUNDING_V2_NOT_SUPPORTED = 310,
FUNDING_UNKNOWN_CHANNEL = 311,
FUNDING_STATE_INVALID = 312,
FUND_CANNOT_AFFORD_WITH_EMERGENCY = 313,
/* `connect` errors */
CONNECT_NO_KNOWN_ADDRESS = 400,

View File

@ -16,7 +16,9 @@ The address can be of any Bitcoin accepted type, including bech32.
*satoshi* is the amount to be withdrawn from the internal wallet
(expressed, as name suggests, in satoshi). The string *all* can be used
to specify withdrawal of all available funds. Otherwise, it is in
to specify withdrawal of all available funds (but if we have
any anchor channels, this will always leave at least `min-emergency-msat` as change).
. Otherwise, it is in
satoshi precision; 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*.
@ -50,6 +52,7 @@ The following error codes may occur:
- 301: There are not enough funds in the internal wallet (including
fees) to create the transaction.
- 302: The dust limit is not met.
- 313: The `min-emergency-msat` reserve not be preserved (and we have anchor channels).
AUTHOR
------

View File

@ -729,6 +729,31 @@ struct channel *find_channel_by_alias(const struct peer *peer,
return NULL;
}
bool have_anchor_channel(struct lightningd *ld)
{
struct peer *p;
struct channel *channel;
struct peer_node_id_map_iter it;
for (p = peer_node_id_map_first(ld->peers, &it);
p;
p = peer_node_id_map_next(ld->peers, &it)) {
if (p->uncommitted_channel) {
/* FIXME: Assume anchors if supported */
if (feature_negotiated(ld->our_features,
p->their_features,
OPT_ANCHORS_ZERO_FEE_HTLC_TX))
return true;
}
list_for_each(&p->channels, channel, list) {
if (channel_type_has(channel->type,
OPT_ANCHORS_ZERO_FEE_HTLC_TX))
return true;
}
}
return false;
}
void channel_set_last_tx(struct channel *channel,
struct bitcoin_tx *tx,
const struct bitcoin_signature *sig)

View File

@ -441,6 +441,10 @@ struct channel *find_channel_by_alias(const struct peer *peer,
const struct short_channel_id *alias,
enum side side);
/* Do we have any channel with option_anchors_zero_fee_htlc_tx? (i.e. we
* might need to CPFP the fee if it force closes!) */
bool have_anchor_channel(struct lightningd *ld);
void channel_set_last_tx(struct channel *channel,
struct bitcoin_tx *tx,
const struct bitcoin_signature *sig);

View File

@ -1126,6 +1126,16 @@ static char *opt_set_sat(const char *arg, struct amount_sat *sat)
return NULL;
}
static char *opt_set_sat_nondust(const char *arg, struct amount_sat *sat)
{
char *ret = opt_set_sat(arg, sat);
if (ret)
return ret;
if (amount_sat_less(*sat, chainparams->dust_limit))
return tal_fmt(tmpctx, "Option must be over dust limit!");
return NULL;
}
static bool opt_show_sat(char *buf, size_t len, const struct amount_sat *sat)
{
struct amount_msat msat;
@ -1447,7 +1457,7 @@ static void register_opts(struct lightningd *ld)
opt_set_u64, opt_show_u64, &ld->config.commit_fee_percent,
"Percentage of fee to request for their commitment");
clnopt_witharg("--min-emergency-msat", OPT_SHOWMSATS,
opt_set_sat, opt_show_sat, &ld->emergency_sat,
opt_set_sat_nondust, opt_show_sat, &ld->emergency_sat,
"Amount to leave in wallet for spending anchor closes");
clnopt_witharg("--subdaemon",
OPT_MULTI,

View File

@ -10,6 +10,7 @@
#include <common/key_derive.h>
#include <common/type_to_string.h>
#include <lightningd/chaintopology.h>
#include <lightningd/channel.h>
#include <lightningd/hsm_control.h>
#include <lightningd/jsonrpc.h>
#include <lightningd/lightningd.h>
@ -436,6 +437,52 @@ static inline u32 minconf_to_maxheight(u32 minconf, struct lightningd *ld)
return ld->topology->tip->height - minconf + 1;
}
/* Returns false if it needed to create change, but couldn't afford. */
static bool change_for_emergency(struct lightningd *ld,
bool have_anchor_channel,
struct utxo **utxos,
u32 feerate_per_kw,
u32 weight,
struct amount_sat *excess,
struct amount_sat *change)
{
struct amount_sat fee;
/* Only needed for anchor channels */
if (!have_anchor_channel)
return true;
/* Fine if rest of wallet has funds. */
if (wallet_has_funds(ld->wallet,
cast_const2(const struct utxo **, utxos),
get_block_height(ld->topology),
ld->emergency_sat))
return true;
/* If we can afford with existing change output, great (or
* ld->emergency_sat is 0) */
if (amount_sat_greater_eq(change_amount(*change,
feerate_per_kw, weight),
ld->emergency_sat))
return true;
/* Try splitting excess to add to change. */
fee = change_fee(feerate_per_kw, weight);
if (!amount_sat_sub(excess, *excess, fee)
|| !amount_sat_sub(excess, *excess, ld->emergency_sat))
return false;
if (!amount_sat_add(change, *change, fee)
|| !amount_sat_add(change, *change, ld->emergency_sat))
abort();
/* We *will* get a change output now! */
assert(amount_sat_eq(change_amount(*change, feerate_per_kw,
weight),
ld->emergency_sat));
return true;
}
static struct command_result *json_fundpsbt(struct command *cmd,
const char *buffer,
const jsmntok_t *obj UNNEEDED,
@ -447,6 +494,7 @@ static struct command_result *json_fundpsbt(struct command *cmd,
struct amount_sat *amount, input, diff, change;
bool all, *excess_as_change, *nonwrapped;
u32 *locktime, *reserve, maxheight;
u32 current_height;
if (!param(cmd, buffer, params,
p_req("satoshi", param_sat_or_all, &amount),
@ -468,6 +516,8 @@ static struct command_result *json_fundpsbt(struct command *cmd,
all = amount_sat_eq(*amount, AMOUNT_SAT(-1ULL));
maxheight = minconf_to_maxheight(*minconf, cmd->ld);
current_height = get_block_height(cmd->ld->topology);
/* We keep adding until we meet their output requirements. */
utxos = tal_arr(cmd, struct utxo *, 0);
@ -479,7 +529,7 @@ static struct command_result *json_fundpsbt(struct command *cmd,
u32 utxo_weight;
utxo = wallet_find_utxo(utxos, cmd->ld->wallet,
cmd->ld->topology->tip->height,
current_height,
&diff,
*feerate_per_kw,
maxheight,
@ -556,6 +606,17 @@ static struct command_result *json_fundpsbt(struct command *cmd,
change = AMOUNT_SAT(0);
}
/* If needed, add change output for emergency_sat */
if (!change_for_emergency(cmd->ld,
have_anchor_channel(cmd->ld),
utxos, *feerate_per_kw, *weight,
&diff, &change)) {
return command_fail(cmd, FUND_CANNOT_AFFORD_WITH_EMERGENCY,
"We would not have enough left for min-emergency-msat %s",
fmt_amount_sat(tmpctx,
cmd->ld->emergency_sat));
}
return finish_psbt(cmd, utxos, *feerate_per_kw, *weight, diff, *reserve,
locktime, change);
}
@ -724,6 +785,17 @@ static struct command_result *json_utxopsbt(struct command *cmd,
change = AMOUNT_SAT(0);
}
/* If needed, add change output for emergency_sat */
if (!change_for_emergency(cmd->ld,
have_anchor_channel(cmd->ld),
utxos, *feerate_per_kw, *weight,
&excess, &change)) {
return command_fail(cmd, FUND_CANNOT_AFFORD_WITH_EMERGENCY,
"We would not have enough left for min-emergency-msat %s",
fmt_amount_sat(tmpctx,
cmd->ld->emergency_sat));
}
return finish_psbt(cmd, utxos, *feerate_per_kw, *weight, excess,
*reserve, locktime, change);
}

View File

@ -604,6 +604,69 @@ struct utxo *wallet_find_utxo(const tal_t *ctx, struct wallet *w,
return utxo;
}
bool wallet_has_funds(struct wallet *w,
const struct utxo **excludes,
u32 current_blockheight,
struct amount_sat sats)
{
struct db_stmt *stmt;
struct amount_sat total = AMOUNT_SAT(0);
stmt = db_prepare_v2(w->db, SQL("SELECT"
" prev_out_tx"
", prev_out_index"
", value"
", type"
", status"
", keyindex"
", channel_id"
", peer_id"
", commitment_point"
", option_anchor_outputs"
", confirmation_height"
", spend_height"
", scriptpubkey "
", reserved_til"
", csv_lock"
", is_in_coinbase"
" FROM outputs"
" WHERE status = ?"
" OR (status = ? AND reserved_til <= ?)"));
db_bind_int(stmt, 0, output_status_in_db(OUTPUT_STATE_AVAILABLE));
db_bind_int(stmt, 1, output_status_in_db(OUTPUT_STATE_RESERVED));
db_bind_u64(stmt, 2, current_blockheight);
db_query_prepared(stmt);
while (db_step(stmt)) {
struct utxo *utxo = wallet_stmt2output(tmpctx, stmt);
if (excluded(excludes, utxo)
|| !deep_enough(-1U, utxo, current_blockheight)) {
continue;
}
/* Overflow Should Not Happen */
if (!amount_sat_add(&total, total, utxo->amount)) {
db_fatal("Invalid value for %s: %s",
type_to_string(tmpctx,
struct bitcoin_outpoint,
&utxo->outpoint),
fmt_amount_sat(tmpctx, utxo->amount));
}
/* If we've found enough, answer is yes. */
if (amount_sat_greater_eq(total, sats)) {
tal_free(stmt);
return true;
}
}
/* Insufficient funds! */
tal_free(stmt);
return false;
}
bool wallet_add_onchaind_utxo(struct wallet *w,
const struct bitcoin_outpoint *outpoint,
const u8 *scriptpubkey,

View File

@ -482,6 +482,20 @@ struct utxo *wallet_find_utxo(const tal_t *ctx, struct wallet *w,
bool nonwrapped,
const struct utxo **excludes);
/**
* wallet_has_funds: do we have sufficient other UTXOs for this amount?
* @w: the wallet
* @excludes: the utxos not to count (tal_arr or NULL)
* @current_blockheight: current chain length.
* @sats: the target
*
* This is a gross estimate, since it doesn't take into account the fees we
* would need to actually spend these utxos!
*/
bool wallet_has_funds(struct wallet *wallet,
const struct utxo **excludes,
u32 current_blockheight,
struct amount_sat sats);
/**
* wallet_add_onchaind_utxo - Add a UTXO with spending info from onchaind.
*