Skip to content

Updates Chainnotifier Service to leverage batchAPI support #6811

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

Closed
wants to merge 2 commits into from

Conversation

Vib-UX
Copy link

@Vib-UX Vib-UX commented Aug 9, 2022

Change Description

Fixes #5041 Dependency : #807 btcwallet
Related discussions and making : Vib-UX#1

Steps to Test

  • cd chainntnfs/bitcoindnotify/
  • go test -v -tags=dev

Pull Request Checklist

Testing

  • Your PR passes all CI checks.
  • Tests covering the positive and negative (error paths) are included.
  • Bug fixes contain tests triggering the bug to prevent regressions.

Code Style and Documentation

📝 Please see our Contribution Guidelines for further guidance.

Copy link
Contributor

@positiveblue positiveblue left a comment

Choose a reason for hiding this comment

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

This PR looks really good. We will gain quite a lot of perf in some execution paths 👍, thank you for it Vib-UX

I left a few non-blocking comments, but I think we need to wait for btcwallet, which looks almost ready.

Needs rebase

P.S: you had me at support of updated batchAPI for bitcoind making it **lightning fast**

@@ -525,8 +526,31 @@ func (b *BitcoindNotifier) confDetailsManually(confRequest chainntnfs.ConfReques
heightHint, currentHeight uint32) (*chainntnfs.TxConfirmation,
chainntnfs.TxConfStatus, error) {

// Begin scanning blocks at every height to determine where the
// transaction was included in.
// batchRequest will store the scan requests at every height to determine
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we should put the logic for fetching blocks in its own struct/interface. That way we would not have to copy twice the same logic structure in confDetailsManually and historicalSpendDetails. It would also abstract how it is done (in batches or one by one).


// Now we fetch blocks in interval of 15 to avoid out of memory
// errors in case of fetching too many blocks with GetBlocksBatch().
const batchSize = 15
Copy link
Contributor

Choose a reason for hiding this comment

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

I checked in bitcoin core but I did not see if there is any limit for this. The idea is to set a reasonable limit so bitcoind does not run OOM? It is hard for me to see what is a good number here (5, 15 or 50 😕 )

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the concern for OOM is in btcwallet itself if it needs to process and keep in memory more than 15 blocks at a time.

continue
}
// Note:- blockHashes are stored in reverse order
// endHeight --> startHeight, so we maintain the same refs
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 only because we are doing

blockHashes := make([]*chainhash.Hash, 0, len(batchRequest))
for height := endHeight; height >= startHeight && height > 0; height-- {
    // ...
    blockHashes = append(blockHashes, blockHash)
}

If so we could directly put them in order (we know the size) or simply reverse the array before start using it.

that way it may be easier to follow

 for i := 0; i < total; i += batchSize {
    //...
    start := i
    end := i + batchSize
    // ...
    height := int(endHeight) - start
    for j := range blocks {
        for _, tx := range blocks[j].Transactions {
            // ...
            return &chainntnfs.SpendDetail{
					// ...
					SpendingHeight:    int32(height - j), # this part is correct, but I needed to think about it twice 
				}, nil
            }
        }
    }

Copy link
Author

Choose a reason for hiding this comment

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

I mean sure we can reverse the slice and traverse on it, changes would be in +ve sign that's all, something like:

height:= int(startHeight) + start
SpendingHeight: int32(height + j)

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, i just found it counter intuitive because we are counting backwards. It was just a small nit, nothing to be fixed 👍 .

SpendingHeight: int32(startHeight + start + j) vs SpendingHeight: int32(endHeight - start - j)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Again, probably a good idea to refactor this into a method so we have the same reverse loop logic only once in the code. Then the batch size of 15 could also be a parameter there.

go.mod Outdated
@@ -60,6 +60,9 @@ require (
gopkg.in/macaroon.v2 v2.0.0
)

// This replace is to leverage added batch-json rpc
Copy link
Contributor

Choose a reason for hiding this comment

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

The changes look good, we will have to wait for them to be merged in btcwallet before merging this one

Vib-UX added 2 commits August 22, 2022 12:08
With this RegisterSpendNtfn() & RegisterConfirmationsNtfn() services
will leverage the support of updated batchAPI for bitcoind making it
lightning fast.
Testcase to ensure the updated historicalSpendDetails() and confDetailsManually()
which leverages batchAPI for scanning the chain manually works as expected
with enhanced performance.
Copy link
Collaborator

@guggero guggero left a comment

Choose a reason for hiding this comment

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

Thanks a lot for your work on this!
The test cases looks good and should also be able to show the speed up potential if we add benchmarks to them in the future (for example to determine a good value for the batch size).

Though I think the whole reverse loop logic should be extracted into btcwallet itself so we don't have the same, hard to understand logic in 3 places.

// transaction was included in.
// batchRequest will store the scan requests at every height to determine
// where the transaction was included in.
batchRequest := make(
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: use plural as it's a bunch of requests?


// Now we fetch blocks in interval of 15 to avoid out of memory
// errors in case of fetching too many blocks with GetBlocksBatch().
const batchSize = 15
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the concern for OOM is in btcwallet itself if it needs to process and keep in memory more than 15 blocks at a time.

const batchSize = 15
total := len(blockHashes)

for i := 0; i < total; i += batchSize {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This whole logic is almost exactly the same as in BitcoindClient.FilterBlocksBatched. The only difference is that there we call blockFilterer.FilterBlock() and here we call confRequest.MatchesTx(). So this could be refactored into a more generic method on the BitcoindClient that just takes a callback cb func(block *wire.MsgBlock) bool.

continue
}
// Note:- blockHashes are stored in reverse order
// endHeight --> startHeight, so we maintain the same refs
Copy link
Collaborator

Choose a reason for hiding this comment

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

Again, probably a good idea to refactor this into a method so we have the same reverse loop logic only once in the code. Then the batch size of 15 could also be a parameter there.

var unknownHash chainhash.Hash
copy(unknownHash[:], bytes.Repeat([]byte{0x10}, 32))
invalidOutpoint := wire.NewOutPoint(&unknownHash, 0)
unknownSpendReq, err := chainntnfs.NewSpendRequest(invalidOutpoint, testScript)
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: the code in this whole commit is very "blocky" and hard to read. Can we group this into smaller logical pieces separated by empty lines? For example:

	// Since the node has its txindex disabled, we fall back to scanning the
	// chain manually. A outpoint unknown to the network should not be
	// notified.
	var unknownHash chainhash.Hash
	copy(unknownHash[:], bytes.Repeat([]byte{0x10}, 32))
	invalidOutpoint := wire.NewOutPoint(&unknownHash, 0)
	unknownSpendReq, err := chainntnfs.NewSpendRequest(invalidOutpoint, testScript)
	require.NoError(t, err, "unable to create spend request")

	broadcastHeight := syncNotifierWithMiner(t, notifier, miner)
	spendDetails, errSpend := notifier.historicalSpendDetails(
		unknownSpendReq, broadcastHeight, broadcastHeight,
	)
	require.NoError(t, errSpend, "unable to retrieve historical spend details")
	require.Equal(t, spendDetails, (*chainntnfs.SpendDetail)(nil))

	// Now, we'll create a test transaction and attempt to retrieve its
	// spending details. In order to fall back to manually scanning the
	// chain, the transaction must be in the chain and not contain any
	// unspent outputs. To ensure this, we'll create a transaction with only
	// one output, which we will manually spend. The backend node's
	// transaction index should also be disabled, which we've already
	// ensured above.
	outpoint, output, privKey := chainntnfs.CreateSpendableOutput(t, miner)
	spendTx := chainntnfs.CreateSpendTx(t, outpoint, output, privKey)
	spendTxHash, err := miner.Client.SendRawTransaction(spendTx, true)
	require.NoError(t, err, "unable to broadcast tx")

	broadcastHeight = syncNotifierWithMiner(t, notifier, miner)
	err = chainntnfs.WaitForMempoolTx(miner, spendTxHash)
	require.NoError(t, err, "tx not relayed to miner")

	const noOfBlocks = 100
	_, err = miner.Client.Generate(noOfBlocks)
	require.NoError(t, err, "unable to generate blocks")

	// Ensure the notifier and miner are synced to the same height to ensure
	// we can find the transaction spend details when manually scanning the
	// chain.
	spendReq, err := chainntnfs.NewSpendRequest(outpoint, output.PkScript)
	require.NoError(t, err, "unable to create conf request")

	currentHeight := syncNotifierWithMiner(t, notifier, miner)
	validSpendDetails, err := notifier.historicalSpendDetails(
		spendReq, broadcastHeight, currentHeight,
	)
	require.NoError(t, err, "unable to retrieve historical spend details")

	// Since the backend node's txindex is disabled and the transaction has
	// confirmed, we should be able to find it by falling back to scanning
	// the chain manually.
	require.NotNil(t, validSpendDetails)
	require.Equal(t, validSpendDetails.SpentOutPoint, outpoint)
	require.Equal(t, validSpendDetails.SpenderTxHash, spendTxHash)
	require.Equal(t, validSpendDetails.SpendingTx, spendTx)
	require.Equal(t, validSpendDetails.SpendingHeight, int32(broadcastHeight+1))

@@ -243,32 +245,124 @@ func testHistoricalConfDetailsNoTxIndex(t *testing.T, rpcpolling bool) {
// ensured above.
outpoint, output, privKey := chainntnfs.CreateSpendableOutput(t, miner)
spendTx := chainntnfs.CreateSpendTx(t, outpoint, output, privKey)
const noOfBlocks = 100
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: move constant to directly above where it's used?

unknownSpendReq, broadcastHeight, broadcastHeight,
)
require.NoError(t, errSpend, "unable to retrieve historical spend details")
require.Equal(t, spendDetails, (*chainntnfs.SpendDetail)(nil))
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: use require.Nil()?

@saubyk saubyk removed this from the v0.16.0 milestone Aug 27, 2022
@lightninglabs-deploy
Copy link

@Vib-UX, remember to re-request review from reviewers when ready

@lightninglabs-deploy
Copy link

Closing due to inactivity

2 similar comments
@lightninglabs-deploy
Copy link

Closing due to inactivity

@lightninglabs-deploy
Copy link

Closing due to inactivity

@ellemouton ellemouton closed this Jul 28, 2023
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.

chainntnfs: leverage batched JSON-RPC requests to speed up spend/conf rescans
7 participants