wallet: generate fixup chainmoves and channelmoves when first starting.
What changed, and why it matters
This commit finishes a previously-stubbed database migration that invents historical accounting records ('coin movements') for nodes that never ran the optional bookkeeper plugin. It does not change how funds are secured on the blockchain; it only backfills internal ledger entries so balances displayed by the accounting plugin look correct after an upgrade. There is no obvious way for an external attacker to exploit it, but any migration that writes made-up ledger entries carries a risk of miscounting funds or confusing downstream tools if the invented numbers are wrong.
Treat this as a data-integrity migration rather than an active vulnerability. Operators upgrading without a bookkeeper accountdb should verify that bkpr-listaccountevents and channel balances look reasonable after first startup, and report discrepancies before relying on the migrated records for accounting or tax purposes. Developers should review the synthetic journal-entry arithmetic and edge cases around closed channels, lease fees, and push amounts.
Security signals we found
Migration writes synthetic accounting records derived from existing on-chain/channel state
Use of abort() on arithmetic overflow/underflow in balance calculations
New generalized constructors accept explicit channel_id and timestamp, changing assumptions previously tied to live channel objects
Previously stubbed migration function now executes automatically at startup when no bookkeeper db exists
Evidence from the diff
The patch implements migrate_setup_coinmoves() in wallet/wallet.c. When a node starts without an existing bookkeeper accountdb, it scans confirmed UTXOs and open channels and synthesizes chain_moves/channel_moves rows: wallet deposits, channel_open events, pushed/leased channel transfers, and a balancing ‘journal’ entry if the node’s current channel balance does not match the synthetic starting balance. To support this, coin-movement constructors were generalized to accept an explicit channel_id and timestamp, and the chain/channel insert logic was refactored so it can write to an arbitrary db handle during migration. Tests using pre-upgrade SQLite snapshots were added to verify the expected synthetic events.
Changed components
wallet/wallet.ccommon/coin_mvt.ccommon/coin_mvt.hplugins/bkpr (bookkeeper accounting plugin)channel_moves and chain_moves database tablesInspect captured patch +608 / −93
diff --git a/common/coin_mvt.c b/common/coin_mvt.c
index a31a8f3e..20168317 100644
--- a/common/coin_mvt.c
+++ b/common/coin_mvt.c
@@ -207,21 +207,22 @@ struct mvt_account_id *new_mvt_account_id(const tal_t *ctx,
return acct;
}
-struct channel_coin_mvt *new_channel_coin_mvt(const tal_t *ctx,
- const struct channel *channel,
- u64 timestamp,
- const struct sha256 *payment_hash TAKES,
- const u64 *part_id,
- const u64 *group_id,
- enum coin_mvt_dir direction,
- struct amount_msat amount,
- struct mvt_tags tags,
- struct amount_msat fees)
+struct channel_coin_mvt *new_channel_coin_mvt_general(const tal_t *ctx,
+ const struct channel *channel,
+ const struct channel_id *cid,
+ u64 timestamp,
+ const struct sha256 *payment_hash TAKES,
+ const u64 *part_id,
+ const u64 *group_id,
+ enum coin_mvt_dir direction,
+ struct amount_msat amount,
+ struct mvt_tags tags,
+ struct amount_msat fees)
{
struct channel_coin_mvt *mvt = tal(ctx, struct channel_coin_mvt);
assert(mvt_tags_valid(tags));
- set_mvt_account_id(&mvt->account, channel, NULL);
+ set_mvt_account_id(&mvt->account, channel, cid ? take(fmt_channel_id(NULL, cid)) : NULL);
mvt->timestamp = timestamp;
mvt->payment_hash = tal_dup_or_null(mvt, struct sha256, payment_hash);
if (!part_id) {
@@ -251,6 +252,21 @@ struct channel_coin_mvt *new_channel_coin_mvt(const tal_t *ctx,
abort();
}
+struct channel_coin_mvt *new_channel_coin_mvt(const tal_t *ctx,
+ const struct channel *channel,
+ u64 timestamp,
+ const struct sha256 *payment_hash TAKES,
+ const u64 *part_id,
+ const u64 *group_id,
+ enum coin_mvt_dir direction,
+ struct amount_msat amount,
+ struct mvt_tags tags,
+ struct amount_msat fees)
+{
+ return new_channel_coin_mvt_general(ctx, channel, NULL, timestamp, payment_hash,
+ part_id, group_id, direction, amount, tags, fees);
+}
+
static struct chain_coin_mvt *new_chain_coin_mvt(const tal_t *ctx,
const struct channel *channel,
const char *account_name TAKES,
@@ -408,15 +424,17 @@ struct chain_coin_mvt *new_coin_channel_open_proposed(const tal_t *ctx,
return mvt;
}
-struct chain_coin_mvt *new_coin_channel_open(const tal_t *ctx,
- const struct channel *channel,
- const struct bitcoin_outpoint *out,
- const struct node_id *peer_id,
- u32 blockheight,
- const struct amount_msat amount,
- const struct amount_sat output_val,
- bool is_opener,
- bool is_leased)
+struct chain_coin_mvt *new_coin_channel_open_general(const tal_t *ctx,
+ const struct channel *channel,
+ const struct channel_id *cid,
+ u64 timestamp,
+ const struct bitcoin_outpoint *out,
+ const struct node_id *peer_id,
+ u32 blockheight,
+ const struct amount_msat amount,
+ const struct amount_sat output_val,
+ bool is_opener,
+ bool is_leased)
{
struct chain_coin_mvt *mvt;
struct mvt_tags tags = tag_to_mvt_tags(MVT_CHANNEL_OPEN);
@@ -428,7 +446,8 @@ struct chain_coin_mvt *new_coin_channel_open(const tal_t *ctx,
if (is_leased)
mvt_tag_set(&tags, MVT_LEASED);
- mvt = new_chain_coin_mvt(ctx, channel, NULL, time_now().ts.tv_sec,
+ mvt = new_chain_coin_mvt(ctx, channel, cid ? take(fmt_channel_id(NULL, cid)) : NULL,
+ timestamp,
NULL, out, NULL, blockheight,
tags,
COIN_CREDIT, amount,
@@ -438,6 +457,22 @@ struct chain_coin_mvt *new_coin_channel_open(const tal_t *ctx,
return mvt;
}
+struct chain_coin_mvt *new_coin_channel_open(const tal_t *ctx,
+ const struct channel *channel,
+ const struct bitcoin_outpoint *out,
+ const struct node_id *peer_id,
+ u32 blockheight,
+ const struct amount_msat amount,
+ const struct amount_sat output_val,
+ bool is_opener,
+ bool is_leased)
+{
+ return new_coin_channel_open_general(ctx, channel, NULL,
+ time_now().ts.tv_sec,
+ out, peer_id, blockheight,
+ amount, output_val, is_opener, is_leased);
+}
+
struct chain_coin_mvt *new_onchain_htlc_deposit(const tal_t *ctx,
const struct bitcoin_outpoint *outpoint,
u32 blockheight,
@@ -522,16 +557,29 @@ struct chain_coin_mvt *new_coin_wallet_withdraw(const tal_t *ctx,
COIN_DEBIT, amount);
}
+struct channel_coin_mvt *new_coin_channel_push_general(const tal_t *ctx,
+ const struct channel *channel,
+ const struct channel_id *cid,
+ u64 timestamp,
+ enum coin_mvt_dir direction,
+ struct amount_msat amount,
+ struct mvt_tags tags)
+{
+ return new_channel_coin_mvt_general(ctx, channel, cid, timestamp, NULL,
+ NULL, NULL, direction, amount,
+ tags,
+ AMOUNT_MSAT(0));
+}
+
struct channel_coin_mvt *new_coin_channel_push(const tal_t *ctx,
const struct channel *channel,
enum coin_mvt_dir direction,
struct amount_msat amount,
struct mvt_tags tags)
{
- return new_channel_coin_mvt(ctx, channel, time_now().ts.tv_sec, NULL,
- NULL, NULL, direction, amount,
- tags,
- AMOUNT_MSAT(0));
+ return new_coin_channel_push_general(ctx, channel, NULL,
+ time_now().ts.tv_sec,
+ direction, amount, tags);
}
struct chain_coin_mvt *new_foreign_deposit(const tal_t *ctx,
diff --git a/common/coin_mvt.h b/common/coin_mvt.h
index feef4883..50d3e2b1 100644
--- a/common/coin_mvt.h
+++ b/common/coin_mvt.h
@@ -280,6 +280,39 @@ struct chain_coin_mvt *new_foreign_withdrawal(const tal_t *ctx,
u64 timestamp)
NON_NULL_ARGS(2, 3, 6);
+/* Generic versions (prefer the above ones: these are for migrations) */
+struct channel_coin_mvt *new_channel_coin_mvt_general(const tal_t *ctx,
+ const struct channel *channel,
+ const struct channel_id *cid,
+ u64 timestamp,
+ const struct sha256 *payment_hash TAKES,
+ const u64 *part_id,
+ const u64 *group_id,
+ enum coin_mvt_dir direction,
+ struct amount_msat amount,
+ struct mvt_tags tags,
+ struct amount_msat fees);
+
+struct chain_coin_mvt *new_coin_channel_open_general(const tal_t *ctx,
+ const struct channel *channel,
+ const struct channel_id *cid,
+ u64 timestamp,
+ const struct bitcoin_outpoint *out,
+ const struct node_id *peer_id,
+ u32 blockheight,
+ const struct amount_msat amount,
+ const struct amount_sat output_val,
+ bool is_opener,
+ bool is_leased);
+
+struct channel_coin_mvt *new_coin_channel_push_general(const tal_t *ctx,
+ const struct channel *channel,
+ const struct channel_id *cid,
+ u64 timestamp,
+ enum coin_mvt_dir direction,
+ struct amount_msat amount,
+ struct mvt_tags tags);
+
/* There are three standard accounts:
* "wallet" for our internal wallet,
* "external" for other bitcoin sources,
diff --git a/common/test/run-coin_mvt.c b/common/test/run-coin_mvt.c
index a61bcb73..6fbac51c 100644
--- a/common/test/run-coin_mvt.c
+++ b/common/test/run-coin_mvt.c
@@ -43,6 +43,9 @@ struct amount_asset amount_sat_to_asset(struct amount_sat *sat UNNEEDED, const u
/* Generated stub for amount_tx_fee */
struct amount_sat amount_tx_fee(u32 fee_per_kw UNNEEDED, size_t weight UNNEEDED)
{ fprintf(stderr, "amount_tx_fee 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 fromwire */
const u8 *fromwire(const u8 **cursor UNNEEDED, size_t *max UNNEEDED, void *copy UNNEEDED, size_t n UNNEEDED)
{ fprintf(stderr, "fromwire called!\n"); abort(); }
diff --git a/common/test/run-route_blinding_test.c b/common/test/run-route_blinding_test.c
index 799c540e..3358319d 100644
--- a/common/test/run-route_blinding_test.c
+++ b/common/test/run-route_blinding_test.c
@@ -17,6 +17,9 @@
#include <stdio.h>
/* AUTOGENERATED MOCKS START */
+/* 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 fromwire_channel_id */
bool fromwire_channel_id(const u8 **cursor UNNEEDED, size_t *max UNNEEDED,
struct channel_id *channel_id UNNEEDED)
diff --git a/plugins/bkpr/test/run-sql.c b/plugins/bkpr/test/run-sql.c
index 3d497492..595661ae 100644
--- a/plugins/bkpr/test/run-sql.c
+++ b/plugins/bkpr/test/run-sql.c
@@ -35,6 +35,9 @@ u32 find_blockheight(const struct bkpr *bkpr UNNEEDED, const struct bitcoin_txid
/* 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(); }
diff --git a/tests/test_bookkeeper.py b/tests/test_bookkeeper.py
index 30b004e9..c80c03fb 100644
--- a/tests/test_bookkeeper.py
+++ b/tests/test_bookkeeper.py
@@ -1083,3 +1083,64 @@ def test_migration(node_factory, bitcoind):
'payment_id': '7ccef7e9fabbf4a841af44b1fc7319bc70ce98697b77ce6dacffa84bebcd4350',
'tag': 'invoice',
'type': 'channel'}]
+
+
+@unittest.skipIf(TEST_NETWORK != 'regtest', "Snapshots are bitcoin regtest.")
+@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "uses snapshots")
+def test_migration_no_bkpr(node_factory, bitcoind):
+ """These nodes need to invent coinmoves to make the balances work"""
+ bitcoind.generate_block(1)
+ l1 = node_factory.get_node(dbfile="l1-before-moves-in-db.sqlite3.xz",
+ options={'database-upgrade': True})
+ l2 = node_factory.get_node(dbfile="l2-before-moves-in-db.sqlite3.xz",
+ options={'database-upgrade': True})
+
+ chan = only_one(l1.rpc.listpeerchannels()['channels'])
+
+ l1_events = l1.rpc.bkpr_listaccountevents()['events']
+ for e in l1_events:
+ del e['timestamp']
+
+ l2_events = l2.rpc.bkpr_listaccountevents()['events']
+ for e in l2_events:
+ del e['timestamp']
+
+ assert l1_events == [{'account': chan['channel_id'],
+ 'blockheight': 103,
+ 'credit_msat': 1000000000,
+ 'currency': 'bcrt',
+ 'debit_msat': 0,
+ 'outpoint': f"{chan['funding_txid']}:{chan['funding_outnum']}",
+ 'tag': 'channel_open',
+ 'type': 'chain'},
+ {'account': 'wallet',
+ 'blockheight': 103,
+ 'credit_msat': 995073000,
+ 'currency': 'bcrt',
+ 'debit_msat': 0,
+ 'outpoint': f"{chan['funding_txid']}:{chan['funding_outnum'] ^ 1}",
+ 'tag': 'deposit',
+ 'type': 'chain'},
+ {'account': chan['channel_id'],
+ 'credit_msat': 0,
+ 'currency': 'bcrt',
+ 'debit_msat': 12345678,
+ 'is_rebalance': False,
+ 'tag': 'journal',
+ 'type': 'channel'}]
+
+ assert l2_events == [{'account': chan['channel_id'],
+ 'blockheight': 103,
+ 'credit_msat': 0,
+ 'currency': 'bcrt',
+ 'debit_msat': 0,
+ 'outpoint': f"{chan['funding_txid']}:{chan['funding_outnum']}",
+ 'tag': 'channel_open',
+ 'type': 'chain'},
+ {'account': chan['channel_id'],
+ 'credit_msat': 12345678,
+ 'currency': 'bcrt',
+ 'debit_msat': 0,
+ 'is_rebalance': False,
+ 'tag': 'journal',
+ 'type': 'channel'}]
diff --git a/tests/test_coinmoves.py b/tests/test_coinmoves.py
index 3a0f757d..485361f3 100644
--- a/tests/test_coinmoves.py
+++ b/tests/test_coinmoves.py
@@ -4,6 +4,7 @@ from utils import (
sync_blockheight, wait_for, only_one, TIMEOUT
)
+import os
import unittest
import pytest
import re
@@ -1917,3 +1918,147 @@ def test_wait(node_factory, bitcoind, executor):
'channelmoves': {'account': fund['channel_id'],
'debit_msat': 1000000000,
'credit_msat': 0}}
+
+
+@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "uses snapshots")
+@unittest.skipIf(TEST_NETWORK != 'regtest', "Snapshots are bitcoin regtest.")
+def test_migration(node_factory, bitcoind):
+ """These nodes import coinmoves from the old bookkeeper account.db"""
+ bitcoind.generate_block(1)
+ l1 = node_factory.get_node(dbfile="l1-before-moves-in-db.sqlite3.xz",
+ bkpr_dbfile="l1-bkpr-accounts.sqlite3.xz",
+ options={'database-upgrade': True})
+ l2 = node_factory.get_node(dbfile="l2-before-moves-in-db.sqlite3.xz",
+ bkpr_dbfile="l2-bkpr-accounts.sqlite3.xz",
+ options={'database-upgrade': True})
+ chan = only_one(l1.rpc.listpeerchannels()['channels'])
+ payment = only_one(l1.rpc.listsendpays()['payments'])
+
+ expected_channel1 = [{'account_id': chan['channel_id'],
+ 'created_index': 1,
+ 'credit_msat': 0,
+ 'debit_msat': 12345678,
+ 'fees_msat': 0,
+ 'payment_hash': payment['payment_hash'],
+ 'primary_tag': 'invoice'}]
+ expected_channel2 = [{'account_id': chan['channel_id'],
+ 'created_index': 1,
+ 'credit_msat': 12345678,
+ 'debit_msat': 0,
+ 'fees_msat': 0,
+ 'payment_hash': payment['payment_hash'],
+ 'primary_tag': 'invoice'}]
+ expected_chain1 = [{'account_id': 'wallet',
+ 'blockheight': 102,
+ 'created_index': 1,
+ 'credit_msat': 2000000000,
+ 'debit_msat': 0,
+ 'extra_tags': [],
+ 'output_msat': 2000000000,
+ 'primary_tag': 'deposit',
+ 'utxo': '63c59b312976320528552c258ae51563498dfd042b95bb0c842696614d59bb89:1'},
+ {'account_id': 'wallet',
+ 'blockheight': 103,
+ 'created_index': 2,
+ 'credit_msat': 0,
+ 'debit_msat': 2000000000,
+ 'extra_tags': [],
+ 'output_msat': 2000000000,
+ 'primary_tag': 'withdrawal',
+ 'spending_txid': chan['funding_txid'],
+ 'utxo': '63c59b312976320528552c258ae51563498dfd042b95bb0c842696614d59bb89:1'},
+ {'account_id': 'wallet',
+ 'blockheight': 103,
+ 'created_index': 3,
+ 'credit_msat': 995073000,
+ 'debit_msat': 0,
+ 'extra_tags': [],
+ 'output_msat': 995073000,
+ 'primary_tag': 'deposit',
+ 'utxo': f"{chan['funding_txid']}:{chan['funding_outnum'] ^ 1}"},
+ {'account_id': chan['channel_id'],
+ 'blockheight': 103,
+ 'created_index': 4,
+ 'credit_msat': 1000000000,
+ 'debit_msat': 0,
+ 'extra_tags': ['opener'],
+ 'output_msat': 1000000000,
+ 'peer_id': l2.info['id'],
+ 'primary_tag': 'channel_open',
+ 'utxo': f"{chan['funding_txid']}:{chan['funding_outnum']}"}]
+ expected_chain2 = [{'account_id': chan['channel_id'],
+ 'blockheight': 103,
+ 'created_index': 1,
+ 'credit_msat': 0,
+ 'debit_msat': 0,
+ 'extra_tags': [],
+ 'output_msat': 1000000000,
+ 'peer_id': l1.info['id'],
+ 'primary_tag': 'channel_open',
+ 'utxo': f"{chan['funding_txid']}:{chan['funding_outnum']}"}]
+ check_channel_moves(l1, expected_channel1)
+ check_channel_moves(l2, expected_channel2)
+ check_chain_moves(l1, expected_chain1)
+ check_chain_moves(l2, expected_chain2)
+
+
+@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "uses snapshots")
+@unittest.skipIf(TEST_NETWORK != 'regtest', "Snapshots are for regtest.")
+def test_migration_no_bkpr(node_factory, bitcoind):
+ """These nodes need to invent coinmoves to make the balances work"""
+ bitcoind.generate_block(1)
+ l1 = node_factory.get_node(dbfile="l1-before-moves-in-db.sqlite3.xz",
+ options={'database-upgrade': True})
+ l2 = node_factory.get_node(dbfile="l2-before-moves-in-db.sqlite3.xz",
+ options={'database-upgrade': True})
+
+ chan = only_one(l1.rpc.listpeerchannels()['channels'])
+
+ expected_channel1 = [{'account_id': chan['channel_id'],
+ 'created_index': 1,
+ 'credit_msat': 0,
+ 'debit_msat': 12345678,
+ 'fees_msat': 0,
+ 'primary_tag': 'journal',
+ }]
+ expected_channel2 = [{'account_id': chan['channel_id'],
+ 'created_index': 1,
+ 'credit_msat': 12345678,
+ 'debit_msat': 0,
+ 'fees_msat': 0,
+ 'primary_tag': 'journal',
+ }]
+ expected_chain1 = [{'account_id': 'wallet',
+ 'blockheight': 103,
+ 'created_index': 1,
+ 'credit_msat': 995073000,
+ 'debit_msat': 0,
+ 'extra_tags': [],
+ 'output_msat': 995073000,
+ 'primary_tag': 'deposit',
+ 'utxo': f"{chan['funding_txid']}:{chan['funding_outnum'] ^ 1}"},
+ {'account_id': chan['channel_id'],
+ 'blockheight': 103,
+ 'created_index': 2,
+ 'credit_msat': 1000000000,
+ 'debit_msat': 0,
+ 'extra_tags': ['opener'],
+ 'output_msat': 1000000000,
+ 'peer_id': l2.info['id'],
+ 'primary_tag': 'channel_open',
+ 'utxo': f"{chan['funding_txid']}:{chan['funding_outnum']}"}]
+ expected_chain2 = [{'account_id': chan['channel_id'],
+ 'blockheight': 103,
+ 'created_index': 1,
+ 'credit_msat': 0,
+ 'debit_msat': 0,
+ 'extra_tags': [],
+ 'output_msat': 1000000000,
+ 'peer_id': l1.info['id'],
+ 'primary_tag': 'channel_open',
+ 'utxo': f"{chan['funding_txid']}:{chan['funding_outnum']}"}]
+
+ check_channel_moves(l1, expected_channel1)
+ check_channel_moves(l2, expected_channel2)
+ check_chain_moves(l1, expected_chain1)
+ check_chain_moves(l2, expected_chain2)
diff --git a/wallet/test/run-db.c b/wallet/test/run-db.c
index ce3e2b9f..106cd380 100644
--- a/wallet/test/run-db.c
+++ b/wallet/test/run-db.c
@@ -256,6 +256,19 @@ struct channel *new_channel(struct peer *peer UNNEEDED, u64 dbid UNNEEDED,
const struct channel_stats *stats UNNEEDED,
struct channel_state_change **state_changes STEALS UNNEEDED)
{ fprintf(stderr, "new_channel called!\n"); abort(); }
+/* Generated stub for new_channel_coin_mvt_general */
+struct channel_coin_mvt *new_channel_coin_mvt_general(const tal_t *ctx UNNEEDED,
+ const struct channel *channel UNNEEDED,
+ const struct channel_id *cid UNNEEDED,
+ u64 timestamp UNNEEDED,
+ const struct sha256 *payment_hash TAKES UNNEEDED,
+ const u64 *part_id UNNEEDED,
+ const u64 *group_id UNNEEDED,
+ enum coin_mvt_dir direction UNNEEDED,
+ struct amount_msat amount UNNEEDED,
+ struct mvt_tags tags UNNEEDED,
+ struct amount_msat fees UNNEEDED)
+{ fprintf(stderr, "new_channel_coin_mvt_general called!\n"); abort(); }
/* Generated stub for new_channel_state_change */
struct channel_state_change *new_channel_state_change(const tal_t *ctx UNNEEDED,
struct timeabs timestamp UNNEEDED,
@@ -264,6 +277,28 @@ struct channel_state_change *new_channel_state_change(const tal_t *ctx UNNEEDED,
enum state_change cause UNNEEDED,
const char *message TAKES UNNEEDED)
{ fprintf(stderr, "new_channel_state_change called!\n"); abort(); }
+/* Generated stub for new_coin_channel_open_general */
+struct chain_coin_mvt *new_coin_channel_open_general(const tal_t *ctx UNNEEDED,
+ const struct channel *channel UNNEEDED,
+ const struct channel_id *cid UNNEEDED,
+ u64 timestamp UNNEEDED,
+ const struct bitcoin_outpoint *out UNNEEDED,
+ const struct node_id *peer_id UNNEEDED,
+ u32 blockheight UNNEEDED,
+ const struct amount_msat amount UNNEEDED,
+ const struct amount_sat output_val UNNEEDED,
+ bool is_opener UNNEEDED,
+ bool is_leased UNNEEDED)
+{ fprintf(stderr, "new_coin_channel_open_general called!\n"); abort(); }
+/* Generated stub for new_coin_channel_push_general */
+struct channel_coin_mvt *new_coin_channel_push_general(const tal_t *ctx UNNEEDED,
+ const struct channel *channel UNNEEDED,
+ const struct channel_id *cid UNNEEDED,
+ u64 timestamp UNNEEDED,
+ enum coin_mvt_dir direction UNNEEDED,
+ struct amount_msat amount UNNEEDED,
+ struct mvt_tags tags UNNEEDED)
+{ fprintf(stderr, "new_coin_channel_push_general called!\n"); abort(); }
/* Generated stub for new_coin_wallet_deposit */
struct chain_coin_mvt *new_coin_wallet_deposit(const tal_t *ctx UNNEEDED,
const struct bitcoin_outpoint *outpoint UNNEEDED,
diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c
index 421bb6a7..92ec32bc 100644
--- a/wallet/test/run-wallet.c
+++ b/wallet/test/run-wallet.c
@@ -661,6 +661,19 @@ struct mvt_tags mk_mvt_tags_(enum mvt_tag tag UNNEEDED, ...)
/* Generated stub for mvt_tags_valid */
bool mvt_tags_valid(struct mvt_tags tags UNNEEDED)
{ fprintf(stderr, "mvt_tags_valid called!\n"); abort(); }
+/* Generated stub for new_channel_coin_mvt_general */
+struct channel_coin_mvt *new_channel_coin_mvt_general(const tal_t *ctx UNNEEDED,
+ const struct channel *channel UNNEEDED,
+ const struct channel_id *cid UNNEEDED,
+ u64 timestamp UNNEEDED,
+ const struct sha256 *payment_hash TAKES UNNEEDED,
+ const u64 *part_id UNNEEDED,
+ const u64 *group_id UNNEEDED,
+ enum coin_mvt_dir direction UNNEEDED,
+ struct amount_msat amount UNNEEDED,
+ struct mvt_tags tags UNNEEDED,
+ struct amount_msat fees UNNEEDED)
+{ fprintf(stderr, "new_channel_coin_mvt_general called!\n"); abort(); }
/* Generated stub for new_channel_mvt_invoice_hin */
struct channel_coin_mvt *new_channel_mvt_invoice_hin(const tal_t *ctx UNNEEDED,
const struct htlc_in *hin UNNEEDED,
@@ -681,6 +694,28 @@ struct channel_coin_mvt *new_channel_mvt_routed_hout(const tal_t *ctx UNNEEDED,
const struct htlc_out *hout UNNEEDED,
const struct channel *channel UNNEEDED)
{ fprintf(stderr, "new_channel_mvt_routed_hout called!\n"); abort(); }
+/* Generated stub for new_coin_channel_open_general */
+struct chain_coin_mvt *new_coin_channel_open_general(const tal_t *ctx UNNEEDED,
+ const struct channel *channel UNNEEDED,
+ const struct channel_id *cid UNNEEDED,
+ u64 timestamp UNNEEDED,
+ const struct bitcoin_outpoint *out UNNEEDED,
+ const struct node_id *peer_id UNNEEDED,
+ u32 blockheight UNNEEDED,
+ const struct amount_msat amount UNNEEDED,
+ const struct amount_sat output_val UNNEEDED,
+ bool is_opener UNNEEDED,
+ bool is_leased UNNEEDED)
+{ fprintf(stderr, "new_coin_channel_open_general called!\n"); abort(); }
+/* Generated stub for new_coin_channel_push_general */
+struct channel_coin_mvt *new_coin_channel_push_general(const tal_t *ctx UNNEEDED,
+ const struct channel *channel UNNEEDED,
+ const struct channel_id *cid UNNEEDED,
+ u64 timestamp UNNEEDED,
+ enum coin_mvt_dir direction UNNEEDED,
+ struct amount_msat amount UNNEEDED,
+ struct mvt_tags tags UNNEEDED)
+{ fprintf(stderr, "new_coin_channel_push_general called!\n"); abort(); }
/* Generated stub for new_coin_wallet_deposit */
struct chain_coin_mvt *new_coin_wallet_deposit(const tal_t *ctx UNNEEDED,
const struct bitcoin_outpoint *outpoint UNNEEDED,
diff --git a/wallet/wallet.c b/wallet/wallet.c
index 46201216..ddc933d7 100644
--- a/wallet/wallet.c
+++ b/wallet/wallet.c
@@ -499,19 +499,11 @@ struct utxo **wallet_get_all_utxos(const tal_t *ctx, struct wallet *w)
return gather_utxos(ctx, stmt);
}
-/**
- * wallet_get_unspent_utxos - Return reserved and unreserved UTXOs.
- *
- * Returns a `tal_arr` of `utxo` structs. Double indirection in order
- * to be able to steal individual elements onto something else.
- *
- * Use utxo_is_reserved() to test if it's reserved.
- */
-struct utxo **wallet_get_unspent_utxos(const tal_t *ctx, struct wallet *w)
+static struct utxo **db_get_unspent_utxos(const tal_t *ctx, struct db *db)
{
struct db_stmt *stmt;
- stmt = db_prepare_v2(w->db, SQL("SELECT"
+ stmt = db_prepare_v2(db, SQL("SELECT"
" prev_out_tx"
", prev_out_index"
", value"
@@ -534,6 +526,19 @@ struct utxo **wallet_get_unspent_utxos(const tal_t *ctx, struct wallet *w)
return gather_utxos(ctx, stmt);
}
+/**
+ * wallet_get_unspent_utxos - Return reserved and unreserved UTXOs.
+ *
+ * Returns a `tal_arr` of `utxo` structs. Double indirection in order
+ * to be able to steal individual elements onto something else.
+ *
+ * Use utxo_is_reserved() to test if it's reserved.
+ */
+struct utxo **wallet_get_unspent_utxos(const tal_t *ctx, struct wallet *w)
+{
+ return db_get_unspent_utxos(ctx, w->db);
+}
+
struct utxo **wallet_get_unconfirmed_closeinfo_utxos(const tal_t *ctx,
struct wallet *w)
{
@@ -6940,13 +6945,14 @@ void db_bind_mvt_tags(struct db_stmt *stmt, struct mvt_tags tags)
db_bind_u64(stmt, tags.bits);
}
-void wallet_save_channel_mvt(struct lightningd *ld,
- const struct channel_coin_mvt *chan_mvt)
+static u64 insert_channel_mvt(struct lightningd *ld,
+ struct db *db,
+ const struct channel_coin_mvt *chan_mvt)
{
struct db_stmt *stmt;
u64 id;
- stmt = db_prepare_v2(ld->wallet->db,
+ stmt = db_prepare_v2(db,
SQL("INSERT INTO channel_moves ("
" id,"
" account_channel_id,"
@@ -6958,11 +6964,11 @@ void wallet_save_channel_mvt(struct lightningd *ld,
" payment_part_id,"
" payment_group_id,"
" fees) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"));
- id = channel_mvt_index_created(ld, ld->wallet->db,
+ id = channel_mvt_index_created(ld, db,
&chan_mvt->account,
chan_mvt->credit, chan_mvt->debit);
db_bind_u64(stmt, id);
- db_bind_mvt_account_id(stmt, ld->wallet->db, &chan_mvt->account);
+ db_bind_mvt_account_id(stmt, db, &chan_mvt->account);
db_bind_credit_debit(stmt, chan_mvt->credit, chan_mvt->debit);
db_bind_mvt_tags(stmt, chan_mvt->tags);
db_bind_u64(stmt, chan_mvt->timestamp);
@@ -6984,6 +6990,75 @@ void wallet_save_channel_mvt(struct lightningd *ld,
notify_channel_mvt(ld, chan_mvt, id);
if (taken(chan_mvt))
tal_free(chan_mvt);
+
+ return id;
+}
+
+void wallet_save_channel_mvt(struct lightningd *ld,
+ const struct channel_coin_mvt *chan_mvt)
+{
+ insert_channel_mvt(ld, ld->wallet->db, chan_mvt);
+}
+
+static u64 insert_chain_mvt(struct lightningd *ld,
+ struct db *db,
+ const struct chain_coin_mvt *chain_mvt)
+{
+ struct db_stmt *stmt;
+ u64 id;
+
+ stmt = db_prepare_v2(db,
+ SQL("INSERT INTO chain_moves ("
+ " id,"
+ " account_channel_id,"
+ " account_nonchannel_id,"
+ " tag_bitmap,"
+ " credit_or_debit,"
+ " timestamp,"
+ " utxo,"
+ " spending_txid,"
+ " peer_id,"
+ " payment_hash,"
+ " block_height,"
+ " output_sat,"
+ " originating_channel_id,"
+ " originating_nonchannel_id,"
+ " output_count) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
+ id = chain_mvt_index_created(ld, db,
+ &chain_mvt->account,
+ chain_mvt->credit, chain_mvt->debit);
+ db_bind_u64(stmt, id);
+ db_bind_mvt_account_id(stmt, db, &chain_mvt->account);
+ db_bind_mvt_tags(stmt, chain_mvt->tags);
+ db_bind_credit_debit(stmt, chain_mvt->credit, chain_mvt->debit);
+ db_bind_u64(stmt, chain_mvt->timestamp);
+ db_bind_outpoint(stmt, &chain_mvt->outpoint);
+ if (chain_mvt->spending_txid)
+ db_bind_txid(stmt, chain_mvt->spending_txid);
+ else
+ db_bind_null(stmt);
+ if (chain_mvt->peer_id)
+ db_bind_node_id(stmt, chain_mvt->peer_id);
+ else
+ db_bind_null(stmt);
+ if (chain_mvt->payment_hash)
+ db_bind_sha256(stmt, chain_mvt->payment_hash);
+ else
+ db_bind_null(stmt);
+ db_bind_int(stmt, chain_mvt->blockheight);
+ db_bind_amount_sat(stmt, chain_mvt->output_val);
+ if (chain_mvt->originating_acct) {
+ db_bind_mvt_account_id(stmt, db, chain_mvt->originating_acct);
+ } else {
+ db_bind_null(stmt);
+ db_bind_null(stmt);
+ }
+ if (chain_mvt->output_count > 0)
+ db_bind_int(stmt, chain_mvt->output_count);
+ else
+ db_bind_null(stmt);
+ db_exec_prepared_v2(take(stmt));
+ return id;
}
void wallet_save_chain_mvt(struct lightningd *ld,
@@ -7052,58 +7127,7 @@ void wallet_save_chain_mvt(struct lightningd *ld,
}
tal_free(stmt);
- stmt = db_prepare_v2(ld->wallet->db,
- SQL("INSERT INTO chain_moves ("
- " id,"
- " account_channel_id,"
- " account_nonchannel_id,"
- " tag_bitmap,"
- " credit_or_debit,"
- " timestamp,"
- " utxo,"
- " spending_txid,"
- " peer_id,"
- " payment_hash,"
- " block_height,"
- " output_sat,"
- " originating_channel_id,"
- " originating_nonchannel_id,"
- " output_count) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
- id = chain_mvt_index_created(ld, ld->wallet->db,
- &chain_mvt->account,
- chain_mvt->credit, chain_mvt->debit);
- db_bind_u64(stmt, id);
- db_bind_mvt_account_id(stmt, ld->wallet->db, &chain_mvt->account);
- db_bind_mvt_tags(stmt, chain_mvt->tags);
- db_bind_credit_debit(stmt, chain_mvt->credit, chain_mvt->debit);
- db_bind_u64(stmt, chain_mvt->timestamp);
- db_bind_outpoint(stmt, &chain_mvt->outpoint);
- if (chain_mvt->spending_txid)
- db_bind_txid(stmt, chain_mvt->spending_txid);
- else
- db_bind_null(stmt);
- if (chain_mvt->peer_id)
- db_bind_node_id(stmt, chain_mvt->peer_id);
- else
- db_bind_null(stmt);
- if (chain_mvt->payment_hash)
- db_bind_sha256(stmt, chain_mvt->payment_hash);
- else
- db_bind_null(stmt);
- db_bind_int(stmt, chain_mvt->blockheight);
- db_bind_amount_sat(stmt, chain_mvt->output_val);
- if (chain_mvt->originating_acct) {
- db_bind_mvt_account_id(stmt, ld->wallet->db, chain_mvt->originating_acct);
- } else {
- db_bind_null(stmt);
- db_bind_null(stmt);
- }
- if (chain_mvt->output_count > 0)
- db_bind_int(stmt, chain_mvt->output_count);
- else
- db_bind_null(stmt);
- db_exec_prepared_v2(take(stmt));
-
+ id = insert_chain_mvt(ld, ld->wallet->db, chain_mvt),
notify_chain_mvt(ld, chain_mvt, id);
out:
if (taken(chain_mvt))
@@ -7502,6 +7526,131 @@ void wallet_begin_old_close_rescan(struct lightningd *ld)
/* An existing node without accounting. Fill in what we have so far. */
void migrate_setup_coinmoves(struct lightningd *ld, struct db *db)
{
- /* FIXME: implement! */
+ struct utxo **utxos = db_get_unspent_utxos(tmpctx, db);
+ struct db_stmt *stmt;
+ u64 base_timestamp = time_now().ts.tv_sec - 2;
+
+ for (size_t i = 0; i < tal_count(utxos); i++) {
+ struct chain_coin_mvt *mvt;
+
+ /* Only confirmed ones */
+ if (!utxos[i]->blockheight)
+ continue;
+ mvt = new_coin_wallet_deposit(tmpctx,
+ &utxos[i]->outpoint,
+ *utxos[i]->blockheight,
+ utxos[i]->amount,
+ mk_mvt_tags(MVT_DEPOSIT));
+ insert_chain_mvt(ld, db, mvt);
+ }
+
+ /* Now channels. We create the open event, and then pushed/leased,
+ * the finally fixup with a journal entry. */
+ stmt = db_prepare_v2(db, SQL("SELECT"
+ " p.node_id"
+ ", scid"
+ ", full_channel_id"
+ ", funding_tx_id"
+ ", funding_tx_outnum"
+ ", funder"
+ ", push_msatoshi"
+ ", lease_commit_sig"
+ ", funding_satoshi"
+ ", our_funding_satoshi"
+ ", msatoshi_local"
+ " FROM channels c"
+ " JOIN peers p ON c.peer_id = p.id"
+ " WHERE c.scid IS NOT NULL"
+ " AND c.state != ?;"));
+ db_bind_int(stmt, CLOSED);
+ db_query_prepared(stmt);
+
+ while (db_step(stmt)) {
+ struct chain_coin_mvt *mvt;
+ struct bitcoin_outpoint funding;
+ struct channel_id cid;
+ struct node_id peerid;
+ struct amount_sat funding_sat, our_funding_sat;
+ struct amount_msat start_balance, our_msat, push_msat;
+ struct short_channel_id scid;
+ enum side opener;
+ bool is_leased;
+
+ db_col_node_id(stmt, "p.node_id", &peerid);
+ scid = db_col_short_channel_id(stmt, "scid");
+ db_col_txid(stmt, "funding_tx_id", &funding.txid);
+ funding.n = db_col_int(stmt, "funding_tx_outnum");
+ db_col_channel_id(stmt, "full_channel_id", &cid);
+ opener = db_col_int(stmt, "funder");
+ funding_sat = db_col_amount_sat(stmt, "funding_satoshi");
+ our_funding_sat = db_col_amount_sat(stmt, "our_funding_satoshi");
+ push_msat = db_col_amount_msat(stmt, "push_msatoshi");
+ is_leased = !db_col_is_null(stmt, "lease_commit_sig");
+ our_msat = db_col_amount_msat(stmt, "msatoshi_local");
+
+ /* If funds were pushed, add/sub them from the starting balance */
+ if (opener == LOCAL) {
+ if (!amount_sat_sub_msat(&start_balance,
+ our_funding_sat, push_msat))
+ abort();
+ } else {
+ if (!amount_msat_add_sat(&start_balance,
+ push_msat, our_funding_sat))
+ abort();
+ }
+ mvt = new_coin_channel_open_general(tmpctx,
+ NULL,
+ &cid,
+ /* We ensure strict ordering of events */
+ base_timestamp,
+ &funding,
+ &peerid,
+ short_channel_id_blocknum(scid),
+ start_balance,
+ funding_sat,
+ opener == LOCAL,
+ is_leased);
+ insert_chain_mvt(ld, db, mvt);
+
+ /* If we pushed, mark that transfer in channel. */
+ if (!amount_msat_is_zero(push_msat)) {
+ struct channel_coin_mvt *chan_mvt;
+
+ chan_mvt = new_coin_channel_push_general(tmpctx,
+ NULL,
+ &cid,
+ base_timestamp + 1,
+ opener == REMOTE ? COIN_CREDIT : COIN_DEBIT,
+ push_msat,
+ is_leased
+ ? mk_mvt_tags(MVT_LEASE_FEE)
+ : mk_mvt_tags(MVT_PUSHED));
+ insert_channel_mvt(ld, db, chan_mvt);
+ }
+
+ /* If our funds are not exactly what we expect, journal entry to adjust */
+ if (!amount_msat_eq(our_msat, start_balance)) {
+ struct amount_msat diff;
+ enum coin_mvt_dir direction;
+ struct channel_coin_mvt *chan_mvt;
+ if (amount_msat_sub(&diff, our_msat, start_balance))
+ direction = COIN_CREDIT;
+ else {
+ if (!amount_msat_sub(&diff, start_balance, our_msat))
+ abort();
+ direction = COIN_DEBIT;
+ }
+
+ chan_mvt = new_channel_coin_mvt_general(tmpctx, NULL, &cid,
+ base_timestamp + 2,
+ NULL, NULL, NULL,
+ direction,
+ diff,
+ mk_mvt_tags(MVT_JOURNAL),
+ AMOUNT_MSAT(0));
+ insert_channel_mvt(ld, db, chan_mvt);
+ }
+ }
+ tal_free(stmt);
}
Why this scored 22/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.