diff --git a/plugins/bkpr/Makefile b/plugins/bkpr/Makefile index f7c53e1a0..7e7730e79 100644 --- a/plugins/bkpr/Makefile +++ b/plugins/bkpr/Makefile @@ -6,6 +6,7 @@ BOOKKEEPER_PLUGIN_SRC := \ plugins/bkpr/bookkeeper.c \ plugins/bkpr/chain_event.c \ plugins/bkpr/channel_event.c \ + plugins/bkpr/channelsapy.c \ plugins/bkpr/db.c \ plugins/bkpr/incomestmt.c \ plugins/bkpr/onchain_fee.c \ @@ -21,6 +22,7 @@ BOOKKEEPER_HEADER := \ plugins/bkpr/account_entry.h \ plugins/bkpr/chain_event.h \ plugins/bkpr/channel_event.h \ + plugins/bkpr/channelsapy.h \ plugins/bkpr/db.h \ plugins/bkpr/incomestmt.h \ plugins/bkpr/onchain_fee.h \ diff --git a/plugins/bkpr/bookkeeper.c b/plugins/bkpr/bookkeeper.c index 1f2defd47..a9ebdd65c 100644 --- a/plugins/bkpr/bookkeeper.c +++ b/plugins/bkpr/bookkeeper.c @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +42,55 @@ static struct fee_sum *find_sum_for_txid(struct fee_sum **sums, return NULL; } +static struct command_result *json_channel_apy(struct command *cmd, + const char *buf, + const jsmntok_t *params) +{ + struct json_stream *res; + struct channel_apy **apys, *net_apys; + u64 *start_time, *end_time; + + if (!param(cmd, buf, params, + p_opt_def("start_time", param_u64, &start_time, 0), + p_opt_def("end_time", param_u64, &end_time, SQLITE_MAX_UINT), + NULL)) + return command_param_failed(); + + /* Get the income events */ + db_begin_transaction(db); + apys = compute_channel_apys(cmd, db, *start_time, *end_time, + /* FIXME: current blockheight */ + 1414); + db_commit_transaction(db); + + /* Setup the net_apys entry */ + net_apys = new_channel_apy(cmd); + net_apys->end_blockheight = 0; + net_apys->start_blockheight = UINT_MAX; + net_apys->our_start_bal = AMOUNT_MSAT(0); + net_apys->total_start_bal = AMOUNT_MSAT(0); + + res = jsonrpc_stream_success(cmd); + json_array_start(res, "channel_apys"); + for (size_t i = 0; i < tal_count(apys); i++) { + json_add_channel_apy(res, apys[i]); + + /* Add to net/rollup APY */ + if (!channel_apy_sum(net_apys, apys[i])) + return command_fail(cmd, PLUGIN_ERROR, + "Overflow adding APYs net"); + } + + /* Append a net/rollup entry */ + if (!amount_msat_zero(net_apys->total_start_bal)) { + net_apys->acct_name = tal_fmt(net_apys, "net"); + json_add_channel_apy(res, net_apys); + } + json_array_end(res); + + return command_finished(cmd, res); +} + static struct command_result *param_csv_format(struct command *cmd, const char *name, const char *buffer, const jsmntok_t *tok, struct csv_fmt **csv_fmt) @@ -1385,6 +1435,13 @@ static const struct plugin_command commands[] = { " (default: true)", json_dump_income }, + { + "channelsapy", + "bookkeeping", + "Stats on channel fund usage", + "Print out stats on chanenl fund usage", + json_channel_apy + }, }; static const char *init(struct plugin *p, const char *b, const jsmntok_t *t) diff --git a/plugins/bkpr/channelsapy.c b/plugins/bkpr/channelsapy.c new file mode 100644 index 000000000..429fbffcb --- /dev/null +++ b/plugins/bkpr/channelsapy.c @@ -0,0 +1,413 @@ +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define BLOCK_YEAR 52364 + +static int cmp_channel_event_acct(struct channel_event *const *ev1, + struct channel_event *const *ev2, + void *unused UNUSED) +{ + if ((*ev1)->acct_db_id < (*ev2)->acct_db_id) + return -1; + else if ((*ev1)->acct_db_id > (*ev2)->acct_db_id) + return 1; + return 0; +} + +static int cmp_acct(struct account *const *a1, + struct account *const *a2, + void *unused UNUSED) +{ + if ((*a1)->db_id < (*a2)->db_id) + return -1; + else if ((*a1)->db_id > (*a2)->db_id) + return 1; + return 0; +} + +struct channel_apy *new_channel_apy(const tal_t *ctx) +{ + struct channel_apy *apy = tal(ctx, struct channel_apy); + + apy->routed_in = AMOUNT_MSAT(0); + apy->routed_out = AMOUNT_MSAT(0); + apy->fees_in = AMOUNT_MSAT(0); + apy->fees_out = AMOUNT_MSAT(0); + apy->push_in = AMOUNT_MSAT(0); + apy->push_out = AMOUNT_MSAT(0); + apy->lease_in = AMOUNT_MSAT(0); + apy->lease_out = AMOUNT_MSAT(0); + return apy; +} + +bool channel_apy_sum(struct channel_apy *sum_apy, + const struct channel_apy *entry) +{ + bool ok; + ok = amount_msat_add(&sum_apy->routed_in, + sum_apy->routed_in, + entry->routed_in); + ok &= amount_msat_add(&sum_apy->routed_out, + sum_apy->routed_out, + entry->routed_out); + ok &= amount_msat_add(&sum_apy->fees_in, + sum_apy->fees_in, + entry->fees_in); + ok &= amount_msat_add(&sum_apy->fees_out, + sum_apy->fees_out, + entry->fees_out); + ok &= amount_msat_add(&sum_apy->push_in, + sum_apy->push_in, + entry->push_in); + ok &= amount_msat_add(&sum_apy->push_out, + sum_apy->push_out, + entry->push_out); + ok &= amount_msat_add(&sum_apy->lease_in, + sum_apy->lease_in, + entry->lease_in); + ok &= amount_msat_add(&sum_apy->lease_out, + sum_apy->lease_out, + entry->lease_out); + + ok &= amount_msat_add(&sum_apy->our_start_bal, + sum_apy->our_start_bal, + entry->our_start_bal); + + ok &= amount_msat_add(&sum_apy->total_start_bal, + sum_apy->total_start_bal, + entry->total_start_bal); + + if (sum_apy->start_blockheight > entry->start_blockheight) + sum_apy->start_blockheight = entry->start_blockheight; + + if (sum_apy->end_blockheight < entry->end_blockheight) + sum_apy->end_blockheight = entry->end_blockheight; + + return ok; +} + +static struct account *search_account(struct account **accts, u64 acct_id) +{ + for (size_t i = 0; i < tal_count(accts); i++) { + if (accts[i]->db_id == acct_id) + return accts[i]; + } + + return NULL; +} + +static void fillin_apy_acct_details(struct db *db, + const struct account *acct, + u32 current_blockheight, + struct channel_apy *apy) +{ + struct chain_event *ev; + bool ok; + + apy->acct_name = tal_strdup(apy, acct->name); + + assert(acct->open_event_db_id); + ev = find_chain_event_by_id(acct, db, *acct->open_event_db_id); + assert(ev); + + apy->start_blockheight = ev->blockheight; + apy->our_start_bal = ev->credit; + apy->total_start_bal = ev->output_value; + + /* if this account is closed, add closing blockheight */ + if (acct->closed_event_db_id) { + ev = find_chain_event_by_id(acct, db, + *acct->closed_event_db_id); + assert(ev); + apy->end_blockheight = ev->blockheight; + } else + apy->end_blockheight = current_blockheight; + + /* If there is any push_out or lease_fees_out, we subtract + * from starting balance */ + ok = amount_msat_sub(&apy->our_start_bal, apy->our_start_bal, + apy->push_out); + assert(ok); + ok = amount_msat_sub(&apy->our_start_bal, apy->our_start_bal, + apy->lease_out); + assert(ok); + + /* we add values in to starting balance */ + ok = amount_msat_add(&apy->our_start_bal, apy->our_start_bal, + apy->push_in); + assert(ok); + ok = amount_msat_add(&apy->our_start_bal, apy->our_start_bal, + apy->lease_in); + assert(ok); +} + +struct channel_apy **compute_channel_apys(const tal_t *ctx, struct db *db, + u64 start_time, + u64 end_time, + u32 current_blockheight) +{ + struct channel_event **evs; + struct channel_apy *apy, **apys; + struct account *acct, **accts; + + evs = list_channel_events_timebox(ctx, db, start_time, end_time); + accts = list_accounts(ctx, db); + + apys = tal_arr(ctx, struct channel_apy *, 0); + + /* Sort events by acct_name */ + asort(evs, tal_count(evs), cmp_channel_event_acct, NULL); + /* Sort accounts by name also */ + asort(accts, tal_count(accts), cmp_acct, NULL); + + acct = NULL; + apy = new_channel_apy(apys); + for (size_t i = 0; i < tal_count(evs); i++) { + struct channel_event *ev = evs[i]; + bool ok; + + if (!acct || acct->db_id != ev->acct_db_id) { + if (acct && is_channel_account(acct)) { + fillin_apy_acct_details(db, acct, + current_blockheight, + apy); + /* Save current apy, make new */ + tal_arr_expand(&apys, apy); + apy = new_channel_apy(apys); + } + acct = search_account(accts, ev->acct_db_id); + assert(acct); + } + + /* No entry for external or wallet accts */ + if (!is_channel_account(acct)) + continue; + + /* Accumulate routing stats */ + if (streq("routed", ev->tag) + || streq("invoice", ev->tag)) { + ok = amount_msat_add(&apy->routed_in, + apy->routed_in, + ev->credit); + assert(ok); + ok = amount_msat_add(&apy->routed_out, + apy->routed_out, + ev->debit); + assert(ok); + + /* No fees for invoices */ + if (streq("invoice", ev->tag)) + continue; + + if (!amount_msat_zero(ev->credit)) + ok = amount_msat_add(&apy->fees_in, + apy->fees_in, + ev->fees); + else + ok = amount_msat_add(&apy->fees_out, + apy->fees_out, + ev->fees); + assert(ok); + } + else if (streq("pushed", ev->tag)) { + ok = amount_msat_add(&apy->push_in, + apy->push_in, + ev->credit); + assert(ok); + ok = amount_msat_add(&apy->push_out, + apy->push_out, + ev->debit); + assert(ok); + } else if (streq("lease_fee", ev->tag)) { + ok = amount_msat_add(&apy->lease_in, + apy->lease_in, + ev->credit); + assert(ok); + ok = amount_msat_add(&apy->lease_out, + apy->lease_out, + ev->debit); + assert(ok); + } + + /* Note: we ignore 'journal_entry's because there's no + * relevant fee data attached to them */ + } + + if (acct && is_channel_account(acct)) { + fillin_apy_acct_details(db, acct, + current_blockheight, + apy); + /* Save current apy, make new */ + tal_arr_expand(&apys, apy); + } + + return apys; +} + +WARN_UNUSED_RESULT static bool calc_apy(struct amount_msat earned, + struct amount_msat capital, + u32 blocks_elapsed, + double *result) +{ + double apy; + + assert(!amount_msat_zero(capital)); + assert(blocks_elapsed > 0); + + apy = amount_msat_ratio(earned, capital) * BLOCK_YEAR / blocks_elapsed; + + /* convert to percent */ + apy *= 100; + + /* If mantissa is < 64 bits, a naive "if (scaled > + * UINT64_MAX)" doesn't work. Stick to powers of 2. */ + if (apy >= (double)((u64)1 << 63) * 2) + return false; + + *result = apy; + return true; +} + +void json_add_channel_apy(struct json_stream *res, + const struct channel_apy *apy) +{ + bool ok; + u32 blocks_elapsed; + double apy_result, utilization; + struct amount_msat total_fees, their_start_bal; + + ok = amount_msat_sub(&their_start_bal, apy->total_start_bal, + apy->our_start_bal); + assert(ok); + + json_object_start(res, NULL); + + json_add_string(res, "account", apy->acct_name); + + json_add_amount_msat_only(res, "routed_out", apy->routed_out); + json_add_amount_msat_only(res, "routed_in", apy->routed_in); + json_add_amount_msat_only(res, "lease_fee_paid", apy->lease_out); + json_add_amount_msat_only(res, "lease_fee_earned", apy->lease_in); + json_add_amount_msat_only(res, "pushed_out", apy->push_out); + json_add_amount_msat_only(res, "pushed_in", apy->push_in); + + json_add_amount_msat_only(res, "our_start_balance", apy->our_start_bal); + json_add_amount_msat_only(res, "channel_start_balance", + apy->total_start_bal); + + ok = amount_msat_add(&total_fees, apy->fees_in, apy->fees_out); + assert(ok); + json_add_amount_msat_only(res, "fees_out", apy->fees_out); + json_add_amount_msat_only(res, "fees_in", apy->fees_in); + + /* utilization (out): routed_out/total_balance */ + assert(!amount_msat_zero(apy->total_start_bal)); + utilization = amount_msat_ratio(apy->routed_out, apy->total_start_bal); + json_add_string(res, "utilization_out", + tal_fmt(apy, "%.4f%%", utilization * 100)); + + if (!amount_msat_zero(apy->our_start_bal)) { + utilization = amount_msat_ratio(apy->routed_out, + apy->our_start_bal); + json_add_string(res, "utilization_out_initial", + tal_fmt(apy, "%.4f%%", utilization * 100)); + } + + /* utilization (in): routed_in/total_balance */ + utilization = amount_msat_ratio(apy->routed_in, apy->total_start_bal); + json_add_string(res, "utilization_in", + tal_fmt(apy, "%.4f%%", utilization * 100)); + + if (!amount_msat_zero(their_start_bal)) { + utilization = amount_msat_ratio(apy->routed_in, + their_start_bal); + json_add_string(res, "utilization_in_initial", + tal_fmt(apy, "%.4f%%", utilization * 100)); + } + + blocks_elapsed = apy->end_blockheight - apy->start_blockheight; + assert(blocks_elapsed > 0); + + /* APY (outbound) */ + ok = calc_apy(apy->fees_out, apy->total_start_bal, + blocks_elapsed, &apy_result); + assert(ok); + json_add_string(res, "apy_out", tal_fmt(apy, "%.4f%%", apy_result)); + + /* APY (outbound, initial) */ + if (!amount_msat_zero(apy->our_start_bal)) { + ok = calc_apy(apy->fees_out, apy->our_start_bal, + blocks_elapsed, &apy_result); + assert(ok); + json_add_string(res, "apy_out_initial", + tal_fmt(apy, "%.4f%%", apy_result)); + } + + /* APY (inbound) */ + ok = calc_apy(apy->fees_in, apy->total_start_bal, + blocks_elapsed, &apy_result); + assert(ok); + json_add_string(res, "apy_in", tal_fmt(apy, "%.4f%%", apy_result)); + + if (!amount_msat_zero(their_start_bal)) { + ok = calc_apy(apy->fees_in, their_start_bal, + blocks_elapsed, &apy_result); + assert(ok); + json_add_string(res, "apy_in_initial", + tal_fmt(apy, "%.4f%%", apy_result)); + } + + /* APY (total) */ + ok = calc_apy(total_fees, apy->total_start_bal, + blocks_elapsed, &apy_result); + assert(ok); + json_add_string(res, "apy_total", tal_fmt(apy, "%.4f%%", apy_result)); + + if (!amount_msat_zero(apy->our_start_bal)) { + ok = calc_apy(total_fees, apy->total_start_bal, + blocks_elapsed, &apy_result); + assert(ok); + json_add_string(res, "apy_total_initial", + tal_fmt(apy, "%.4f%%", apy_result)); + } + + /* If you earned fees for leasing funds, calculate APY + * Note that this is a bit higher than it *should* be, + * given that the onchainfees are partly covered here */ + if (!amount_msat_zero(apy->lease_in)) { + struct amount_msat start_no_lease_in; + + /* We added the lease in to the starting balance, so we + * should subtract it out again before finding APY */ + ok = amount_msat_sub(&start_no_lease_in, + apy->our_start_bal, + apy->lease_in); + assert(ok); + ok = calc_apy(apy->lease_in, start_no_lease_in, + /* we use the lease rate duration here! */ + LEASE_RATE_DURATION, &apy_result); + assert(ok); + json_add_string(res, "apy_lease", + tal_fmt(apy, "%.4f%%", apy_result)); + } + + json_object_end(res); +} + diff --git a/plugins/bkpr/channelsapy.h b/plugins/bkpr/channelsapy.h new file mode 100644 index 000000000..1e4ccef2d --- /dev/null +++ b/plugins/bkpr/channelsapy.h @@ -0,0 +1,43 @@ +#ifndef LIGHTNING_PLUGINS_BKPR_CHANNELSAPY_H +#define LIGHTNING_PLUGINS_BKPR_CHANNELSAPY_H +#include "config.h" + +#include + +struct channel_apy { + char *acct_name; + + struct amount_msat routed_in; + struct amount_msat routed_out; + struct amount_msat fees_in; + struct amount_msat fees_out; + + struct amount_msat push_in; + struct amount_msat push_out; + struct amount_msat lease_in; + struct amount_msat lease_out; + + struct amount_msat our_start_bal; + struct amount_msat total_start_bal; + + /* Blockheight the channel opened */ + u32 start_blockheight; + + /* If channel_close, the channel_close event's blockheight, + * otherwise the current blockheight */ + u32 end_blockheight; +}; + +struct channel_apy *new_channel_apy(const tal_t *ctx); + +WARN_UNUSED_RESULT bool channel_apy_sum(struct channel_apy *sum_apy, + const struct channel_apy *entry); + +struct channel_apy **compute_channel_apys(const tal_t *ctx, struct db *db, + u64 start_time, + u64 end_time, + u32 current_blockheight); + +void json_add_channel_apy(struct json_stream *res, + const struct channel_apy *apy); +#endif /* LIGHTNING_PLUGINS_BKPR_CHANNELSAPY_H */