Skip to content

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

Merged
merged 22 commits into from
Mar 6, 2018

Conversation

cdecker
Copy link
Member

@cdecker cdecker commented Feb 27, 2018

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:

  • Owned funds now receive a confirmation height and a spend height in order to reduce the chances of running out of sync with the bitcoin UTXO
  • Adds an 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 spent
  • Tracks P2WSH UTXOs in the wallet to avoid calling out to bitcoin-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.
  • Allows us not to rescan the entire blockchain from the first channel, instead starting to scan from the last known height - overlap.

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., starting onchaind when seeing the funding spend, without additional state to be persisted.

@ZmnSCPxj
Copy link
Contributor

I believe this fixes #451?

@cdecker
Copy link
Member Author

cdecker commented Feb 27, 2018

Indeed it does, I'll collect the issues once I'm back online 😉

@cdecker
Copy link
Member Author

cdecker commented Feb 27, 2018

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));",
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Member Author

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 >= ?");
Copy link
Contributor

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.

Copy link
Member Author

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);
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay, seems good.

Copy link
Contributor

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.

@ZmnSCPxj ZmnSCPxj removed the request for review from rustyrussell February 27, 2018 12:36
@ZmnSCPxj
Copy link
Contributor

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.

@cdecker
Copy link
Member Author

cdecker commented Feb 27, 2018

Thanks for the quick review @ZmnSCPxj, I'll address the blockheight issue now and build on top 😉

@cdecker cdecker mentioned this pull request Mar 4, 2018
@cdecker cdecker force-pushed the trackblocks branch 2 times, most recently from 15f9e83 to 2f4e0d1 Compare March 4, 2018 03:44
@cdecker
Copy link
Member Author

cdecker commented Mar 4, 2018

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 master every day :-)

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 bitcoin_txid calls in filter_block_txs.

@cdecker cdecker dismissed ZmnSCPxj’s stale review March 4, 2018 04:22

Please review again

@cdecker cdecker changed the title WIP: Track output confirmations, spends and maintain an internal UTXO set Track output confirmations, spends and maintain an internal UTXO set Mar 4, 2018
Copy link
Contributor

@ZmnSCPxj ZmnSCPxj left a 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
Copy link
Contributor

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary bind.

@@ -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'])
Copy link
Contributor

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?

Copy link
Member Author

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++) {
Copy link
Contributor

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

Copy link
Member Author

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

@cdecker
Copy link
Member Author

cdecker commented Mar 4, 2018

Thanks @ZmnSCPxj for the in-depth review, I think I addressed the issues and it should be ready for another round ^^

Copy link
Contributor

@rustyrussell rustyrussell left a 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;
Copy link
Contributor

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;

Copy link
Contributor

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.

Copy link
Contributor

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?

Copy link
Member Author

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 */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fields

static bool outpoint_eq(const struct outpointfilter_entry *o1,
const struct outpointfilter_entry *o2)
{
return memeq(&o1->txid, sizeof(o1->txid), &o2->txid,
Copy link
Contributor

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.

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));
Copy link
Contributor

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);
Copy link
Contributor

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,"
Copy link
Contributor

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?

Copy link
Member Author

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);
Copy link
Contributor

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;
Copy link
Contributor

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?

Copy link
Member Author

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.

}
}
}

Copy link
Contributor

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...

Copy link
Member Author

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.

cdecker added 14 commits March 6, 2018 16:09
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]>
Transaction filters are strongly related to the wallet, this move just
makes it a bit more explicit.

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]>
Otherwise we would be doing a table scan per block being reorged/rescanned.

Signed-off-by: Christian Decker <[email protected]>
@cdecker
Copy link
Member Author

cdecker commented Mar 6, 2018

Minor changes to address the feedback from @rustyrussell, merging as soon as CI is happy 👍

@cdecker cdecker merged commit aba3d5f into ElementsProject:master Mar 6, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants