diff --git a/plugins/bkpr/bookkeeper.c b/plugins/bkpr/bookkeeper.c index 88f7f406b..3da169e41 100644 --- a/plugins/bkpr/bookkeeper.c +++ b/plugins/bkpr/bookkeeper.c @@ -78,6 +78,149 @@ static void json_add_onchain_fee(struct json_stream *out, json_object_end(out); } +static struct fee_sum *find_sum_for_txid(struct fee_sum **sums, + struct bitcoin_txid *txid) +{ + for (size_t i = 0; i < tal_count(sums); i++) { + if (bitcoin_txid_eq(txid, sums[i]->txid)) + return sums[i]; + } + return NULL; +} + +static struct command_result *json_inspect(struct command *cmd, + const char *buf, + const jsmntok_t *params) +{ + struct json_stream *res; + struct account *acct; + const char *acct_name; + struct fee_sum **fee_sums; + struct txo_set **txos; + + /* Only available for channel accounts? */ + if (!param(cmd, buf, params, + p_opt("account", param_string, &acct_name), + NULL)) + return command_param_failed(); + + if (!acct_name) + return command_fail(cmd, PLUGIN_ERROR, + "Account not provided"); + + if (streq(acct_name, WALLET_ACCT) + || streq(acct_name, EXTERNAL_ACCT)) + return command_fail(cmd, PLUGIN_ERROR, + "`inspect` not supported for" + " non-channel accounts"); + + db_begin_transaction(db); + acct = find_account(cmd, db, acct_name); + db_commit_transaction(db); + + if (!acct) + return command_fail(cmd, PLUGIN_ERROR, + "Account %s not found", + acct_name); + + db_begin_transaction(db); + find_txo_chain(cmd, db, acct, &txos); + fee_sums = find_account_onchain_fees(cmd, db, acct); + db_commit_transaction(db); + + res = jsonrpc_stream_success(cmd); + json_array_start(res, "txs"); + for (size_t i = 0; i < tal_count(txos); i++) { + struct txo_set *set = txos[i]; + struct fee_sum *fee_sum; + + json_object_start(res, NULL); + json_add_txid(res, "txid", set->txid); + + /* annoyting, but we can only add the block height + * if we have a txo for it */ + for (size_t j = 0; j < tal_count(set->pairs); j++) { + if (set->pairs[j]->txo + && set->pairs[j]->txo->blockheight > 0) { + json_add_num(res, "blockheight", + set->pairs[j]->txo->blockheight); + break; + } + } + + fee_sum = find_sum_for_txid(fee_sums, set->txid); + if (fee_sum) + json_add_amount_msat_only(res, "fees_paid", + fee_sum->fees_paid); + else + json_add_amount_msat_only(res, "fees_paid", + AMOUNT_MSAT(0)); + + json_array_start(res, "outputs"); + for (size_t j = 0; j < tal_count(set->pairs); j++) { + struct txo_pair *pr = set->pairs[j]; + + /* Is this an event that belongs to this account? */ + if (pr->txo) { + if (pr->txo->origin_acct) { + if (!streq(pr->txo->origin_acct, acct->name)) + continue; + } else if (pr->txo->acct_db_id != acct->db_id + /* We make an exception for wallet events */ + && !streq(pr->txo->acct_name, WALLET_ACCT)) + continue; + } else if (pr->spend + && pr->spend->acct_db_id != acct->db_id) + continue; + + json_object_start(res, NULL); + if (set->pairs[j]->txo) { + struct chain_event *ev = set->pairs[j]->txo; + + json_add_string(res, "account", ev->acct_name); + json_add_num(res, "outnum", + ev->outpoint.n); + json_add_string(res, "output_tag", ev->tag); + json_add_amount_msat_only(res, "output_value", + ev->output_value); + json_add_amount_msat_only(res, "credit", + ev->credit); + json_add_string(res, "currency", ev->currency); + if (ev->origin_acct) + json_add_string(res, "originating_account", + ev->origin_acct); + } + if (set->pairs[j]->spend) { + struct chain_event *ev = set->pairs[j]->spend; + /* If we didn't already populate this info */ + if (!set->pairs[j]->txo) { + json_add_string(res, "account", + ev->acct_name); + json_add_num(res, "outnum", + ev->outpoint.n); + json_add_amount_msat_only(res, "output_value", + ev->output_value); + json_add_string(res, "currency", + ev->currency); + } + json_add_string(res, "spend_tag", ev->tag); + json_add_txid(res, "spending_txid", + ev->spending_txid); + json_add_amount_msat_only(res, "debit", ev->debit); + if (ev->payment_id) + json_add_sha256(res, "payment_id", + ev->payment_id); + } + json_object_end(res); + } + json_array_end(res); + json_object_end(res); + } + json_array_end(res); + + return command_finished(cmd, res); +} + /* Find all the events for this account, ordered by timestamp */ static struct command_result *json_list_account_events(struct command *cmd, const char *buf, @@ -1115,6 +1258,13 @@ static const struct plugin_command commands[] = { " no account specified) in {format}. Sorted by timestamp", json_list_account_events }, + { + "inspect", + "utilities", + "See the current on-chain graph of an {account}", + "Prints out the on-chain footprint of a given {account}.", + json_inspect + }, }; static const char *init(struct plugin *p, const char *b, const jsmntok_t *t) diff --git a/plugins/bkpr/recorder.c b/plugins/bkpr/recorder.c index 929e2628a..c03e4824a 100644 --- a/plugins/bkpr/recorder.c +++ b/plugins/bkpr/recorder.c @@ -160,6 +160,231 @@ struct chain_event **account_get_chain_events(const tal_t *ctx, return find_chain_events(ctx, take(stmt)); } +static struct chain_event **find_txos_for_tx(const tal_t *ctx, + struct db *db, + struct bitcoin_txid *txid) +{ + struct db_stmt *stmt; + + stmt = db_prepare_v2(db, SQL("SELECT" + " e.id" + ", e.account_id" + ", a.name" + ", e.origin" + ", e.tag" + ", e.credit" + ", e.debit" + ", e.output_value" + ", e.currency" + ", e.timestamp" + ", e.blockheight" + ", e.utxo_txid" + ", e.outnum" + ", e.spending_txid" + ", e.payment_id" + " FROM chain_events e" + " LEFT OUTER JOIN accounts a" + " ON e.account_id = a.id" + " WHERE e.utxo_txid = ?" + " ORDER BY " + " e.utxo_txid" + ", e.outnum" + ", e.spending_txid NULLS FIRST")); + + db_bind_txid(stmt, 0, txid); + return find_chain_events(ctx, take(stmt)); +} + +struct fee_sum **find_account_onchain_fees(const tal_t *ctx, + struct db *db, + struct account *acct) +{ + struct db_stmt *stmt; + struct fee_sum **sums; + stmt = db_prepare_v2(db, SQL("SELECT" + " txid" + ", CAST(SUM(credit) AS BIGINT) as credit" + ", CAST(SUM(debit) AS BIGINT) as debit" + " FROM onchain_fees" + " WHERE account_id = ?" + " GROUP BY txid" + " ORDER BY txid, update_count")); + + db_bind_u64(stmt, 0, acct->db_id); + db_query_prepared(stmt); + + sums = tal_arr(ctx, struct fee_sum *, 0); + while (db_step(stmt)) { + struct fee_sum *sum; + struct amount_msat amt; + bool ok; + + sum = tal(sums, struct fee_sum); + sum->txid = tal(sum, struct bitcoin_txid); + db_col_txid(stmt, "txid", sum->txid); + + db_col_amount_msat(stmt, "credit", &sum->fees_paid); + db_col_amount_msat(stmt, "debit", &amt); + ok = amount_msat_sub(&sum->fees_paid, sum->fees_paid, amt); + assert(ok); + tal_arr_expand(&sums, sum); + } + + return sums; +} + +static struct txo_pair *new_txo_pair(const tal_t *ctx) +{ + struct txo_pair *pr = tal(ctx, struct txo_pair); + pr->txo = NULL; + pr->spend = NULL; + return pr; +} + +static struct txo_set *find_txo_set(const tal_t *ctx, + struct db *db, + struct bitcoin_txid *txid, + u64 *acct_db_id, + bool *is_complete) +{ + struct txo_pair *pr; + struct chain_event **evs; + struct txo_set *txos = tal(ctx, struct txo_set); + + /* In some special cases (the opening tx), we only + * want the outputs that pertain to a given account, + * most other times we want all utxos, regardless of account */ + evs = find_txos_for_tx(ctx, db, txid); + txos->pairs = tal_arr(txos, struct txo_pair *, 0); + txos->txid = tal_dup(txos, struct bitcoin_txid, txid); + + pr = NULL; + + /* If there's nothing for this txid, we're missing data */ + if (is_complete) + *is_complete = tal_count(evs) > 0; + + for (size_t i = 0; i < tal_count(evs); i++) { + struct chain_event *ev = evs[i]; + + if (acct_db_id && ev->acct_db_id != *acct_db_id) + continue; + + if (ev->spending_txid) { + if (!pr) { + /* We're missing data!! */ + pr = new_txo_pair(txos->pairs); + if (is_complete) + *is_complete = false; + } else { + assert(pr->txo); + /* Make sure it's the same txo */ + assert(bitcoin_outpoint_eq(&pr->txo->outpoint, + &ev->outpoint)); + } + + pr->spend = tal_steal(pr, ev); + tal_arr_expand(&txos->pairs, pr); + pr = NULL; + } else { + /* We might not have a spend event + * for everything */ + if (pr) + tal_arr_expand(&txos->pairs, pr); + pr = new_txo_pair(txos->pairs); + pr->txo = tal_steal(pr, ev); + } + } + + /* Might have a single entry 'pr' left over */ + if (pr) + tal_arr_expand(&txos->pairs, pr); + + return txos; +} + +static bool is_channel_acct(struct chain_event *ev) +{ + return !streq(ev->acct_name, WALLET_ACCT) + && !streq(ev->acct_name, EXTERNAL_ACCT); +} + +static bool txid_in_list(struct bitcoin_txid **list, + struct bitcoin_txid *txid) +{ + for (size_t i = 0; i < tal_count(list); i++) { + if (bitcoin_txid_eq(list[i], txid)) + return true; + } + + return false; +} + +bool find_txo_chain(const tal_t *ctx, + struct db *db, + struct account *acct, + struct txo_set ***sets) +{ + struct bitcoin_txid **txids; + struct chain_event *open_ev; + bool is_complete = true; + u64 *start_acct_id = tal(NULL, u64); + + assert(acct->open_event_db_id); + open_ev = find_chain_event_by_id(ctx, db, + *acct->open_event_db_id); + + *sets = tal_arr(ctx, struct txo_set *, 0); + txids = tal_arr(ctx, struct bitcoin_txid *, 0); + tal_arr_expand(&txids, &open_ev->outpoint.txid); + + /* We only want to filter by the account for the very + * first utxo that we get the tree for, so we + * start w/ this acct id... */ + *start_acct_id = open_ev->acct_db_id; + + for (size_t i = 0; i < tal_count(txids); i++) { + struct txo_set *set; + bool set_complete; + + set = find_txo_set(ctx, db, txids[i], + start_acct_id, + &set_complete); + + /* After first use, we free the acct dbid ptr, + * which will pass in NULL and not filter by + * account for any subsequent txo_set hunt */ + if (start_acct_id) + start_acct_id = tal_free(start_acct_id); + + is_complete &= set_complete; + for (size_t j = 0; j < tal_count(set->pairs); j++) { + struct txo_pair *pr = set->pairs[j]; + + /* Has this been resolved? */ + if ((pr->txo + && is_channel_acct(pr->txo)) + && !pr->spend) + is_complete = false; + + /* wallet accts and zero-fee-htlc anchors + * might overlap txids */ + if (pr->spend + && pr->spend->spending_txid + && !txid_in_list(txids, pr->spend->spending_txid) + /* We dont trace utxos for non related accts */ + && pr->spend->acct_db_id == acct->db_id) { + tal_arr_expand(&txids, + pr->spend->spending_txid); + } + } + + tal_arr_expand(sets, set); + } + + return is_complete; +} + struct chain_event *find_chain_event_by_id(const tal_t *ctx, struct db *db, u64 event_db_id) diff --git a/plugins/bkpr/recorder.h b/plugins/bkpr/recorder.h index 0d2cdfad4..e1468c76f 100644 --- a/plugins/bkpr/recorder.h +++ b/plugins/bkpr/recorder.h @@ -22,6 +22,21 @@ struct acct_balance { struct amount_msat balance; }; +struct fee_sum { + struct bitcoin_txid *txid; + struct amount_msat fees_paid; +}; + +struct txo_pair { + struct chain_event *txo; + struct chain_event *spend; +}; + +struct txo_set { + struct bitcoin_txid *txid; + struct txo_pair **pairs; +}; + /* Get all accounts */ struct account **list_accounts(const tal_t *ctx, struct db *db); @@ -65,9 +80,25 @@ struct chain_event *find_chain_event_by_id(const tal_t *ctx, struct db *db, u64 event_db_id); +/* Find the utxos for this account. + * + * Returns true if chain is complete: + * (all outputs terminate either to wallet or external) + */ +bool find_txo_chain(const tal_t *ctx, + struct db *db, + struct account *acct, + struct txo_set ***sets); + /* List all chain fees, for all accounts */ struct onchain_fee **list_chain_fees(const tal_t *ctx, struct db *db); +/* Returns a list of sums of the fees we've recorded for every txid + * for the given account */ +struct fee_sum **find_account_onchain_fees(const tal_t *ctx, + struct db *db, + struct account *acct); + /* Add the given account to the database */ void account_add(struct db *db, struct account *acct); /* Given an account name, find that account record */