Skip to content

Commit 9c035ab

Browse files
committed
bkpr: add two custom notifications that we listen for
It might be nice to let the bookkeeper keep track of external accounts as well as the internal onchain wallet? To this end, we add some new custom notifications, which the bookkeeper will ingest and add to its ledger. Changelog-Added: PLUGINS: `bookkeeper` now listens for two custom events: `coin_onchain_deposit` and `coin_onchain_spend`. This allows for 3rd party plugins to send onchain coin events to the `bookkeeper`. See the new plugins/bkpr/README.md for details on how these work!
1 parent 4c6966d commit 9c035ab

File tree

7 files changed

+410
-46
lines changed

7 files changed

+410
-46
lines changed

plugins/bkpr/README.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
The bookkeeper keeps track of coins moving through your Lightning node.
2+
3+
See the doc/PLUGINS.md#coin_movement section on the message that CLN emits for us to process.
4+
5+
// FIXME: add more detailed documenation for how bookkeeper works.
6+
7+
8+
## 3rd Party Coin Movements
9+
Bookeeper ingests 3rd party plugin notifications about on-chain movements that it should watch.
10+
11+
This allows for us to account for non-internal on-chain wallets in the single place, making `bookkeeper` your single source of truth for bitcoin for an organization or node-operator.
12+
13+
As a plugin writer, if you want to emit onchain events that the bookkeeper should track, you should emit an event with the following format:
14+
15+
```
16+
{
17+
"coin_onchain_deposit": {
18+
"account": "nifty's secret stash",
19+
"transfer_from: null,
20+
"outpoint": xxxx:x,
21+
"amount_msat": "10000sat",
22+
"coin_type": "bc",
23+
"timestamp": xxxx,
24+
"blockheight": xxx,
25+
}
26+
}
27+
```
28+
29+
```
30+
{
31+
"coin_onchain_spend": {
32+
"account": "nifty's secret stash",
33+
"outpoint": xxxx:x,
34+
"spending_txid": xxxx,
35+
"amount_msat": "10000sat",
36+
"coin_type": "bc",
37+
"timestamp": xxxx,
38+
"blockheight": xxx,
39+
}
40+
}
41+
```
42+
43+
44+
## Withdrawing money (sending to a external account)
45+
46+
Sending money to an external account is a bit unintuitive in in the UTXO model that we're using to track coin moves; technically a send to an external account is a "deposit" to 3rd party's UTXO.
47+
48+
To account for these, `bookkeeper` expects to receive a `coin_onchain_deposit` event for the creation of an output to a 3rd party. It's assumed that you'll issue these at transaction creation time, and that they won't be final until we receive notice of spend of the inputs in the tx that created them.
49+
50+
To notify that money is being sent to a 3rd party output, here's the event we'd expect.
51+
52+
The two keys here are the following:
53+
54+
- The `account` is `external`. This is a special account in `bookkeeper` and used for tracking external deposits (aka sends)
55+
- The `transfer_from` field is set to the name of the account that is sending out the money.
56+
57+
58+
```
59+
{
60+
"coin_onchain_deposit": {
61+
"account": "external",
62+
"transfer_from": "nifty's secret stash",
63+
"outpoint": xxxx:x,
64+
"amount_msat": "10000sat",
65+
"coin_type": "bc",
66+
"timestamp": xxxx,
67+
"blockheight": xxx,
68+
}
69+
}
70+
```
71+
72+
73+
## List of todos
74+
75+
List of things to check/work on, as a todo list.
76+
77+
- Transfers btw a 3rd party wallet and the internal CLN wallet? These should be registered as internal transfers and not show up in `listincome`

plugins/bkpr/bookkeeper.c

Lines changed: 180 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -693,7 +693,7 @@ static bool new_missed_channel_account(struct command *cmd,
693693

694694
chain_ev->credit = amt;
695695
db_begin_transaction(db);
696-
if (!log_chain_event(db, acct, chain_ev))
696+
if (!log_chain_event(chain_ev, db, acct, chain_ev))
697697
goto done;
698698

699699
maybe_update_account(db, acct, chain_ev,
@@ -1498,7 +1498,7 @@ parse_and_log_chain_move(struct command *cmd,
14981498
orig_acct = NULL;
14991499

15001500

1501-
if (!log_chain_event(db, acct, e)) {
1501+
if (!log_chain_event(e, db, acct, e)) {
15021502
db_commit_transaction(db);
15031503
/* This is not a new event, do nothing */
15041504
return notification_handled(cmd);
@@ -1530,7 +1530,8 @@ parse_and_log_chain_move(struct command *cmd,
15301530
/* Go see if there's any deposits to an external
15311531
* that are now confirmed */
15321532
/* FIXME: might need updating when we can splice? */
1533-
maybe_closeout_external_deposits(db, e);
1533+
maybe_closeout_external_deposits(db, e->spending_txid,
1534+
e->blockheight);
15341535
db_commit_transaction(db);
15351536
}
15361537

@@ -1685,6 +1686,173 @@ static char *parse_tags(const tal_t *ctx,
16851686
return NULL;
16861687
}
16871688

1689+
static struct command_result *json_coin_onchain_deposit(struct command *cmd, const char *buf, const jsmntok_t *params)
1690+
{
1691+
const char *move_tag ="coin_onchain_deposit";
1692+
struct chain_event ev;
1693+
struct account *acct;
1694+
const char *err;
1695+
1696+
err = json_scan(tmpctx, buf, params,
1697+
"{payload:{coin_onchain_deposit:{"
1698+
"account:%"
1699+
",transfer_from:%"
1700+
",outpoint:%"
1701+
",amount_msat:%"
1702+
",coin_type:%"
1703+
",timestamp:%"
1704+
",blockheight:%"
1705+
"}}}",
1706+
JSON_SCAN_TAL(tmpctx, json_strdup, &ev.acct_name),
1707+
JSON_SCAN_TAL(tmpctx, json_strdup, &ev.origin_acct),
1708+
JSON_SCAN(json_to_outpoint, &ev.outpoint),
1709+
JSON_SCAN(json_to_msat, &ev.credit),
1710+
JSON_SCAN_TAL(tmpctx, json_strdup, &ev.currency),
1711+
JSON_SCAN(json_to_u64, &ev.timestamp),
1712+
JSON_SCAN(json_to_u32, &ev.blockheight));
1713+
1714+
if (err)
1715+
plugin_err(cmd->plugin,
1716+
"`%s` payload did not scan %s: %.*s",
1717+
move_tag, err, json_tok_full_len(params),
1718+
json_tok_full(buf, params));
1719+
1720+
/* Log the thing */
1721+
db_begin_transaction(db);
1722+
acct = find_account(tmpctx, db, ev.acct_name);
1723+
1724+
if (!acct) {
1725+
acct = new_account(tmpctx, ev.acct_name, NULL);
1726+
account_add(db, acct);
1727+
}
1728+
1729+
ev.tag = "deposit";
1730+
ev.ignored = false;
1731+
ev.stealable = false;
1732+
ev.rebalance = false;
1733+
ev.debit = AMOUNT_MSAT(0);
1734+
ev.output_value = ev.credit;
1735+
ev.spending_txid = NULL;
1736+
ev.payment_id = NULL;
1737+
ev.desc = NULL;
1738+
1739+
plugin_log(cmd->plugin, LOG_DBG, "%s (%s|%s) %s -%s %"PRIu64" %d %s",
1740+
move_tag, ev.tag, ev.acct_name,
1741+
type_to_string(tmpctx, struct amount_msat, &ev.credit),
1742+
type_to_string(tmpctx, struct amount_msat, &ev.debit),
1743+
ev.timestamp, ev.blockheight,
1744+
type_to_string(tmpctx, struct bitcoin_outpoint, &ev.outpoint));
1745+
1746+
if (!log_chain_event(cmd, db, acct, &ev)) {
1747+
db_commit_transaction(db);
1748+
/* This is not a new event, do nothing */
1749+
return notification_handled(cmd);
1750+
}
1751+
1752+
/* Can we calculate any onchain fees now? */
1753+
err = maybe_update_onchain_fees(cmd, db, &ev.outpoint.txid);
1754+
db_commit_transaction(db);
1755+
if (err)
1756+
plugin_err(cmd->plugin,
1757+
"Unable to update onchain fees %s",
1758+
err);
1759+
1760+
/* FIXME: do account close checks, when allow onchain close to externals? */
1761+
return notification_handled(cmd);;
1762+
}
1763+
1764+
static struct command_result *json_coin_onchain_spend(struct command *cmd, const char *buf, const jsmntok_t *params)
1765+
{
1766+
const char *move_tag ="coin_onchain_spend";
1767+
struct account *acct;
1768+
struct chain_event ev;
1769+
const char *err, *acct_name;
1770+
1771+
ev.spending_txid = tal(cmd, struct bitcoin_txid);
1772+
err = json_scan(tmpctx, buf, params,
1773+
"{payload:{coin_onchain_spend:{"
1774+
"account:%"
1775+
",outpoint:%"
1776+
",spending_txid:%"
1777+
",amount_msat:%"
1778+
",coin_type:%"
1779+
",timestamp:%"
1780+
",blockheight:%"
1781+
"}}}",
1782+
JSON_SCAN_TAL(tmpctx, json_strdup, &acct_name),
1783+
JSON_SCAN(json_to_outpoint, &ev.outpoint),
1784+
JSON_SCAN(json_to_txid, ev.spending_txid),
1785+
JSON_SCAN(json_to_msat, &ev.debit),
1786+
JSON_SCAN_TAL(tmpctx, json_strdup, &ev.currency),
1787+
JSON_SCAN(json_to_u64, &ev.timestamp),
1788+
JSON_SCAN(json_to_u32, &ev.blockheight));
1789+
1790+
if (err)
1791+
plugin_err(cmd->plugin,
1792+
"`%s` payload did not scan %s: %.*s",
1793+
move_tag, err, json_tok_full_len(params),
1794+
json_tok_full(buf, params));
1795+
1796+
/* Log the thing */
1797+
db_begin_transaction(db);
1798+
acct = find_account(tmpctx, db, acct_name);
1799+
1800+
if (!acct) {
1801+
acct = new_account(tmpctx, acct_name, NULL);
1802+
account_add(db, acct);
1803+
}
1804+
1805+
ev.origin_acct = NULL;
1806+
ev.tag = "withdrawal";
1807+
ev.ignored = false;
1808+
ev.stealable = false;
1809+
ev.rebalance = false;
1810+
ev.credit = AMOUNT_MSAT(0);
1811+
ev.output_value = ev.debit;
1812+
ev.payment_id = NULL;
1813+
ev.desc = NULL;
1814+
1815+
plugin_log(cmd->plugin, LOG_DBG, "%s (%s|%s) %s -%s %"PRIu64" %d %s %s",
1816+
move_tag, ev.tag, acct_name,
1817+
type_to_string(tmpctx, struct amount_msat, &ev.credit),
1818+
type_to_string(tmpctx, struct amount_msat, &ev.debit),
1819+
ev.timestamp, ev.blockheight,
1820+
type_to_string(tmpctx, struct bitcoin_outpoint, &ev.outpoint),
1821+
type_to_string(tmpctx, struct bitcoin_txid, ev.spending_txid));
1822+
1823+
if (!log_chain_event(cmd, db, acct, &ev)) {
1824+
db_commit_transaction(db);
1825+
/* This is not a new event, do nothing */
1826+
return notification_handled(cmd);
1827+
}
1828+
1829+
err = maybe_update_onchain_fees(cmd, db, ev.spending_txid);
1830+
if (err) {
1831+
db_commit_transaction(db);
1832+
plugin_err(cmd->plugin,
1833+
"Unable to update onchain fees %s",
1834+
err);
1835+
}
1836+
1837+
err = maybe_update_onchain_fees(cmd, db, &ev.outpoint.txid);
1838+
if (err) {
1839+
db_commit_transaction(db);
1840+
plugin_err(cmd->plugin,
1841+
"Unable to update onchain fees %s",
1842+
err);
1843+
}
1844+
1845+
/* Go see if there's any deposits to an external
1846+
* that are now confirmed */
1847+
/* FIXME: might need updating when we can splice? */
1848+
maybe_closeout_external_deposits(db, ev.spending_txid,
1849+
ev.blockheight);
1850+
db_commit_transaction(db);
1851+
1852+
/* FIXME: do account close checks, when allow onchain close to externals? */
1853+
return notification_handled(cmd);;
1854+
}
1855+
16881856
static struct command_result *json_coin_moved(struct command *cmd,
16891857
const char *buf,
16901858
const jsmntok_t *params)
@@ -1760,7 +1928,15 @@ const struct plugin_notification notifs[] = {
17601928
{
17611929
"balance_snapshot",
17621930
json_balance_snapshot,
1763-
}
1931+
},
1932+
{
1933+
"coin_onchain_deposit",
1934+
json_coin_onchain_deposit,
1935+
},
1936+
{
1937+
"coin_onchain_spend",
1938+
json_coin_onchain_spend,
1939+
},
17641940
};
17651941

17661942
static const struct plugin_command commands[] = {

plugins/bkpr/recorder.c

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1939,11 +1939,12 @@ char *maybe_update_onchain_fees(const tal_t *ctx, struct db *db,
19391939
}
19401940

19411941
void maybe_closeout_external_deposits(struct db *db,
1942-
struct chain_event *ev)
1942+
struct bitcoin_txid *txid,
1943+
u32 blockheight)
19431944
{
19441945
struct db_stmt *stmt;
19451946

1946-
assert(ev->spending_txid);
1947+
assert(txid);
19471948
stmt = db_prepare_v2(db, SQL("SELECT "
19481949
" e.id"
19491950
" FROM chain_events e"
@@ -1955,7 +1956,7 @@ void maybe_closeout_external_deposits(struct db *db,
19551956

19561957
/* Blockheight for unconfirmeds is zero */
19571958
db_bind_int(stmt, 0, 0);
1958-
db_bind_txid(stmt, 1, ev->spending_txid);
1959+
db_bind_txid(stmt, 1, txid);
19591960
db_bind_text(stmt, 2, EXTERNAL_ACCT);
19601961
db_query_prepared(stmt);
19611962

@@ -1968,15 +1969,15 @@ void maybe_closeout_external_deposits(struct db *db,
19681969
" blockheight = ?"
19691970
" WHERE id = ?"));
19701971

1971-
db_bind_int(update_stmt, 0, ev->blockheight);
1972+
db_bind_int(update_stmt, 0, blockheight);
19721973
db_bind_u64(update_stmt, 1, id);
19731974
db_exec_prepared_v2(take(update_stmt));
19741975
}
19751976

19761977
tal_free(stmt);
19771978
}
19781979

1979-
bool log_chain_event(struct db *db,
1980+
bool log_chain_event(const tal_t *ctx, struct db *db,
19801981
const struct account *acct,
19811982
struct chain_event *e)
19821983
{
@@ -2044,7 +2045,7 @@ bool log_chain_event(struct db *db,
20442045
db_exec_prepared_v2(stmt);
20452046
e->db_id = db_last_insert_id_v2(stmt);
20462047
e->acct_db_id = acct->db_id;
2047-
e->acct_name = tal_strdup(e, acct->name);
2048+
e->acct_name = tal_strdup(ctx, acct->name);
20482049
tal_free(stmt);
20492050
return true;
20502051
}

plugins/bkpr/recorder.h

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,9 @@ void add_payment_hash_desc(struct db *db,
208208
*
209209
* This method updates the blockheight on these events to the
210210
* height an input was spent into */
211-
void maybe_closeout_external_deposits(struct db *db, struct chain_event *ev);
211+
void maybe_closeout_external_deposits(struct db *db,
212+
struct bitcoin_txid *txid,
213+
u32 blockheight);
212214

213215
/* Keep track of rebalancing payments (payments paid to/from ourselves.
214216
* Returns true if was rebalance */
@@ -224,8 +226,9 @@ void log_channel_event(struct db *db,
224226
struct channel_event *e);
225227

226228
/* Log a chain event.
227-
* Returns true if inserted, false if already exists */
228-
bool log_chain_event(struct db *db,
229+
* Returns true if inserted, false if already exists;
230+
* ctx is for allocating objects onto chain_event `e` */
231+
bool log_chain_event(const tal_t *ctx, struct db *db,
229232
const struct account *acct,
230233
struct chain_event *e);
231234

0 commit comments

Comments
 (0)