-
Notifications
You must be signed in to change notification settings - Fork 942
Track output confirmations, spends and maintain an internal UTXO set #1117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
I believe this fixes #451? |
Indeed it does, I'll collect the issues once I'm back online 😉 |
Oh and it may be way too soon to review, which is why I didn't assign reviewers yet. |
wallet/db.c
Outdated
/* What do we think the head of the blockchain looks like? Used | ||
* primarily to track confirmations across restarts and making | ||
* sure we handle reorgs correctly. */ | ||
"CREATE TABLE blocks (height INT, hash BLOB, prev_hash BLOB, UNIQUE(height));", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The UNIQUE(height)
constraint implies that a reorg deletes entries in this table, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does, yes, and it's also what I'm using to set spend_height
and confirmation_height
to NULL
in case of a reorg.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea is to only track what is actually the valid chain, and have a rollback to the fork point in case of a reorg.
wallet/wallet.c
Outdated
void wallet_block_remove(struct wallet *w, struct block *b) | ||
{ | ||
sqlite3_stmt *stmt = db_prepare(w->db, "DELETE FROM blocks " | ||
"WHERE hash = ? OR height >= ?"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The OR
in the condition means the equality check on hash
does not buy you any additional assurance of correctness. It will only be relevant if the hash
and height
of the given struct block *
does not match the entry in the DB, but if your struct block *
does not match the corresponding entry in the DB you have bigger problems (a cache incoherence). If the hash
and height
on the struct block *
maches the DB row, then height >= ?
is sufficient. But that then means that wallet_block_rollback
is sufficient and wallet_block_remove
is unnecessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, I should remove the height check, I was hoping to replicate the remove_tip
logic, hoping that it'd remove in reverse height order.
wallet/wallet.c
Outdated
@@ -16,6 +16,17 @@ | |||
#define DIRECTION_INCOMING 0 | |||
#define DIRECTION_OUTGOING 1 | |||
|
|||
static void outpointfilters_init(struct wallet *w) | |||
{ | |||
struct utxo **utxos = wallet_get_utxos(NULL, w, output_state_any); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will monitor all outputs, even those already spent?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm planning to have a cleanup that triggers periodically and garbage collects spent outputs every once in a while if they are deeply buried, i.e., cannot be reorged out. That'll drop from the DB and from the filter set.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
okay, seems good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, we treat 100 as "forever" elsewhere, so that seems a good benchmark.
Overall OK so far, even if WIP; github messes up their ordering so I just got it directly to my local, pleasant to review. A few questions though in my review. |
Thanks for the quick review @ZmnSCPxj, I'll address the blockheight issue now and build on top 😉 |
15f9e83
to
2f4e0d1
Compare
I think we should be good for a first iteration of reviews. It doesn't yet contain all the features I eventually want to build on top of the utxoset being tracked in the wallet but it is a self-contained set of changes and it's starting to get hard to rebase on top of I decided to keep the owned outputs and the utxoset separate, though the terms are sometimes a bit intermingled. We track all P2WSH utxos because they might be channels and might be needed for later lookups, and prune the spent ones that are older than a day. Each output being tracked adds about 200 bytes to the DB size, so the impact isn't too big. Performance wise the overhead is tiny compared to the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you, each commit seems reasonably clear, have some questions though.
void wallet_block_add(struct wallet *w, struct block *b); | ||
|
||
/** | ||
* wallet_block_remove - Remove a block (and all its descendants) from the tracked blockchain |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment is no longer accurate --- now it only removes a block without affecting its descendants if any.
Looking over the rest of the code it seems wallet_block_remove
is only called on the current tip? If so, suggest adding an assertion somewhere in the implementation of wallet_block_remove
.
/* after DELETE */
stmt = db_prepare(w->db, "SELECT * FROM blocks WHERE height >= ?;");
sqlite3_bind_int(stmt, 1, b->height);
assert(sqlite3_step(stmt) == SQLITE_DONE);
sqlite3_finalize(stmt);
wallet/wallet.c
Outdated
sqlite3_stmt *stmt = db_prepare(w->db, | ||
"DELETE FROM blocks WHERE hash = ?"); | ||
sqlite3_bind_sha256_double(stmt, 1, &b->blkid.shad); | ||
sqlite3_bind_int(stmt, 2, b->height); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unnecessary bind.
tests/test_lightningd.py
Outdated
@@ -1131,11 +1131,11 @@ def test_bitcoin_failure(self): | |||
|
|||
# This should cause both estimatefee and getblockhash fail | |||
l1.daemon.wait_for_logs(['estimatesmartfee .* exited with status 1', | |||
'getblockhash .* exited with status 1']) | |||
'getblock.* exited with status 1']) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe #1128 makes this superfluous?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed 😉
wallet/wallet.c
Outdated
stmt = db_prepare(w->db, "SELECT txid, outnum FROM utxoset WHERE spendheight = ?"); | ||
sqlite3_bind_null(stmt, 1); | ||
|
||
for (size_t i=0; sqlite3_step(stmt) == SQLITE_ROW; i++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i
seems unused? Why not simply while (sqlite3_step(stmt) == SQLITE_ROW)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Greta catch, this was a copy-paste leftover
Thanks @ZmnSCPxj for the in-depth review, I think I addressed the issues and it should be ready for another round ^^ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor nits only. Note that commit ab79f7f is empty :)
Changing the startup not to go all the way back will need the ability to replay old notifications for onchaind, which we need anyway for onchaind handling reorgs. I look forward to that
common/utxo.h
Outdated
@@ -26,6 +26,9 @@ struct utxo { | |||
/* Optional unilateral close information, NULL if this is just | |||
* a HD key */ | |||
struct unilateral_close_info *close_info; | |||
|
|||
/* Blockchain height references */ | |||
const int *confirmation_height, *spend_height; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like a bit more description for these fields; and we use u32 for block heights (the other option is to make these non-pointers and use -1, but I like the crash we get if they're NULL and we make a mistake):
/* NULL if we haven't seen it in a block, otherwise the block it's in */
const u32 *confirmation_height;
/* NULL if not spent yet, otherwise, the block the spending transaction is in */
const u32 *spend_height;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, 'confirmation_height' here and in db could also be called 'block_height'? The word confirmation is usually used with depth, not height, so it seems a bit awkward here? In other places you use blockheight and spendheight, it seems.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
confirmed_at_height
and spent_at_height
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now using blockheight
and spendheight
, consistent with the utxoset nomenclature. The block->height
is of type int
, I'll change that in a cleanup PR.
wallet/db.c
Outdated
@@ -215,6 +215,8 @@ char *dbmigrations[] = { | |||
* die. */ | |||
"ALTER TABLE outputs ADD COLUMN confirmation_height INTEGER REFERENCES blocks(height) ON DELETE SET NULL;", | |||
"ALTER TABLE outputs ADD COLUMN spend_height INTEGER REFERENCES blocks(height) ON DELETE SET NULL;", | |||
/* Create a covering index that covers both fiels */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fields
lightningd/txfilter.c
Outdated
static bool outpoint_eq(const struct outpointfilter_entry *o1, | ||
const struct outpointfilter_entry *o2) | ||
{ | ||
return memeq(&o1->txid, sizeof(o1->txid), &o2->txid, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
structeq() is preferred over memeq because it make sure they really are the same type. And it's safe on bitcoin_txid.
lightningd/txfilter.c
Outdated
return; | ||
/* Have to mark the entries as notleak since they'll not be | ||
* pointed to by anything other than the htable */ | ||
op = notleak(tal(of->set, struct outpointfilter_entry)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK for now, but better is to add outpointfilter_mark_pointers_used; see chaintopology_mark_pointers_used().
wallet/wallet.c
Outdated
@@ -16,6 +16,17 @@ | |||
#define DIRECTION_INCOMING 0 | |||
#define DIRECTION_OUTGOING 1 | |||
|
|||
static void outpointfilters_init(struct wallet *w) | |||
{ | |||
struct utxo **utxos = wallet_get_utxos(NULL, w, output_state_any); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, we treat 100 as "forever" elsewhere, so that seems a good benchmark.
wallet/db.c
Outdated
" spendheight INT REFERENCES blocks(height) ON DELETE SET NULL," | ||
" txindex INT," | ||
" scriptpubkey BLOB," | ||
" satoshis INT," |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
satoshis should be BIGINT. Doesn't matter for sqlite, but could for other dbs. Wonder if we get this wrong elsewhere too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, presumably we can just replace these without migrations since they map to the same internal datatype in sqlite3. Will change this instance and then go through the others in a new PR.
wallet/wallet.c
Outdated
|
||
w->utxoset_outpoints = outpointfilter_new(w); | ||
stmt = db_prepare(w->db, "SELECT txid, outnum FROM utxoset WHERE spendheight = ?"); | ||
sqlite3_bind_null(stmt, 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Binding here seems wierd: why not just say "WHERE spendheight IS NULL" directly?
wallet/wallet.h
Outdated
@@ -34,6 +34,7 @@ struct wallet { | |||
u64 max_channel_dbid; | |||
|
|||
struct outpointfilter *owned_outpoints; | |||
struct outpointfilter *utxoset_outpoints; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The relationship between these two is not immediately obvious to me?
owned_outputs is all outputs we own, including spent ones? utxoset_outpoints is a complete UTXO set? Or UTXOs corresponding to channel opens?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a comment to each about what they include.
lightningd/chaintopology.c
Outdated
} | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this a wise choice? That may be putting a lot of data into the db...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It only stores P2WSH outputs, which may actually be funding txs. The cost is about 200 bytes / output so without pruning spent ones all P2WSH outputs since segwit was activated take about 30MB in size, much less with pruning. It's the usual space-vs-speed tradeoff and this way we can run on pruned nodes and not parse blocks over and over again.
Signed-off-by: Christian Decker <[email protected]>
Signed-off-by: Christian Decker <[email protected]>
Signed-off-by: Christian Decker <[email protected]>
Signed-off-by: Christian Decker <[email protected]>
Signed-off-by: Christian Decker <[email protected]>
Signed-off-by: Christian Decker <[email protected]>
Signed-off-by: Christian Decker <[email protected]>
Signed-off-by: Christian Decker <[email protected]>
Avoids performing a table scan, now deletes on blocks are a lot faster. Signed-off-by: Christian Decker <[email protected]>
This can be used both for our own outputs as well as the utxos we are tracking. Signed-off-by: Christian Decker <[email protected]>
Will be used later to filter out outputs we are interested in, and trigger db updates with them. Signed-off-by: Christian Decker <[email protected]>
Signed-off-by: Christian Decker <[email protected]>
Transaction filters are strongly related to the wallet, this move just makes it a bit more explicit. Signed-off-by: Christian Decker <[email protected]>
Signed-off-by: Christian Decker <[email protected]>
When we already know about an output we would stop scanning the remaining outputs. Known outputs happen whenever we extracted from our own transactions and then extracted again from blocks. We would not update if the first update fails. Signed-off-by: Christian Decker <[email protected]>
Signed-off-by: Christian Decker <[email protected]>
Signed-off-by: Christian Decker <[email protected]>
Signed-off-by: Christian Decker <[email protected]>
Signed-off-by: Christian Decker <[email protected]>
Otherwise we would be doing a table scan per block being reorged/rescanned. Signed-off-by: Christian Decker <[email protected]>
Signed-off-by: Christian Decker <[email protected]>
Signed-off-by: Christian Decker <[email protected]>
Minor changes to address the feedback from @rustyrussell, merging as soon as CI is happy 👍 |
This PR aims to provide better tracking of the blockchain and internalizing some of the information which is now being fetched on demand. A number of use-cases should be improved with it:
outpointfilter
that can be used to filter outpoint spends that we are interested in, so that we can be notified when our owned outputs are being spent, or when a P2WSH UTXO has been spentbitcoin-cli
for each channel we learn about. Should allow us to reduce the sync time and is needed if we want to persist gossip announcement across restarts.I'm currently planning to have the sync start at
last_sync_height - 432
which should cover the cases in which we have an active close and some HTLCs waiting to be redeemed as well, i.e., startingonchaind
when seeing the funding spend, without additional state to be persisted.