From f8a44d911dfca71f7eee75134eac3e6f913d1a15 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 19 Aug 2025 10:30:53 +0930 Subject: [PATCH] bkpr: restore run-recorder. This requires us to turn "sql" calls into calls to a local db, which means pulling in a lot of infrastructure. But it's possible. Signed-off-by: Rusty Russell --- plugins/bkpr/sql.h | 7 +- plugins/bkpr/test/Makefile | 1 + plugins/bkpr/test/run-recorder.c | 1677 ++++++++++++++++++++++++++++++ plugins/bkpr/test/run-sql.c | 54 - 4 files changed, 1681 insertions(+), 58 deletions(-) create mode 100644 plugins/bkpr/test/run-recorder.c diff --git a/plugins/bkpr/sql.h b/plugins/bkpr/sql.h index 8cde88ab3..5d2489722 100644 --- a/plugins/bkpr/sql.h +++ b/plugins/bkpr/sql.h @@ -43,10 +43,9 @@ struct command_result; " AND et.extra_tags = 'splice') AS spliced" \ " FROM chainmoves cm " -const jsmntok_t * -PRINTF_FMT(4, 5) sql_req(const tal_t *ctx, - struct command *cmd, const char **buf, - const char *fmt, ...); +const jsmntok_t *PRINTF_FMT(4, 5) sql_req(const tal_t *ctx, + struct command *cmd, const char **buf, + const char *fmt, ...); const jsmntok_t *sql_reqv(const tal_t *ctx, struct command *cmd, const char **buf, diff --git a/plugins/bkpr/test/Makefile b/plugins/bkpr/test/Makefile index 52b92a1f3..80fed1875 100644 --- a/plugins/bkpr/test/Makefile +++ b/plugins/bkpr/test/Makefile @@ -13,6 +13,7 @@ BOOKKEEPER_TEST_COMMON_OBJS := \ common/channel_type.o \ common/coin_mvt.o \ common/features.o \ + common/json_parse.o \ common/json_stream.o \ common/json_parse_simple.o \ common/key_derive.o \ diff --git a/plugins/bkpr/test/run-recorder.c b/plugins/bkpr/test/run-recorder.c new file mode 100644 index 000000000..a8b131e16 --- /dev/null +++ b/plugins/bkpr/test/run-recorder.c @@ -0,0 +1,1677 @@ +#include "config.h" +#include "common/json_filter.c" +#include "test_utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "plugins/bkpr/account.c" +#include "plugins/bkpr/blockheights.c" +#include "plugins/bkpr/channel_event.c" +#include "plugins/bkpr/chain_event.c" +#include "plugins/bkpr/recorder.c" +#include "plugins/bkpr/onchain_fee.c" +#include "plugins/bkpr/rebalances.c" +#include "plugins/bkpr/sql.c" + +#if HAVE_SQLITE3 + #include +#endif + +/* AUTOGENERATED MOCKS START */ +/* Generated stub for chain_event_description */ +const char *chain_event_description(const struct bkpr *bkpr UNNEEDED, + const struct chain_event *ce UNNEEDED) +{ fprintf(stderr, "chain_event_description called!\n"); abort(); } +/* Generated stub for channel_event_description */ +const char *channel_event_description(const struct bkpr *bkpr UNNEEDED, + const struct channel_event *ce UNNEEDED) +{ fprintf(stderr, "channel_event_description called!\n"); abort(); } +/* Generated stub for command_fail_badparam */ +struct command_result *command_fail_badparam(struct command *cmd UNNEEDED, + const char *paramname UNNEEDED, + const char *buffer UNNEEDED, + const jsmntok_t *tok UNNEEDED, + const char *msg UNNEEDED) +{ fprintf(stderr, "command_fail_badparam called!\n"); abort(); } +/* Generated stub for command_filter_ptr */ +struct json_filter **command_filter_ptr(struct command *cmd UNNEEDED) +{ fprintf(stderr, "command_filter_ptr called!\n"); abort(); } +/* Generated stub for first_fee_state */ +enum htlc_state first_fee_state(enum side opener UNNEEDED) +{ fprintf(stderr, "first_fee_state called!\n"); abort(); } +/* Generated stub for fmt_channel_id */ +char *fmt_channel_id(const tal_t *ctx UNNEEDED, const struct channel_id *channel_id UNNEEDED) +{ fprintf(stderr, "fmt_channel_id called!\n"); abort(); } +/* Generated stub for fmt_wireaddr_without_port */ +char *fmt_wireaddr_without_port(const tal_t *ctx UNNEEDED, const struct wireaddr *a UNNEEDED) +{ fprintf(stderr, "fmt_wireaddr_without_port called!\n"); abort(); } +/* Generated stub for fromwire_wireaddr */ +bool fromwire_wireaddr(const u8 **cursor UNNEEDED, size_t *max UNNEEDED, struct wireaddr *addr UNNEEDED) +{ fprintf(stderr, "fromwire_wireaddr called!\n"); abort(); } +/* Generated stub for htlc_state_flags */ +int htlc_state_flags(enum htlc_state state UNNEEDED) +{ fprintf(stderr, "htlc_state_flags called!\n"); abort(); } +/* Generated stub for htlc_state_name */ +const char *htlc_state_name(enum htlc_state s UNNEEDED) +{ fprintf(stderr, "htlc_state_name called!\n"); abort(); } +/* Generated stub for last_fee_state */ +enum htlc_state last_fee_state(enum side opener UNNEEDED) +{ fprintf(stderr, "last_fee_state called!\n"); abort(); } +/* Generated stub for plugin_err */ +void plugin_err(struct plugin *p UNNEEDED, const char *fmt UNNEEDED, ...) +{ fprintf(stderr, "plugin_err called!\n"); abort(); } +/* Generated stub for plugin_log */ +void plugin_log(struct plugin *p UNNEEDED, enum log_level l UNNEEDED, const char *fmt UNNEEDED, ...) +{ fprintf(stderr, "plugin_log called!\n"); abort(); } +/* AUTOGENERATED MOCKS END */ + +static sqlite3 *bkpr_db; + +/* Stolen from old plugins/bkpr/db.c */ +struct migration { + const char *sql; + void (*func)(struct plugin *p, struct db *db); +}; + +static struct migration db_migrations[] = { + {SQL("CREATE TABLE version (version INTEGER);"), NULL}, + {SQL("INSERT INTO version VALUES (1);"), NULL}, + {SQL("CREATE TABLE vars (" + " name TEXT" + ", val TEXT" + ", intval INTEGER" + ", blobval BLOB" + ", PRIMARY KEY (name)" + ");"), + NULL}, + {SQL("INSERT INTO vars (" + " name" + ", intval" + ") VALUES (" + " 'data_version'" + ", 0" + ");"), + NULL}, + {SQL("CREATE TABLE accounts (" + " id INTEGER" + ", name TEXT" + ", peer_id BLOB" + ", opened_event_id INTEGER" + ", closed_event_id INTEGER" + ", onchain_resolved_block INTEGER" + ", is_wallet INTEGER" + ", we_opened INTEGER" + ", leased INTEGER" + ", PRIMARY KEY (id)" + ");"), + NULL}, + {SQL("CREATE TABLE chainmoves (" + " created_index INTEGER" + ", account_id TEXT" + ", primary_tag TEXT" + ", credit_msat INTEGER" + ", debit_msat INTEGER" + ", output_msat INTEGER" + ", timestamp INTEGER" + ", blockheight INTEGER" + ", utxo TEXT" + ", payment_hash BLOB" + ", spending_txid BLOB" + ", PRIMARY KEY (created_index)" + ");"), + NULL}, + {SQL("CREATE TABLE chainmoves_extra_tags (" + " row INTEGER" + ", arrindex INTEGER" + ", extra_tags TEXT)"), NULL}, + {SQL("CREATE TABLE channelmoves (" + " created_index INTEGER" + ", account_id TEXT" + ", primary_tag TEXT" + ", credit_msat INTEGER" + ", debit_msat INTEGER" + ", fees_msat INTEGER" + ", payment_hash BLOB" + ", part_id INTEGER" + ", timestamp INTEGER" + ", PRIMARY KEY (created_index)" + ");"), + NULL}, + {SQL("CREATE TABLE onchain_fees (" + "account_id TEXT" + ", txid BLOB" + ", credit INTEGER" + ", debit INTEGER" + ", currency TEXT" + ", timestamp INTEGER" + ", update_count INT" + ", PRIMARY KEY (account_id, txid, update_count)" + ");"), + NULL}, + {SQL("ALTER TABLE chainmoves ADD originating_account TEXT;"), NULL}, + {SQL("ALTER TABLE accounts ADD closed_count INTEGER DEFAULT 0;"), NULL}, + {SQL("ALTER TABLE chainmoves ADD ignored INTEGER;"), NULL}, + {SQL("ALTER TABLE chainmoves ADD stealable INTEGER;"), NULL}, + {SQL("ALTER TABLE chainmoves ADD ev_desc TEXT DEFAULT NULL;"), NULL}, + {SQL("ALTER TABLE channelmoves ADD ev_desc TEXT DEFAULT NULL;"), NULL}, + {SQL("ALTER TABLE channelmoves ADD rebalance_id INTEGER DEFAULT NULL;"), NULL}, + {SQL("ALTER TABLE chainmoves ADD spliced INTEGER DEFAULT 0;"), NULL}, +}; + +#undef db_begin_transaction +#define db_begin_transaction(db) +#define db_commit_transaction(db) + +static bool db_migrate(struct plugin *p, sqlite3 *db) +{ + sqlite3_stmt *s; + for (size_t i = 0; i < ARRAY_SIZE(db_migrations); i++) { + int rc; + sqlite3_prepare_v2(db, db_migrations[i].sql, -1, &s, NULL); + rc = sqlite3_step(s); + assert(rc == SQLITE_DONE); + sqlite3_finalize(s); + } + return true; +} + +static sqlite3 *db_setup(struct plugin *p, const char *db_dsn) +{ + sqlite3_stmt *stmt; + sqlite3 *sql; + int err; + + err = sqlite3_open_v2(db_dsn, &sql, SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE, NULL); + assert(err == SQLITE_OK); + + err = sqlite3_extended_result_codes(sql, 1); + assert(err == SQLITE_OK); + + sqlite3_busy_timeout(sql, 60000); + sqlite3_prepare_v2(sql, + "PRAGMA foreign_keys = ON;", -1, &stmt, NULL); + err = sqlite3_step(stmt); + assert(err == SQLITE_DONE); + sqlite3_finalize(stmt); + + db_migrate(p, sql); + return sql; +} + +static int sqlite3_bind_outpoint(sqlite3_stmt *stmt, int pos, const struct bitcoin_outpoint *o) +{ + return sqlite3_bind_text(stmt, pos, fmt_bitcoin_outpoint(tmpctx, o), -1, SQLITE_TRANSIENT); +} + +static int sqlite3_bind_txid(sqlite3_stmt *stmt, int pos, const struct bitcoin_txid *txid) +{ + return sqlite3_bind_blob(stmt, pos, txid, sizeof(*txid), SQLITE_TRANSIENT); +} + +static int sqlite3_bind_hash(sqlite3_stmt *stmt, int pos, const struct sha256 *hash) +{ + return sqlite3_bind_blob(stmt, pos, hash, sizeof(*hash), SQLITE_TRANSIENT); +} + +/* Stolen from old plugins/bkpr/recorder.c */ +static bool find_chain_event(sqlite3 *db, + const struct account *acct, + const struct bitcoin_outpoint *outpoint, + const struct bitcoin_txid *spending_txid, + const char *tag) + +{ + sqlite3_stmt *stmt; + bool ret; + int rc; + + if (spending_txid) { + sqlite3_prepare_v2(db, + "SELECT 1" + " FROM chainmoves e" + " LEFT OUTER JOIN accounts a" + " ON e.account_id = a.name" + " WHERE " + " e.spending_txid = ?" + " AND e.account_id = ?" + " AND e.utxo = ?", + -1, &stmt, NULL); + rc = sqlite3_bind_txid(stmt, 1, spending_txid); + assert(rc == SQLITE_OK); + } else { + sqlite3_prepare_v2(db, + "SELECT 1" + " FROM chainmoves e" + " LEFT OUTER JOIN accounts a" + " ON e.account_id = a.name" + " WHERE " + " e.primary_tag = ?" + " AND e.account_id = ?" + " AND e.utxo = ?" + " AND e.spending_txid IS NULL", + -1, &stmt, NULL); + rc = sqlite3_bind_text(stmt, 1, tag, -1, SQLITE_TRANSIENT); + assert(rc == SQLITE_OK); + } + + rc = sqlite3_bind_text(stmt, 2, acct->name, -1, SQLITE_TRANSIENT); + assert(rc == SQLITE_OK); + rc = sqlite3_bind_outpoint(stmt, 3, outpoint); + assert(rc == SQLITE_OK); + + ret = (sqlite3_step(stmt) == SQLITE_ROW); + sqlite3_finalize(stmt); + return ret; +} + +static bool log_chain_event(struct bkpr *bkpr, + const struct account *acct, + struct chain_event *e) +{ + int rc; + sqlite3_stmt *stmt; + + /* We're responsible for de-duping chain events! */ + if (find_chain_event(bkpr_db, acct, + &e->outpoint, e->spending_txid, + e->tag)) + return false; + rc = sqlite3_prepare_v2(bkpr_db, + "INSERT INTO chainmoves" + " (" + " account_id" + ", originating_account" + ", primary_tag" + ", credit_msat" + ", debit_msat" + ", output_msat" + ", timestamp" + ", blockheight" + ", utxo" + ", payment_hash" + ", spending_txid" + ")" + " VALUES " + "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", + -1, &stmt, NULL); + assert(rc == SQLITE_OK); + + rc = sqlite3_bind_text(stmt, 1, acct->name, -1, SQLITE_TRANSIENT); + assert(rc == SQLITE_OK); + if (e->origin_acct) + sqlite3_bind_text(stmt, 2, e->origin_acct, -1, SQLITE_TRANSIENT); + else + sqlite3_bind_null(stmt, 2); + rc = sqlite3_bind_text(stmt, 3, e->tag, -1, SQLITE_TRANSIENT); + assert(rc == SQLITE_OK); + rc = sqlite3_bind_int64(stmt, 4, (sqlite3_int64)e->credit.millisatoshis /* Raw: db */); + assert(rc == SQLITE_OK); + rc = sqlite3_bind_int64(stmt, 5, (sqlite3_int64)e->debit.millisatoshis /* Raw: db */); + assert(rc == SQLITE_OK); + rc = sqlite3_bind_int64(stmt, 6, (sqlite3_int64)e->output_value.millisatoshis /* Raw: db */); + assert(rc == SQLITE_OK); + rc = sqlite3_bind_int64(stmt, 7, (sqlite3_int64)e->timestamp); + assert(rc == SQLITE_OK); + rc = sqlite3_bind_int64(stmt, 8, (sqlite3_int64)e->blockheight); + assert(rc == SQLITE_OK); + rc = sqlite3_bind_outpoint(stmt, 9, &e->outpoint); + assert(rc == SQLITE_OK); + if (e->payment_id) { + rc = sqlite3_bind_hash(stmt, 10, e->payment_id); + } else { + rc = sqlite3_bind_null(stmt,10); + } + assert(rc == SQLITE_OK); + if (e->spending_txid) { + rc = sqlite3_bind_txid(stmt, 11, e->spending_txid); + } else { + rc = sqlite3_bind_null(stmt, 11); + } + assert(rc == SQLITE_OK); + + /* FIXME: Put stealable and spliced into extra_tags! */ + /* Execute */ + rc = sqlite3_step(stmt); + assert(rc == SQLITE_DONE); + + e->db_id = (u64)sqlite3_last_insert_rowid(bkpr_db); + e->acct_name = tal_strdup(e, acct->name); + sqlite3_finalize(stmt); + return true; +} + +/* All chatgpt */ +static void log_channel_event(sqlite3 *db, + const struct account *acct, + struct channel_event *e) +{ + static const char *SQL = + "INSERT INTO channelmoves (" + " account_id, primary_tag, credit_msat, debit_msat, fees_msat, payment_hash, part_id, timestamp" + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?);"; + + sqlite3_stmt *stmt; + int rc = sqlite3_prepare_v2(db, SQL, -1, &stmt, NULL); + assert(rc == SQLITE_OK); + + /* 1 account_id (TEXT) */ + rc = sqlite3_bind_text(stmt, 1, acct->name, -1, SQLITE_TRANSIENT); + assert(rc == SQLITE_OK); + + /* 2 tag (TEXT) */ + rc = sqlite3_bind_text(stmt, 2, e->tag, -1, SQLITE_TRANSIENT); + assert(rc == SQLITE_OK); + + /* 3 credit (INTEGER) */ + rc = sqlite3_bind_int64(stmt, 3, (sqlite3_int64)e->credit.millisatoshis /* Raw: db */); + assert(rc == SQLITE_OK); + + /* 4 debit (INTEGER) */ + rc = sqlite3_bind_int64(stmt, 4, (sqlite3_int64)e->debit.millisatoshis /* Raw: db */); + assert(rc == SQLITE_OK); + + /* 5 fees (INTEGER) */ + rc = sqlite3_bind_int64(stmt, 5, (sqlite3_int64)e->fees.millisatoshis /* Raw: db */); + assert(rc == SQLITE_OK); + + /* 6 payment_hash (BLOB 32) or NULL */ + if (e->payment_id) { + rc = sqlite3_bind_blob(stmt, 6, (const void *)e->payment_id, + (int)sizeof(*e->payment_id), SQLITE_TRANSIENT); + } else { + rc = sqlite3_bind_null(stmt, 6); + } + assert(rc == SQLITE_OK); + + /* 7 part_id (INTEGER) */ + rc = sqlite3_bind_int64(stmt, 7, (sqlite3_int64)e->part_id); + assert(rc == SQLITE_OK); + + /* 8 timestamp (INTEGER) */ + rc = sqlite3_bind_int64(stmt, 8, (sqlite3_int64)e->timestamp); + assert(rc == SQLITE_OK); + + rc = sqlite3_step(stmt); + assert(rc == SQLITE_DONE); + + e->db_id = (u64)sqlite3_last_insert_rowid(db); + e->acct_name = tal_strdup(e, acct->name); + + sqlite3_finalize(stmt); +} + +struct command_result *jsonrpc_set_datastore_(struct command *cmd UNNEEDED, + const char *path UNNEEDED, + const void *value UNNEEDED, + int len_or_str UNNEEDED, + const char *mode UNNEEDED, + struct command_result *(*cb)(struct command *command, + const char *method, + const char *buf, + const jsmntok_t *result, + void *arg), + struct command_result *(*errcb)(struct command *command UNNEEDED, + const char *method UNNEEDED, + const char *buf UNNEEDED, + const jsmntok_t *result UNNEEDED, + void *arg) UNNEEDED, + void *arg) + +{ + return cb(cmd, NULL, NULL, NULL, arg); +} + +struct command_result *ignore_datastore_reply(struct command *cmd, + const char *method UNNEEDED, + const char *buf UNNEEDED, + const jsmntok_t *result UNNEEDED, + void *arg UNNEEDED) +{ + return NULL; +} + +struct json_out *json_out_obj(const tal_t *ctx, + const char *fieldname, + const char *str) +{ + struct json_out *jout = json_out_new(ctx); + json_out_start(jout, NULL, '{'); + if (str) + json_out_addstr(jout, fieldname, str); + if (taken(str)) + tal_free(str); + json_out_end(jout, '}'); + json_out_finished(jout); + + return jout; +} + +const jsmntok_t *jsonrpc_request_sync(const tal_t *ctx, + struct command *cmd, + const char *method, + const struct json_out *params TAKES, + const char **resp) +{ +#if HAVE_SQLITE3 + sqlite3_stmt *s; + jsmntok_t *toks; + jsmn_parser parser; + size_t len; + char *buf; + const char *p, *querystr; + bool complete; + int rc; + + if (streq(method, "listdatastore")) { + buf = tal_fmt(ctx, "{\"datastore\": []}"); + goto done; + } + + assert(streq(method, "sql")); + + /* Grab select query */ + jsmn_init(&parser); + p = json_out_contents(params, &len); + toks = toks_alloc(tmpctx); + assert(json_parse_input(&parser, &toks, p, len, &complete)); + assert(complete); + /* { "query": } */ + querystr = json_strdup(tmpctx, p, toks + 2); + + sqlite3_prepare_v2(bkpr_db, querystr, + -1, &s, NULL); + + /* Make JSON-style result */ + buf = tal_fmt(ctx, "{\"rows\": ["); + while ((rc = sqlite3_step(s)) == SQLITE_ROW) { + size_t num_cols = sqlite3_column_count(s); + + tal_append_fmt(&buf, "["); + for (size_t i = 0; i < num_cols; i++) { + switch (sqlite3_column_type(s, i)) { + case SQLITE_NULL: + tal_append_fmt(&buf, "null"); + break; + case SQLITE_INTEGER: + tal_append_fmt(&buf, "%"PRIu64, (u64)sqlite3_column_int64(s, i)); + break; + case SQLITE_TEXT: + tal_append_fmt(&buf, "\"%s\"", sqlite3_column_text(s, i)); + break; + case SQLITE_BLOB: { + const void *blob = sqlite3_column_blob(s, i); + int bloblen = sqlite3_column_bytes(s, i); + tal_append_fmt(&buf, "\"%s\"", tal_hexstr(tmpctx, blob, bloblen)); + break; + } + default: + abort(); + } + if (i != num_cols - 1) + tal_append_fmt(&buf, ","); + } + tal_append_fmt(&buf, "]"); + } + assert(rc == SQLITE_DONE); + tal_append_fmt(&buf, "]}"); + sqlite3_finalize(s); + +done: + jsmn_init(&parser); + toks = toks_alloc(ctx); + assert(json_parse_input(&parser, &toks, buf, strlen(buf), &complete)); + assert(complete); + + if (taken(params)) + tal_free(params); + *resp = buf; + return toks; +#else + return NULL; +#endif +} + +static char *tmp_dsn(const tal_t *ctx) +{ + char *dsn, *filename; + int fd = tmpdir_mkstemp(ctx, "lacct-db-XXXXXX", &filename); + if (fd == -1) + return NULL; + close(fd); + + dsn = tal_fmt(ctx, "%s", filename); + tal_free(filename); + + return dsn; +} + +static struct bkpr *bkpr_setup(const tal_t *ctx) +{ + struct bkpr *bkpr = tal(ctx, struct bkpr); + + if (bkpr_db) + assert(sqlite3_close(bkpr_db) == SQLITE_OK); + + bkpr_db = db_setup(NULL, tmp_dsn(ctx)); + bkpr->accounts = init_accounts(ctx, NULL); + bkpr->onchain_fees = init_onchain_fees(bkpr, NULL); + bkpr->rebalances = init_rebalances(bkpr, NULL); + + /* We need to see everything */ + bkpr->chainmoves_index = bkpr->channelmoves_index = UINT64_MAX; + return bkpr; +} + +static bool accountseq(struct account *a1, struct account *a2) +{ + CHECK(streq(a1->name, a2->name)); + CHECK((a1->peer_id != NULL) == (a2->peer_id != NULL)); + if (a1->peer_id) + CHECK(node_id_eq(a1->peer_id, a2->peer_id)); + CHECK(a1->is_wallet == a2->is_wallet); + CHECK(a1->we_opened == a2->we_opened); + CHECK(a1->leased == a2->leased); + CHECK(a1->onchain_resolved_block == a2->onchain_resolved_block); + + CHECK((a1->open_event_db_id != NULL) == (a2->open_event_db_id != NULL)); + if (a1->open_event_db_id) + CHECK(*a1->open_event_db_id == *a2->open_event_db_id); + + CHECK((a1->closed_event_db_id != NULL) == (a2->closed_event_db_id != NULL)); + if (a1->closed_event_db_id) + CHECK(*a1->closed_event_db_id == *a2->closed_event_db_id); + + CHECK(a1->closed_count == a2->closed_count); + + return true; +} + +static bool channel_events_eq(struct channel_event *e1, struct channel_event *e2) +{ + CHECK(e1->db_id == e2->db_id); + CHECK(streq(e1->acct_name, e2->acct_name)); + CHECK(streq(e1->tag, e2->tag)); + CHECK(amount_msat_eq(e1->credit, e2->credit)); + CHECK(amount_msat_eq(e1->debit, e2->debit)); + CHECK(amount_msat_eq(e1->fees, e2->fees)); + CHECK((e1->payment_id != NULL) == (e2->payment_id != NULL)); + if (e1->payment_id) + CHECK(sha256_eq(e1->payment_id, e2->payment_id)); + CHECK(e1->part_id == e2->part_id); + CHECK(e1->timestamp == e2->timestamp); + + return true; +} + +static bool chain_events_eq(struct chain_event *e1, struct chain_event *e2) +{ + CHECK(e1->db_id == e2->db_id); + CHECK(streq(e1->acct_name, e2->acct_name)); + CHECK((e1->origin_acct != NULL) == (e2->origin_acct != NULL)); + if (e1->origin_acct) + CHECK(streq(e1->origin_acct, e2->origin_acct)); + CHECK(streq(e1->tag, e2->tag)); + CHECK(amount_msat_eq(e1->credit, e2->credit)); + CHECK(amount_msat_eq(e1->debit, e2->debit)); + CHECK(amount_msat_eq(e1->output_value, e2->output_value)); + CHECK(e1->timestamp == e2->timestamp); + CHECK(e1->blockheight == e2->blockheight); + CHECK(e1->stealable == e2->stealable); + CHECK(bitcoin_outpoint_eq(&e1->outpoint, &e2->outpoint)); + + CHECK((e1->spending_txid != NULL) == (e2->spending_txid != NULL)); + if (e1->spending_txid) + CHECK(bitcoin_txid_eq(e1->spending_txid, e2->spending_txid)); + + CHECK((e1->payment_id != NULL) == (e2->payment_id != NULL)); + if (e1->payment_id) + CHECK(sha256_eq(e1->payment_id, e2->payment_id)); + + CHECK(e1->splice_close == e2->splice_close); + + return true; +} + +static struct channel_event *make_channel_event(const tal_t *ctx, + char *tag, + struct amount_msat credit, + struct amount_msat debit, + char payment_char) +{ + struct channel_event *ev = tal(ctx, struct channel_event); + + ev->payment_id = tal(ev, struct sha256); + memset(ev->payment_id, payment_char, sizeof(struct sha256)); + ev->credit = credit; + ev->debit = debit; + ev->fees = AMOUNT_MSAT(104); + ev->timestamp = 1919191; + ev->part_id = 19; + ev->tag = tag; + return ev; +} + +static struct chain_event *make_chain_event(const tal_t *ctx, + char *tag, + struct amount_msat credit, + struct amount_msat debit, + struct amount_msat output_val, + u32 blockheight, + char outpoint_char, + u32 outnum, + /* Note that '*' is magic */ + char spend_char) + + +{ + struct chain_event *ev = tal(ctx, struct chain_event); + + /* This event spends the second inserted event */ + ev->tag = tal_fmt(ctx, "%s", tag); + ev->origin_acct = NULL; + ev->credit = credit; + ev->debit = debit; + ev->output_value = output_val; + ev->timestamp = 1919191; + ev->blockheight = blockheight; + ev->stealable = false; + ev->splice_close = false; + memset(&ev->outpoint.txid, outpoint_char, sizeof(struct bitcoin_txid)); + ev->outpoint.n = outnum; + + if (spend_char != '*') { + ev->spending_txid = tal(ctx, struct bitcoin_txid); + memset(ev->spending_txid, spend_char, + sizeof(struct bitcoin_txid)); + } else + ev->spending_txid = NULL; + + ev->payment_id = NULL; + + return ev; +} + +static bool test_onchain_fee_wallet_spend(const tal_t *ctx) +{ + struct bkpr *bkpr = bkpr_setup(ctx); + struct node_id node_id, peer_id; + struct account *wal_acct, *ext_acct; + struct bitcoin_txid txid; + struct onchain_fee **ofs; + u32 blockheight = 100000; + + memset(&node_id, 2, sizeof(struct node_id)); + memset(&peer_id, 3, sizeof(struct node_id)); + + wal_acct = new_account(bkpr->accounts, tal_fmt(ctx, ACCOUNT_NAME_WALLET)); + wal_acct->peer_id = &node_id; + ext_acct = new_account(bkpr->accounts, tal_fmt(ctx, ACCOUNT_NAME_EXTERNAL)); + ext_acct->peer_id = &peer_id; + memset(&txid, '1', sizeof(struct bitcoin_txid)); + + db_begin_transaction(db); + account_datastore_set(NULL, wal_acct, "must-create"); + account_datastore_set(NULL, ext_acct, "must-create"); + db_commit_transaction(db); + + + /* Send funds to an external address + * tag utxo_id vout txid debits credits acct_id + * withdr XXXX 0 1111 1000 wallet + * deposit 1111 1 200 wallet + * deposit 1111 0 700 external + */ + db_begin_transaction(db); + log_chain_event(bkpr, wal_acct, + make_chain_event(ctx, "withdrawal", + AMOUNT_MSAT(0), + AMOUNT_MSAT(1000), + AMOUNT_MSAT(1000), + blockheight, + 'X', 0, '1')); + maybe_update_onchain_fees(ctx, NULL, bkpr, &txid); + + log_chain_event(bkpr, wal_acct, + make_chain_event(ctx, "deposit", + AMOUNT_MSAT(200), + AMOUNT_MSAT(0), + AMOUNT_MSAT(200), + blockheight, + '1', 1, '*')); + maybe_update_onchain_fees(ctx, NULL, bkpr, &txid); + + log_chain_event(bkpr, ext_acct, + make_chain_event(ctx, "deposit", + AMOUNT_MSAT(700), + AMOUNT_MSAT(0), + AMOUNT_MSAT(700), + blockheight, + '1', 0, '*')); + maybe_update_onchain_fees(ctx, NULL, bkpr, &txid); + db_commit_transaction(db); + + ofs = list_chain_fees(ctx, bkpr); + CHECK(tal_count(ofs) == 2); + + /* we expect 800, then -700 */ + CHECK(amount_msat_eq(ofs[0]->credit, AMOUNT_MSAT(800))); + CHECK(amount_msat_is_zero(ofs[0]->debit)); + CHECK(ofs[0]->update_count == 1); + + CHECK(amount_msat_is_zero(ofs[1]->credit)); + CHECK(amount_msat_eq(ofs[1]->debit, AMOUNT_MSAT(700))); + CHECK(ofs[1]->update_count == 2); + + return true; +} + +static bool test_onchain_fee_chan_close(const tal_t *ctx) +{ + struct bkpr *bkpr = bkpr_setup(ctx); + struct node_id node_id, peer_id; + struct account *acct, *wal_acct, *ext_acct; + struct onchain_fee **ofs, **ofs1; + struct bitcoin_txid txid; + struct chain_event *ev; + enum mvt_tag *tags; + u32 close_output_count; + u32 blockheight = 100000; + char *err; + + memset(&node_id, 2, sizeof(struct node_id)); + memset(&peer_id, 3, sizeof(struct node_id)); + /* to_us, to_them, 1 htlc, 2 anchors */ + close_output_count = 5; + + wal_acct = new_account(bkpr->accounts, tal_fmt(ctx, ACCOUNT_NAME_WALLET)); + wal_acct->peer_id = &node_id; + ext_acct = new_account(bkpr->accounts, tal_fmt(ctx, ACCOUNT_NAME_EXTERNAL)); + ext_acct->peer_id = &peer_id; + acct = new_account(bkpr->accounts, tal_fmt(ctx, "chan-1")); + acct->peer_id = &peer_id; + + db_begin_transaction(db); + account_datastore_set(NULL, wal_acct, "must-create"); + account_datastore_set(NULL, ext_acct, "must-create"); + account_datastore_set(NULL, acct, "must-create"); + db_commit_transaction(db); + + /* Close a channel */ + /* tag utxo_id vout txid debits credits acct_id + * close XXXX 0 1111 1000 wallet + * delay 1111 1 200 chan-1 + * anchor 1111 0 30 chan-1 + * anchor 1111 4 30 external + * to_wall 1111 1 2222 200 chan-1 + * htlc_tim1111 2 600 chan-1 + * htlc_tim1111 2 3333 600 chan-1 + * to_them 1111 3 300 external + * deposit 2222 0 150 chan-1 + * htlc_tx 3333 0 450 chan-1 + * to_wall 3333 0 4444 450 chan-1 + * deposit 4444 0 350 wallet + */ + tags = tal_arr(ctx, enum mvt_tag, 1); + db_begin_transaction(db); + ev = make_chain_event(ctx, "channel_open", + AMOUNT_MSAT(1000), + AMOUNT_MSAT(0), + AMOUNT_MSAT(1660), + blockheight, + 'X', 0, '*'); + log_chain_event(bkpr, acct, ev); + tags[0] = MVT_CHANNEL_OPEN; + maybe_update_account(NULL, acct, ev, tags, 0, NULL); + + ev = make_chain_event(ctx, "channel_close", + AMOUNT_MSAT(0), + AMOUNT_MSAT(1000), + AMOUNT_MSAT(1660), + blockheight, + 'X', 0, '1'); + log_chain_event(bkpr, acct, ev); + + /* Update the account to have the right info! */ + tags[0] = MVT_CHANNEL_CLOSE; + maybe_update_account(NULL, acct, ev, tags, close_output_count, NULL); + + log_chain_event(bkpr, acct, + make_chain_event(ctx, "delayed_to_us", + AMOUNT_MSAT(200), + AMOUNT_MSAT(0), + AMOUNT_MSAT(200), + blockheight, + '1', 1, '*')); + memset(&txid, '1', sizeof(struct bitcoin_txid)); + maybe_update_onchain_fees(ctx, NULL, bkpr, &txid); + + log_chain_event(bkpr, wal_acct, + make_chain_event(ctx, "anchor", + AMOUNT_MSAT(30), + AMOUNT_MSAT(0), + AMOUNT_MSAT(30), + blockheight, + '1', 0, '*')); + log_chain_event(bkpr, ext_acct, + make_chain_event(ctx, "anchor", + AMOUNT_MSAT(30), + AMOUNT_MSAT(0), + AMOUNT_MSAT(30), + blockheight, + '1', 4, '*')); + memset(&txid, '1', sizeof(struct bitcoin_txid)); + maybe_update_onchain_fees(ctx, NULL, bkpr, &txid); + + /* Should be no fees yet */ + ofs = list_chain_fees(ctx, bkpr); + CHECK(tal_count(ofs) == 0); + + log_chain_event(bkpr, acct, + make_chain_event(ctx, "htlc_timeout", + AMOUNT_MSAT(600), + AMOUNT_MSAT(0), + AMOUNT_MSAT(600), + blockheight, + '1', 2, '*')); + + log_chain_event(bkpr, ext_acct, + make_chain_event(ctx, "to_them", + AMOUNT_MSAT(300), + AMOUNT_MSAT(0), + AMOUNT_MSAT(300), + blockheight, + '1', 3, '*')); + + memset(&txid, '1', sizeof(struct bitcoin_txid)); + maybe_update_onchain_fees(ctx, NULL, bkpr, &txid); + db_commit_transaction(db); + + /* txid 2222 */ + db_begin_transaction(db); + log_chain_event(bkpr, acct, + make_chain_event(ctx, "to_wallet", + AMOUNT_MSAT(0), + AMOUNT_MSAT(200), + AMOUNT_MSAT(200), + blockheight + 1, + '1', 1, '2')); + + log_chain_event(bkpr, wal_acct, + make_chain_event(ctx, "deposit", + AMOUNT_MSAT(150), + AMOUNT_MSAT(0), + AMOUNT_MSAT(150), + blockheight + 1, + '2', 0, '*')); + memset(&txid, '2', sizeof(struct bitcoin_txid)); + maybe_update_onchain_fees(ctx, NULL, bkpr, &txid); + CHECK(acct->onchain_resolved_block == 0); + + assert(account_onchain_closeheight(bkpr, NULL, acct) == 0); + CHECK(acct->onchain_resolved_block == 0); + db_commit_transaction(db); + + /* Expect: 1 onchain fee records, all for chan-1 */ + ofs = list_chain_fees(ctx, bkpr); + ofs1 = account_get_chain_fees(tmpctx, bkpr, acct->name); + + CHECK(tal_count(ofs) == tal_count(ofs1)); + CHECK(tal_count(ofs) == 1); + + /* txid 4444 */ + db_begin_transaction(db); + log_chain_event(bkpr, wal_acct, + make_chain_event(ctx, "deposit", + AMOUNT_MSAT(350), + AMOUNT_MSAT(0), + AMOUNT_MSAT(350), + blockheight + 2, + '4', 0, '*')); + + memset(&txid, '4', sizeof(struct bitcoin_txid)); + maybe_update_onchain_fees(ctx, NULL, bkpr, &txid); + + /* txid 3333 */ + log_chain_event(bkpr, acct, + make_chain_event(ctx, "htlc_timeout", + AMOUNT_MSAT(0), + AMOUNT_MSAT(600), + AMOUNT_MSAT(600), + blockheight + 2, + '1', 2, '3')); + + assert(account_onchain_closeheight(bkpr, NULL, acct) == 0); + CHECK(acct->onchain_resolved_block == 0); + + log_chain_event(bkpr, acct, + make_chain_event(ctx, "htlc_tx", + AMOUNT_MSAT(450), + AMOUNT_MSAT(0), + AMOUNT_MSAT(450), + blockheight + 2, + '3', 0, '*')); + + memset(&txid, '3', sizeof(struct bitcoin_txid)); + maybe_update_onchain_fees(ctx, NULL, bkpr, &txid); + + log_chain_event(bkpr, acct, + make_chain_event(ctx, "to_wallet", + AMOUNT_MSAT(0), + AMOUNT_MSAT(450), + AMOUNT_MSAT(450), + blockheight + 2, + '3', 0, '4')); + + memset(&txid, '4', sizeof(struct bitcoin_txid)); + maybe_update_onchain_fees(ctx, NULL, bkpr, &txid); + + db_commit_transaction(db); + + /* Expect: onchain fee records for tx except channel close */ + ofs = list_chain_fees(ctx, bkpr); + ofs1 = account_get_chain_fees(tmpctx, bkpr, acct->name); + + CHECK(tal_count(ofs) == tal_count(ofs1)); + CHECK(tal_count(ofs) == 3); + + /* Now we update the channel's onchain fees */ + CHECK(acct->onchain_resolved_block == 0); + db_begin_transaction(db); + account_update_closeheight(NULL, acct, account_onchain_closeheight(bkpr, NULL, acct)); + CHECK(acct->onchain_resolved_block == blockheight + 2); + err = update_channel_onchain_fees(ctx, NULL, bkpr, acct); + CHECK_MSG(!err, err); + db_commit_transaction(db); + ofs = account_get_chain_fees(tmpctx, bkpr, acct->name); + + /* Expect: fees as follows + * + * chan-1, 1111, 200 (anchor outs ignored) + * chan-1, 2222, 50 + * chan-1, 3333, 150 + * chan-1, 4444, 100 + */ + CHECK(tal_count(ofs) == 4); + for (size_t i = 0; i < tal_count(ofs); i++) { + CHECK(streq(ofs[i]->acct_name, acct->name)); + + memset(&txid, '1', sizeof(struct bitcoin_txid)); + if (bitcoin_txid_eq(&txid, &ofs[i]->txid)) { + CHECK(ofs[i]->update_count == 1); + CHECK(amount_msat_eq(ofs[i]->credit, AMOUNT_MSAT(200))); + CHECK(amount_msat_is_zero(ofs[i]->debit)); + continue; + } + memset(&txid, '2', sizeof(struct bitcoin_txid)); + if (bitcoin_txid_eq(&txid, &ofs[i]->txid)) { + CHECK(ofs[i]->update_count == 1); + CHECK(amount_msat_eq(ofs[i]->credit, AMOUNT_MSAT(50))); + CHECK(amount_msat_is_zero(ofs[i]->debit)); + continue; + } + memset(&txid, '3', sizeof(struct bitcoin_txid)); + if (bitcoin_txid_eq(&txid, &ofs[i]->txid)) { + CHECK(ofs[i]->update_count == 1); + CHECK(amount_msat_eq(ofs[i]->credit, AMOUNT_MSAT(150))); + CHECK(amount_msat_is_zero(ofs[i]->debit)); + continue; + } + memset(&txid, '4', sizeof(struct bitcoin_txid)); + if (bitcoin_txid_eq(&txid, &ofs[i]->txid)) { + CHECK(ofs[i]->update_count == 1); + CHECK(amount_msat_eq(ofs[i]->credit, AMOUNT_MSAT(100))); + CHECK(amount_msat_is_zero(ofs[i]->debit)); + continue; + } + + CHECK_MSG(false, "txid didn't match"); + } + + return true; +} + +static bool test_onchain_fee_chan_open(const tal_t *ctx) +{ + struct bkpr *bkpr = bkpr_setup(ctx); + struct node_id node_id, peer_id; + struct account *acct, *acct2, *wal_acct, *ext_acct; + struct bitcoin_txid txid; + struct onchain_fee **ofs; + u32 blockheight = 100000; + + memset(&node_id, 2, sizeof(struct node_id)); + memset(&peer_id, 3, sizeof(struct node_id)); + + wal_acct = new_account(bkpr->accounts, tal_fmt(ctx, ACCOUNT_NAME_WALLET)); + wal_acct->peer_id = &peer_id; + ext_acct = new_account(bkpr->accounts, tal_fmt(ctx, ACCOUNT_NAME_EXTERNAL)); + ext_acct->peer_id = &peer_id; + acct = new_account(bkpr->accounts, tal_fmt(ctx, "chan-1")); + acct->peer_id = &peer_id; + acct2 = new_account(bkpr->accounts, tal_fmt(ctx, "chan-2")); + acct2->peer_id = &peer_id; + + db_begin_transaction(db); + account_datastore_set(NULL, wal_acct, "must-create"); + account_datastore_set(NULL, ext_acct, "must-create"); + account_datastore_set(NULL, acct, "must-create"); + account_datastore_set(NULL, acct2, "must-create"); + db_commit_transaction(db); + + /* Assumption that we rely on later */ + CHECK(strcmp(acct->name, acct2->name) < 0); + + /* Open two channels from wallet */ + /* tag utxo_id vout txid debits credits acct_id + * withd XXXX 0 AAAA 1000 wallet + * withd YYYY 0 AAAA 3001 wallet + * open AAAA 0 500 chan-1 + * open AAAA 1 1000 chan-2 + * depo AAAA 2 2200 wallet + */ + memset(&txid, 'A', sizeof(struct bitcoin_txid)); + db_begin_transaction(db); + log_chain_event(bkpr, wal_acct, + make_chain_event(ctx, "withdrawal", + AMOUNT_MSAT(0), + AMOUNT_MSAT(1000), + AMOUNT_MSAT(1000), + blockheight, + 'X', 0, 'A')); + log_chain_event(bkpr, wal_acct, + make_chain_event(ctx, "withdrawal", + AMOUNT_MSAT(0), + AMOUNT_MSAT(3001), + AMOUNT_MSAT(3001), + blockheight, + 'Y', 0, 'A')); + + log_chain_event(bkpr, acct, + make_chain_event(ctx, "deposit", + AMOUNT_MSAT(500), + AMOUNT_MSAT(0), + AMOUNT_MSAT(500), + blockheight, + 'A', 0, '*')); + maybe_update_onchain_fees(ctx, NULL, bkpr, &txid); + + log_chain_event(bkpr, acct2, + make_chain_event(ctx, "deposit", + AMOUNT_MSAT(1000), + AMOUNT_MSAT(0), + AMOUNT_MSAT(1000), + blockheight, + 'A', 1, '*')); + maybe_update_onchain_fees(ctx, NULL, bkpr, &txid); + + log_chain_event(bkpr, wal_acct, + make_chain_event(ctx, "deposit", + AMOUNT_MSAT(2200), + AMOUNT_MSAT(0), + AMOUNT_MSAT(2200), + blockheight, + 'A', 2, '*')); + maybe_update_onchain_fees(ctx, NULL, bkpr, &txid); + + maybe_update_onchain_fees(ctx, NULL, bkpr, &txid); + db_commit_transaction(db); + + /* Expect: 5 onchain fee records, totaling to 151/150msat ea, + * none for wallet */ + ofs = list_chain_fees(ctx, bkpr); + + CHECK(tal_count(ofs) == 5); + + struct exp_result { + u32 credit; + u32 debit; + u32 update_count; + }; + + struct exp_result exp_results[] = { + { .credit = 3501, .debit = 0, .update_count = 1 }, + { .credit = 0, .debit = 2250, .update_count = 2 }, + { .credit = 0, .debit = 1100, .update_count = 3 }, + { .credit = 1250, .debit = 0, .update_count = 1 }, + { .credit = 0, .debit = 1100, .update_count = 2 }, + }; + + /* Since these are sorted on fetch, + * this *should* be stable */ + for (size_t i = 0; i < tal_count(ofs); i++) { + CHECK(i < ARRAY_SIZE(exp_results)); + CHECK(amount_msat_eq(ofs[i]->credit, + amount_msat(exp_results[i].credit))); + CHECK(amount_msat_eq(ofs[i]->debit, + amount_msat(exp_results[i].debit))); + CHECK(ofs[i]->update_count == exp_results[i].update_count); + + CHECK(bitcoin_txid_eq(&ofs[i]->txid, &txid)); + } + + return true; +} + +static bool test_channel_rebalances(const tal_t *ctx) +{ + struct bkpr *bkpr = bkpr_setup(ctx); + sqlite3 *db = bkpr_db; + struct channel_event *ev1, *ev2, *ev3, **chan_evs; + struct rebalance_pair *rebals; + struct rebalance_htable_iter it; + struct account *acct1, *acct2, *acct3; + struct node_id peer_id; + + memset(&peer_id, 3, sizeof(struct node_id)); + acct1 = new_account(bkpr->accounts, tal_fmt(ctx, "one")); + acct1->peer_id = &peer_id; + acct2 = new_account(bkpr->accounts, tal_fmt(ctx, "two")); + acct2->peer_id = &peer_id; + acct3 = new_account(bkpr->accounts, tal_fmt(ctx, "three")); + acct3->peer_id = &peer_id; + + db_begin_transaction(db); + + account_datastore_set(NULL, acct1, "must-create"); + account_datastore_set(NULL, acct2, "must-create"); + account_datastore_set(NULL, acct3, "must-create"); + + /* Simulate a rebalance of 100msats, w/ a 12msat fee */ + ev1 = make_channel_event(ctx, "invoice", + AMOUNT_MSAT(100), + AMOUNT_MSAT(0), + 'A'); + ev1->fees = AMOUNT_MSAT(0); + log_channel_event(db, acct1, ev1); + + ev2 = make_channel_event(ctx, "invoice", + AMOUNT_MSAT(0), + AMOUNT_MSAT(112), + 'A'); + ev2->fees = AMOUNT_MSAT(12); + log_channel_event(db, acct2, ev2); + + /* Third event w/ same preimage but diff amounts */ + ev3 = make_channel_event(ctx, "invoice", + AMOUNT_MSAT(105), + AMOUNT_MSAT(0), + 'A'); + log_channel_event(db, acct3, ev3); + db_commit_transaction(db); + + db_begin_transaction(db); + chan_evs = account_get_channel_events(ctx, bkpr, NULL, acct1); + CHECK(tal_count(chan_evs) == 1 && !find_rebalance(bkpr, chan_evs[0]->db_id)); + + chan_evs = account_get_channel_events(ctx, bkpr, NULL, acct2); + CHECK(tal_count(chan_evs) == 1 && !find_rebalance(bkpr, chan_evs[0]->db_id)); + + chan_evs = account_get_channel_events(ctx, bkpr, NULL, acct3); + CHECK(tal_count(chan_evs) == 1 && !find_rebalance(bkpr, chan_evs[0]->db_id)); + + maybe_record_rebalance(NULL, bkpr, ev2); + CHECK(find_rebalance(bkpr, ev2->db_id) != NULL); + + /* Both events should be marked as rebalance */ + chan_evs = account_get_channel_events(ctx, bkpr, NULL, acct1); + CHECK(tal_count(chan_evs) == 1 && find_rebalance(bkpr, chan_evs[0]->db_id)); + ev1 = chan_evs[0]; + chan_evs = account_get_channel_events(ctx, bkpr, NULL, acct2); + CHECK(tal_count(chan_evs) == 1 && find_rebalance(bkpr, chan_evs[0]->db_id)); + CHECK(*find_rebalance(bkpr, chan_evs[0]->db_id) == ev1->db_id); + CHECK(*find_rebalance(bkpr, ev1->db_id) == chan_evs[0]->db_id); + + /* Third event is not a rebalance though */ + chan_evs = account_get_channel_events(ctx, bkpr, NULL, acct3); + CHECK(tal_count(chan_evs) == 1 && !find_rebalance(bkpr, chan_evs[0]->db_id)); + + /* Did we get an accurate rebalances entry? */ + assert(rebalance_htable_count(bkpr->rebalances->pairs) == 2); + rebals = rebalance_htable_first(bkpr->rebalances->pairs, &it); + + if (rebals->pair[0] == ev1->db_id) { + CHECK(rebals->pair[1] == ev2->db_id); + } else { + CHECK(rebals->pair[1] == ev1->db_id); + CHECK(rebals->pair[0] == ev2->db_id); + } + db_commit_transaction(db); + + return true; +} + +static bool test_channel_event_crud(const tal_t *ctx) +{ + struct bkpr *bkpr = bkpr_setup(ctx); + sqlite3 *db = bkpr_db; + struct node_id peer_id; + struct account *acct, *acct2; + struct channel_event *ev1, *ev2, *ev3, **chan_evs; + + memset(&peer_id, 3, sizeof(struct node_id)); + + acct = new_account(bkpr->accounts, tal_fmt(ctx, "example")); + acct->peer_id = &peer_id; + acct2 = new_account(bkpr->accounts, tal_fmt(ctx, ACCOUNT_NAME_WALLET)); + acct2->peer_id = &peer_id; + db_begin_transaction(db); + account_datastore_set(NULL, acct, "must-create"); + account_datastore_set(NULL, acct2, "must-create"); + db_commit_transaction(db); + + ev1 = tal(ctx, struct channel_event); + ev1->payment_id = tal(ev1, struct sha256); + memset(ev1->payment_id, 'B', sizeof(struct sha256)); + ev1->credit = AMOUNT_MSAT(100); + ev1->debit = AMOUNT_MSAT(102); + ev1->fees = AMOUNT_MSAT(104); + ev1->timestamp = 11111; + ev1->part_id = 19; + + /* Passing unknown tags in should be ok */ + ev1->tag = "hello"; + + ev2 = tal(ctx, struct channel_event); + ev2->payment_id = tal(ev2, struct sha256); + memset(ev2->payment_id, 'C', sizeof(struct sha256)); + ev2->credit = AMOUNT_MSAT(200); + ev2->debit = AMOUNT_MSAT(202); + ev2->fees = AMOUNT_MSAT(204); + ev2->timestamp = 22222; + ev2->part_id = 0; + ev2->tag = tal_fmt(ev2, "deposit"); + + ev3 = tal(ctx, struct channel_event); + ev3->payment_id = tal(ev3, struct sha256); + memset(ev3->payment_id, 'D', sizeof(struct sha256)); + ev3->credit = AMOUNT_MSAT(300); + ev3->debit = AMOUNT_MSAT(302); + ev3->fees = AMOUNT_MSAT(304); + ev3->timestamp = 33333; + ev3->part_id = 5; + ev3->tag = tal_fmt(ev3, "routed"); + + db_begin_transaction(db); + log_channel_event(db, acct, ev1); + log_channel_event(db, acct, ev2); + + /* log a channel event to a different acct */ + log_channel_event(db, acct2, ev3); + + /* log a channel event without a payment id */ + ev3->payment_id = NULL; + log_channel_event(db, acct2, ev3); + + db_commit_transaction(db); + + db_begin_transaction(db); + chan_evs = account_get_channel_events(ctx, bkpr, NULL, acct); + db_commit_transaction(db); + + CHECK(streq(acct->name, chan_evs[0]->acct_name)); + CHECK(streq(acct->name, chan_evs[1]->acct_name)); + + CHECK(tal_count(chan_evs) == 2); + channel_events_eq(ev1, chan_evs[0]); + channel_events_eq(ev2, chan_evs[1]); + + return true; +} + +static bool test_chain_event_crud(const tal_t *ctx) +{ + struct bkpr *bkpr = bkpr_setup(ctx); + struct node_id peer_id; + struct account *acct, *acct2; + struct chain_event *ev1, *ev2, *ev3, **chain_evs; + char *name = tal_fmt(ctx, "example"); + + ev2 = tal(ctx, struct chain_event); + memset(&peer_id, 3, sizeof(struct node_id)); + + acct = new_account(bkpr->accounts, name); + acct->peer_id = &peer_id; + acct2 = new_account(bkpr->accounts, tal_fmt(ctx, ACCOUNT_NAME_WALLET)); + acct2->peer_id = &peer_id; + db_begin_transaction(db); + account_datastore_set(NULL, acct, "must-create"); + account_datastore_set(NULL, acct2, "must-create"); + db_commit_transaction(db); + + /* This event spends the second inserted event */ + ev1 = tal(ctx, struct chain_event); + ev1->tag = tal_fmt(ev1, "withdrawal"); + ev1->origin_acct = NULL; + ev1->credit = AMOUNT_MSAT(100); + ev1->debit = AMOUNT_MSAT(102); + ev1->output_value = AMOUNT_MSAT(104); + ev1->timestamp = 1919191; + ev1->blockheight = 1919191; + ev1->stealable = false; + ev1->splice_close = false; + memset(&ev1->outpoint.txid, 'D', sizeof(struct bitcoin_txid)); + ev1->outpoint.n = 1; + ev1->spending_txid = tal(ctx, struct bitcoin_txid); + memset(ev1->spending_txid, 'C', sizeof(struct bitcoin_txid)); + ev1->payment_id = NULL; + + db_begin_transaction(db); + log_chain_event(bkpr, acct, ev1); + db_commit_transaction(db); + + ev2->tag = tal_fmt(ctx, "deposit"); + ev2->origin_acct = tal_fmt(ctx, ACCOUNT_NAME_WALLET); + ev2->credit = AMOUNT_MSAT(200); + ev2->debit = AMOUNT_MSAT(202); + ev2->output_value = AMOUNT_MSAT(104); + ev2->timestamp = 1919191; + ev2->blockheight = 1919191; + ev2->stealable = false; + ev2->splice_close = false; + memset(&ev2->outpoint.txid, 'D', sizeof(struct bitcoin_txid)); + ev2->outpoint.n = 1; + ev2->spending_txid = NULL; + ev2->payment_id = tal(ctx, struct sha256); + memset(ev2->payment_id, 'B', sizeof(struct sha256)); + + /* Dummy event, logged to separate account */ + ev3 = tal(ctx, struct chain_event); + ev3->tag = tal_fmt(ev3, "deposit"); + ev3->origin_acct = NULL; + ev3->credit = AMOUNT_MSAT(300); + ev3->debit = AMOUNT_MSAT(302); + ev3->output_value = AMOUNT_MSAT(304); + ev3->timestamp = 3939393; + ev3->blockheight = 3939393; + ev3->stealable = false; + ev3->splice_close = false; + memset(&ev3->outpoint.txid, 'E', sizeof(struct bitcoin_txid)); + ev3->outpoint.n = 1; + ev3->spending_txid = tal(ctx, struct bitcoin_txid); + memset(ev3->spending_txid, 'D', sizeof(struct bitcoin_txid)); + ev3->payment_id = NULL; + + db_begin_transaction(db); + log_chain_event(bkpr, acct, ev2); + + /* log new event to a different account.. */ + log_chain_event(bkpr, acct2, ev3); + db_commit_transaction(db); + + /* Try to add an already exiting event */ + db_begin_transaction(db); + log_chain_event(bkpr, acct, ev2); + db_commit_transaction(db); + + /* Ok now we ge the list, there should only be two */ + db_begin_transaction(db); + chain_evs = account_get_chain_events(ctx, bkpr, NULL, acct); + db_commit_transaction(db); + CHECK(tal_count(chain_evs) == 2); + + CHECK(streq(acct->name, chain_evs[0]->acct_name)); + CHECK(streq(acct->name, chain_evs[1]->acct_name)); + chain_events_eq(ev1, chain_evs[0]); + chain_events_eq(ev2, chain_evs[1]); + + /* Now insert a utxo create and spend, in that order */ + ev1->db_id = 0; + memset(&ev1->outpoint.txid, 'A', sizeof(struct bitcoin_txid)); + ev1->outpoint.n = 10; + ev1->spending_txid = tal_free(ev1->spending_txid); + + ev2->db_id = 0; + memset(&ev2->outpoint.txid, 'A', sizeof(struct bitcoin_txid)); + ev2->outpoint.n = 10; + ev2->spending_txid = tal(ctx, struct bitcoin_txid); + memset(ev2->spending_txid, 'B', sizeof(struct bitcoin_txid)); + + + db_begin_transaction(db); + log_chain_event(bkpr, acct, ev1); + log_chain_event(bkpr, acct, ev2); + chain_evs = account_get_chain_events(ctx, bkpr, NULL, acct); + db_commit_transaction(db); + + /* There should be four now */ + CHECK(tal_count(chain_evs) == 4); + + chain_events_eq(ev1, chain_evs[2]); + chain_events_eq(ev2, chain_evs[3]); + + return true; +} + +struct acct_balance { + struct amount_msat credit; + struct amount_msat debit; + struct amount_msat balance; +}; + +static bool account_get_balance(struct bkpr *bkpr, + const char *acct_name, + struct acct_balance *bal) +{ + account_get_credit_debit(bkpr, NULL, acct_name, + &bal->credit, &bal->debit); + + return amount_msat_sub(&bal->balance, bal->credit, bal->debit); +} + +static bool test_account_balances(const tal_t *ctx) +{ + struct bkpr *bkpr = bkpr_setup(ctx); + sqlite3 *db = bkpr_db; + struct node_id peer_id; + struct account *acct, *acct2; + struct chain_event *ev1; + struct acct_balance balance; + bool ok; + + memset(&peer_id, 3, sizeof(struct node_id)); + + acct = new_account(bkpr->accounts, tal_fmt(ctx, "example")); + acct->peer_id = &peer_id; + acct2 = new_account(bkpr->accounts, tal_fmt(ctx, ACCOUNT_NAME_WALLET)); + acct2->peer_id = &peer_id; + + db_begin_transaction(db); + /* Check that account does not exist yet */ + ok = account_get_balance(bkpr, acct->name, &balance); + CHECK(ok); + + account_datastore_set(NULL, acct, "must-create"); + account_datastore_set(NULL, acct2, "must-create"); + + /* +1000btc */ + log_chain_event(bkpr, acct, + make_chain_event(ctx, "one", + AMOUNT_MSAT(1000), + AMOUNT_MSAT(0), + AMOUNT_MSAT(1000), + 1019, + 'A', 1, '*')); + ev1 = make_chain_event(ctx, "two", + AMOUNT_MSAT(0), AMOUNT_MSAT(999), + AMOUNT_MSAT(999), + 1020, 'A', 2, '*'); + + /* -999btc */ + log_chain_event(bkpr, acct, ev1); + + /* -440btc */ + log_channel_event(db, acct, + make_channel_event(ctx, "chan", + AMOUNT_MSAT(0), + AMOUNT_MSAT(440), + 'C')); + + /* 500btc */ + log_channel_event(db, acct, + make_channel_event(ctx, "chan", + AMOUNT_MSAT(500), + AMOUNT_MSAT(0), + 'D')); + + ok = account_get_balance(bkpr, acct->name, &balance); + CHECK(ok); + db_commit_transaction(db); + + CHECK(amount_msat_eq(balance.balance, AMOUNT_MSAT(500 - 440 + 1))); + + /* Should error if account balance is negative */ + db_begin_transaction(db); + /* -5001btc */ + ev1 = make_chain_event(ctx, "two", + AMOUNT_MSAT(0), AMOUNT_MSAT(5001), + AMOUNT_MSAT(5001), 2020, + 'A', 4, '*'); + log_chain_event(bkpr, acct, ev1); + + ok = account_get_balance(bkpr, acct->name, &balance); + CHECK(!ok); + db_commit_transaction(db); + + return true; +} + +static bool test_account_crud(const tal_t *ctx) +{ + struct bkpr *bkpr = bkpr_setup(ctx); + struct node_id *peer_id; + struct account *acct, *acct2, **acct_list; + struct chain_event *ev1; + enum mvt_tag *tags; + char *name = tal_fmt(ctx, "example"); + + peer_id = tal(ctx, struct node_id); + memset(peer_id, 3, sizeof(struct node_id)); + + acct = new_account(bkpr->accounts, name); + CHECK(!acct->is_wallet); + + db_begin_transaction(db); + account_datastore_set(NULL, acct, "must-create"); + db_commit_transaction(db); + + acct_list = list_accounts(ctx, bkpr); + CHECK(tal_count(acct_list) == 1); + accountseq(acct_list[0], acct); + + acct = new_account(bkpr->accounts, tal_fmt(ctx, ACCOUNT_NAME_WALLET)); + CHECK(acct->is_wallet); + + db_begin_transaction(db); + account_datastore_set(NULL, acct, "must-create"); + db_commit_transaction(db); + + acct_list = list_accounts(ctx, bkpr); + CHECK(tal_count(acct_list) == 2); + + /* Can we find an account ok? */ + acct2 = find_account(bkpr, ACCOUNT_NAME_WALLET); + accountseq(acct, acct2); + + /* Will we update an account's properties + * correctly, given an event and tag list? */ + ev1 = tal(ctx, struct chain_event); + ev1->tag = tal_fmt(ctx, "withdrawal"); + ev1->origin_acct = NULL; + ev1->credit = AMOUNT_MSAT(100); + ev1->debit = AMOUNT_MSAT(102); + ev1->output_value = AMOUNT_MSAT(104); + ev1->timestamp = 1919191; + ev1->blockheight = 1919191; + ev1->stealable = false; + ev1->splice_close = false; + memset(&ev1->outpoint.txid, 'D', sizeof(struct bitcoin_txid)); + ev1->outpoint.n = 1; + ev1->spending_txid = tal(ctx, struct bitcoin_txid); + memset(ev1->spending_txid, 'C', sizeof(struct bitcoin_txid)); + ev1->payment_id = NULL; + + db_begin_transaction(db); + log_chain_event(bkpr, acct, ev1); + + tags = tal_arr(ctx, enum mvt_tag, 2); + + /* should not update the account info */ + tags[0] = MVT_PUSHED; + tags[1] = MVT_PENALTY; + maybe_update_account(NULL, acct, ev1, tags, 0, peer_id); + acct2 = find_account(bkpr, ACCOUNT_NAME_WALLET); + accountseq(acct, acct2); + + /* channel_open -> open event db updated */ + CHECK(!acct->leased); + CHECK(acct->open_event_db_id == NULL); + tags[0] = MVT_CHANNEL_OPEN; + tags[1] = MVT_LEASED; + maybe_update_account(NULL, acct, ev1, tags, 2, peer_id); + acct2 = find_account(bkpr, ACCOUNT_NAME_WALLET); + accountseq(acct, acct2); + CHECK(acct->leased); + CHECK(acct->open_event_db_id != NULL); + CHECK(acct->closed_count == 2); + + tags[0] = MVT_CHANNEL_CLOSE; + tags[1] = MVT_OPENER; + CHECK(acct->closed_event_db_id == NULL); + CHECK(!acct->we_opened); + maybe_update_account(NULL, acct, ev1, tags, 0, NULL); + acct2 = find_account(bkpr, ACCOUNT_NAME_WALLET); + accountseq(acct, acct2); + CHECK(acct->closed_event_db_id != NULL); + CHECK(acct->we_opened); + + db_commit_transaction(db); + + return true; +} + +int main(int argc, char *argv[]) +{ + bool ok = true; + + common_setup(argv[0]); + + if (HAVE_SQLITE3) { + ok &= test_account_crud(tmpctx); + ok &= test_channel_event_crud(tmpctx); + ok &= test_chain_event_crud(tmpctx); + ok &= test_account_balances(tmpctx); + ok &= test_onchain_fee_chan_close(tmpctx); + ok &= test_onchain_fee_chan_open(tmpctx); + ok &= test_channel_rebalances(tmpctx); + ok &= test_onchain_fee_wallet_spend(tmpctx); + sqlite3_close(bkpr_db); + } + + common_shutdown(); + trace_cleanup(); + return !ok; +} diff --git a/plugins/bkpr/test/run-sql.c b/plugins/bkpr/test/run-sql.c index 595661aed..8be5859ad 100644 --- a/plugins/bkpr/test/run-sql.c +++ b/plugins/bkpr/test/run-sql.c @@ -69,60 +69,6 @@ bool json_filter_ok(const struct json_filter *filter UNNEEDED, const char *membe /* Generated stub for json_filter_up */ bool json_filter_up(struct json_filter **filter UNNEEDED) { fprintf(stderr, "json_filter_up called!\n"); abort(); } -/* Generated stub for json_scan */ -const char *json_scan(const tal_t *ctx UNNEEDED, - const char *buffer UNNEEDED, - const jsmntok_t *tok UNNEEDED, - const char *guide UNNEEDED, - ...) -{ fprintf(stderr, "json_scan called!\n"); abort(); } -/* Generated stub for json_scanv */ -const char *json_scanv(const tal_t *ctx UNNEEDED, - const char *buffer UNNEEDED, - const jsmntok_t *tok UNNEEDED, - const char *guide UNNEEDED, - va_list ap UNNEEDED) -{ fprintf(stderr, "json_scanv called!\n"); abort(); } -/* Generated stub for json_to_int */ -bool json_to_int(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, int *num UNNEEDED) -{ fprintf(stderr, "json_to_int called!\n"); abort(); } -/* Generated stub for json_to_msat */ -bool json_to_msat(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, - struct amount_msat *msat UNNEEDED) -{ fprintf(stderr, "json_to_msat called!\n"); abort(); } -/* Generated stub for json_to_node_id */ -bool json_to_node_id(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, - struct node_id *id UNNEEDED) -{ fprintf(stderr, "json_to_node_id called!\n"); abort(); } -/* Generated stub for json_to_number */ -bool json_to_number(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, - unsigned int *num UNNEEDED) -{ fprintf(stderr, "json_to_number called!\n"); abort(); } -/* Generated stub for json_to_outpoint */ -bool json_to_outpoint(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, - struct bitcoin_outpoint *op UNNEEDED) -{ fprintf(stderr, "json_to_outpoint called!\n"); abort(); } -/* Generated stub for json_to_secret */ -bool json_to_secret(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, struct secret *dest UNNEEDED) -{ fprintf(stderr, "json_to_secret called!\n"); abort(); } -/* Generated stub for json_to_sha256 */ -bool json_to_sha256(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, struct sha256 *dest UNNEEDED) -{ fprintf(stderr, "json_to_sha256 called!\n"); abort(); } -/* Generated stub for json_to_short_channel_id */ -bool json_to_short_channel_id(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, - struct short_channel_id *scid UNNEEDED) -{ fprintf(stderr, "json_to_short_channel_id called!\n"); abort(); } -/* Generated stub for json_to_txid */ -bool json_to_txid(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, - struct bitcoin_txid *txid UNNEEDED) -{ fprintf(stderr, "json_to_txid called!\n"); abort(); } -/* Generated stub for json_to_u16 */ -bool json_to_u16(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, - uint16_t *num UNNEEDED) -{ fprintf(stderr, "json_to_u16 called!\n"); abort(); } -/* Generated stub for json_tok_bin_from_hex */ -u8 *json_tok_bin_from_hex(const tal_t *ctx UNNEEDED, const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED) -{ fprintf(stderr, "json_tok_bin_from_hex called!\n"); abort(); } /* Generated stub for last_fee_state */ enum htlc_state last_fee_state(enum side opener UNNEEDED) { fprintf(stderr, "last_fee_state called!\n"); abort(); }