bkpr: add an 'inspect' command to the bookkeeper

Pass in an account id, get out a utxo chain of the channel open and
close (and any other related htlc txs etc).

Note that this prints all wallet deposits that occurred in any of the
tx's that touched this channel.

This is fine and expected for any tx that's not the open; when
considerig the tx open event, the wallet deposit that's present is
typically the change. If there were other channels opened in the same tx
then the change won't match up exactly...
This commit is contained in:
niftynei 2022-07-19 17:04:36 +09:30 committed by Rusty Russell
parent fea33221d4
commit b33bd05524
3 changed files with 406 additions and 0 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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 */