Skip to content
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

Remove txindex requirement for full nodes #751

Merged
merged 2 commits into from
Apr 18, 2018

Conversation

wpaulino
Copy link
Contributor

@wpaulino wpaulino commented Feb 10, 2018

Fixes #527.

So far, lnd has required its backend btcd and bitcoind full nodes to enable the transaction index. It is used to retrieve details for transaction confirmations and/or already spent transactions. This PR aims to remove that requirement by introducing some fallback methods, in the event that the transaction index is not enabled.

When attempting to retrieve these details, we now have the following workflow:

  1. We attempt to look up transactions using the transaction index.
  2. If the transaction index is not enabled, we attempt to look up transactions by manually scanning the chain's blocks starting from the earliest height where the transaction could have been included in, to the current height.
    3. In the case of pruned nodes, some of these blocks that we tried to scan above might have been missing because the node discarded them. Therefore, we decide to fetch these blocks from the node's peers and scan them.

I've also added some unit tests to make sure each part of the workflow above works as expected.

It's important to get this right, so it would be really helpful if people could spin up this patch locally on one of their test nodes and see how things go.

This depends on Roasbeef/btcwallet#14, so you'll either need to pull that locally as well or make the changes yourself within the vendor directory (very minor).

I realize there's a lot of code duplication, it might be worth looking into how to refactor some of these things. That's probably out of scope for this PR though.

@meshcollider meshcollider added funding Related to the opening of new channels with funding transactions on the blockchain channels backend Related to the node backend software/interface (e.g. btcd, bitcoin-core) labels Feb 11, 2018
@wpaulino
Copy link
Contributor Author

wpaulino commented Feb 12, 2018

Looks like to fully support pruned nodes, we'll also have to make some changes in the routing/chainview and lnwallet/btcwallet packages.

@Roasbeef, should I open a new PR to address this?

Copy link
Contributor

@halseth halseth left a comment

Choose a reason for hiding this comment

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

Excellent work on this one! I have reviewed the bitcoind part of this PR, and things are looking very good! 👍

My biggest code is that the massive code duplication that is being added to these files will make them very hard to maintain going forward. With the current way the notifier structs are set up, I can understand it is hard to extract out common code, but I think this PR would be an excellent opportunity to figure out how to refactor this. One way to go about this would be to use this PR to figure out a good way to do the refactoring, then push the refactoring as a separate PR (that does not change behaviour, only pure refactor), before rebasing this PR on top.


// netToParams maps the Bitcoin network specific magic number to its
// parameters.
netToParams = map[wire.BitcoinNet]*chaincfg.Params{
Copy link
Contributor

Choose a reason for hiding this comment

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

Slightly easier would be to store the params used when creating the notifier in the BitcoindNotifier struct.

txOut, err := b.chainConn.GetTxOut(&op.Hash,
op.Index, true)
if err != nil {
chainntnfs.Log.Error(err)
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you want to continue here?


// historicalSpendDetails retrieves the spend details for an already spent
// outpoint and dispatches its spend notification.
func (b *BitcoindNotifier) historicalSpendDetails(outpoint *wire.OutPoint,
Copy link
Contributor

Choose a reason for hiding this comment

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

Naming suggestion: notifyHistoricalSpendDetails to indicate this will notify clients.

err = b.historicalSpendDetails(&op,
msg.heightHint, bestHeight)
if err != nil {
chainntnfs.Log.Error(err)
Copy link
Contributor

Choose a reason for hiding this comment

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

continue?

close(ntfn.spendChan)
}
delete(b.spendNotifications, prevOut)
b.dispatchSpendNotification(prevOut, spendDetails)
Copy link
Contributor

Choose a reason for hiding this comment

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

Here you should either pass the clients instead of the outpoint, or remove the if _, ok := b.spendNotifications[prevOut]; ok { check above.

@@ -3,20 +3,24 @@ package btcdnotify
import (
"errors"
"fmt"
"net"
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this commit is getting quite big, maybe an idea to split it into two? :)

Copy link
Contributor

Choose a reason for hiding this comment

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

Not gonna review this file for now, as I reckon it's mostly the same changes as for bitcoind. We should really try to dedup some of the code, if possible.


// netToParams maps the Bitcoin network specific magic number to its
// parameters.
netToParams = map[wire.BitcoinNet]*chaincfg.Params{
Copy link
Contributor

Choose a reason for hiding this comment

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

same

// Begin looking for the spending transaction by fetching blocks
// from our peer at every height and scanning them.
for height := lastHeight; height <= bestHeight; height++ {
blockHash, err := b.chainConn.GetBlockHash(int64(height))
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure how easy it will be, but could we extract common code by putting the b.chainConn behind an interface that could be used by both btcd and `bitcoind``


// testConfDetailsFromTxIndex tests that we can find the transaction from the
// node's transaction index and that the details retrieved are correct.
func testConfDetailsFromTxIndex(t *testing.T, miner *rpctest.Harness,
Copy link
Contributor

Choose a reason for hiding this comment

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

Are these similar to those for bitcoind? Possible to put the two notifiers behind a common interface, and reuse the tests?

"github.com/roasbeef/btcd/btcec"
"github.com/roasbeef/btcd/btcjson"
Copy link
Contributor

Choose a reason for hiding this comment

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

why was this file changed?

@wpaulino
Copy link
Contributor Author

Thanks for the review @halseth!

This PR has been modified to provide manually scanning the backend's chain as the only fallback method when the transaction index is not enabled.

Before attempting to include support for pruned nodes, the chainntnfs package should be heavily refactored to avoid more code duplication.

Copy link
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

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

Nice work!

Before doing any more work to implement a pruned mode, or the like, think we'll need to aggressively refactor this package as a whole, in order to get rid of all the duplicated code across the various backend implementations.

Also I commented in-line within the PR w.r.t a possible danger when attempting to dispatch confirmations due to an incorrect height hint being passed by the caller. This PR mostly read swell to me, but we'll need to ensure that we're not introducing new bugs for nodes that are running without the txindex.

if err != nil {
chainntnfs.Log.Error(err)
}
b.heightMtx.RLock()

Copy link
Member

Choose a reason for hiding this comment

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

Why is the mutex around access to the txConfNotifier no longer needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The mutex is only being used to guard the ChainNotifier's bestHeight. Calling txConfNotifier.Register without it should be fine since it doesn't have an effect on the bestHeight. I'm guessing it was needed at some point and it didn't get removed. I could be wrong though.

) (*chainntnfs.TxConfirmation, error) {

// If the transaction already has some or all of the confirmations,
// then we may be able to dispatch it immediately.
// TODO: fall back to scanning blocks if txindex isn't on.
tx, err := b.chainConn.GetRawTransactionVerbose(txid)
Copy link
Member

Choose a reason for hiding this comment

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

The prior comment, minus the TOOD which will now be fixed, can likely remain.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

}

// As we need to fully populate the returned TxConfirmation struct,
// grab the block in which the transaction was confirmed so we can
// locate its exact index within the block.
blockHash, err := chainhash.NewHashFromStr(tx.BlockHash)
if err != nil {
return nil, fmt.Errorf("unable to get block hash %v for historical "+
"dispatch: %v", tx.BlockHash, err)
return nil, err
Copy link
Member

Choose a reason for hiding this comment

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

Why was the wrapped error (which had more detail) removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

if txHash == targetTxidStr {
txIndex = i
break
return &chainntnfs.TxConfirmation{
Copy link
Member

Choose a reason for hiding this comment

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

👍 on the simplification here

// earliest height the transaction could have been included in, to the current
// height in the chain. If it is found, the confirmation details are returned.
// Otherwise, an error is not returned.
func (b *BitcoindNotifier) confDetailsManually(txid *chainhash.Hash,
Copy link
Member

Choose a reason for hiding this comment

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

With this change, we'll need to do a clean sweep through the entire codebase to ensure that heightHint is set properly. There are some known cases with neutrino (particularly in the contractcourt) where an incorrect height hint value can cause us to actually miss a notification all together. We'll need to extensively test this patch to ensure these holes are patched up.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was running this patch on testnet for a bit and didn't run into any issues. I'll continue testing and see if I can run into any more.

return nil, err
}
} else {
// Otherwise, we'll rely on manually scanning our chain
Copy link
Member

Choose a reason for hiding this comment

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

Why's this block necessary? Instead we can just query for the block hash of the height hint. Then the block below can be simplified to simply call into Rescan with the proper block hash.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, will fix.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

return nil, err
}

targetTxidStr := outpoint.Hash.String()
out:
Copy link
Member

Choose a reason for hiding this comment

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

Similar comment here w.r.t this block not being necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

// First, we'll attempt to retrieve the transaction details using the
// backend node's transaction index.
txConf, err := b.confDetailsFromTxIndex(txid)
if err != nil {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Noticed this while addressing your comments: maybe we should proceed to use the heightHint if this fails, rather than returning the error?

Copy link
Member

Choose a reason for hiding this comment

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

With the new code, the nature of the error is likely critical. We'll need to properly test with both bitcoind and btcd to ensure that our logic catches the "no tx index" error properly.

@Roasbeef
Copy link
Member

Roasbeef commented Apr 6, 2018

Can you rebase this to master? Thanks!

@wpaulino wpaulino force-pushed the txindex branch 2 times, most recently from 16244be to 7e7c5ef Compare April 6, 2018 06:18
@wpaulino
Copy link
Contributor Author

wpaulino commented Apr 6, 2018

Rebased.

Roasbeef
Roasbeef previously approved these changes Apr 10, 2018
Copy link
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

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

Needs YAR (yet-another-rebase) due to some changes that recently landed to make mempool spends, but afterwards

LGTM ⛄️

"with height %d", height)
}

block, err := b.chainConn.GetBlockVerbose(blockHash)
Copy link
Member

Choose a reason for hiding this comment

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

Worth adding a TODO here that we can actually just fetch the neutrino filters here. Will save us a bit of processing as we can quickly ascertain if a transaction maybe includes a block or not.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@Roasbeef
Copy link
Member

When we (eventually) port over the integration tests to run on all backends, we should ensure that we're able to start and properly execute the tests against instances of btcd and bitcoind that don't have the txindex active.

halseth
halseth previously approved these changes Apr 11, 2018
Copy link
Contributor

@halseth halseth left a comment

Choose a reason for hiding this comment

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

This change looks good to me now, only a nit about a confusing comment 💯

The backend logic is becoming very similar for btcd and bitcoind now (which is good), maybe add a TODO to extract common code?


// confDetailsFromTxIndex looks up whether a transaction is already included
// in a block in the active chain by using the backend node's transaction index.
// If it is found, the confirmation details are returned. Otherwise, an error
Copy link
Contributor

Choose a reason for hiding this comment

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

Comment describing if/when an error is returned is confusing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

return nil, fmt.Errorf("unable to query for txid(%v): %v", txid, err)
}

// Make sure we actually retrieved a transaction that is included in a
Copy link
Contributor

Choose a reason for hiding this comment

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

thumbsup on this refactoring!

// block in the active chain by scanning the chain's blocks, starting from the
// earliest height the transaction could have been included in, to the current
// height in the chain. If it is found, the confirmation details are returned.
// Otherwise, an error is not returned.
Copy link
Contributor

Choose a reason for hiding this comment

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

Same. I think I understand what this means, but it's a bit confusing 😛

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

@wpaulino
Copy link
Contributor Author

Rebased and added TODOs.

// confDetailsFromTxIndex looks up whether a transaction is already included
// in a block in the active chain by using the backend node's transaction index.
// If the transaction is found, its confirmation details are returned.
// Otherwise, a nil error is returned. A non-nil error may also be returned if
Copy link
Contributor

@halseth halseth Apr 17, 2018

Choose a reason for hiding this comment

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

Suggestion:
"If the transaction is found in the index, its confirmation details are returned. If it is not found in the index, nil is returned."
I think that should make it more clear that a non-nil error is critical(?).

Before this commit, we relied on the need of full nodes to enable the
transaction index. This allowed us to fetch historical details about
transactions in order to register and dispatch confirmation and spend
notifications.

This commit allows us to drop that requirement by providing a fallback
method to use when the transaction index is not enabled. This fallback
method relies on manually scanning blocks for the transactions
requested, starting from the earliest height the transactions could have
been included in, to the current height in the chain.
Now that we have a fallback method for when the transaction index is not
enabled, its requirement to be enabled for a full node is no longer
needed, so we can safely remove this check.
Copy link
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

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

LGTM 🔥

@Roasbeef
Copy link
Member

Will make an issue for a clean sweep to ensure height hints are always properly set. Last I investigated, there were some areas in the utxo nursery, and also the contractcourt where an incorrect height hint could at times lead to failure to actually sweep an output or respond to an event.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backend Related to the node backend software/interface (e.g. btcd, bitcoin-core) channels funding Related to the opening of new channels with funding transactions on the blockchain
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants