You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
956 lines
27 KiB
956 lines
27 KiB
package chainview |
|
|
|
import ( |
|
"bytes" |
|
"fmt" |
|
"io/ioutil" |
|
"math/rand" |
|
"os" |
|
"os/exec" |
|
"path/filepath" |
|
"runtime" |
|
"testing" |
|
"time" |
|
|
|
"github.com/btcsuite/btcd/btcec" |
|
"github.com/btcsuite/btcd/btcjson" |
|
"github.com/btcsuite/btcd/chaincfg" |
|
"github.com/btcsuite/btcd/chaincfg/chainhash" |
|
"github.com/btcsuite/btcd/integration/rpctest" |
|
"github.com/btcsuite/btcd/rpcclient" |
|
"github.com/btcsuite/btcd/txscript" |
|
"github.com/btcsuite/btcd/wire" |
|
"github.com/btcsuite/btcutil" |
|
"github.com/btcsuite/btcwallet/chain" |
|
"github.com/btcsuite/btcwallet/walletdb" |
|
_ "github.com/btcsuite/btcwallet/walletdb/bdb" // Required to register the boltdb walletdb implementation. |
|
|
|
"github.com/lightninglabs/neutrino" |
|
"github.com/lightningnetwork/lnd/channeldb" |
|
"github.com/lightningnetwork/lnd/channeldb/kvdb" |
|
) |
|
|
|
var ( |
|
netParams = &chaincfg.RegressionNetParams |
|
|
|
testPrivKey = []byte{ |
|
0x81, 0xb6, 0x37, 0xd8, 0xfc, 0xd2, 0xc6, 0xda, |
|
0x63, 0x59, 0xe6, 0x96, 0x31, 0x13, 0xa1, 0x17, |
|
0xd, 0xe7, 0x95, 0xe4, 0xb7, 0x25, 0xb8, 0x4d, |
|
0x1e, 0xb, 0x4c, 0xfd, 0x9e, 0xc5, 0x8c, 0xe9, |
|
} |
|
|
|
privKey, pubKey = btcec.PrivKeyFromBytes(btcec.S256(), testPrivKey) |
|
addrPk, _ = btcutil.NewAddressPubKey(pubKey.SerializeCompressed(), |
|
netParams) |
|
testAddr = addrPk.AddressPubKeyHash() |
|
|
|
testScript, _ = txscript.PayToAddrScript(testAddr) |
|
) |
|
|
|
func waitForMempoolTx(r *rpctest.Harness, txid *chainhash.Hash) error { |
|
var found bool |
|
var tx *btcutil.Tx |
|
var err error |
|
timeout := time.After(10 * time.Second) |
|
for !found { |
|
// Do a short wait |
|
select { |
|
case <-timeout: |
|
return fmt.Errorf("timeout after 10s") |
|
default: |
|
} |
|
time.Sleep(100 * time.Millisecond) |
|
|
|
// Check for the harness' knowledge of the txid |
|
tx, err = r.Client.GetRawTransaction(txid) |
|
if err != nil { |
|
switch e := err.(type) { |
|
case *btcjson.RPCError: |
|
if e.Code == btcjson.ErrRPCNoTxInfo { |
|
continue |
|
} |
|
default: |
|
} |
|
return err |
|
} |
|
if tx != nil && tx.MsgTx().TxHash() == *txid { |
|
found = true |
|
} |
|
} |
|
return nil |
|
} |
|
|
|
func getTestTXID(miner *rpctest.Harness) (*chainhash.Hash, error) { |
|
script, err := txscript.PayToAddrScript(testAddr) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
outputs := []*wire.TxOut{ |
|
{ |
|
Value: 2e8, |
|
PkScript: script, |
|
}, |
|
} |
|
return miner.SendOutputs(outputs, 2500) |
|
} |
|
|
|
func locateOutput(tx *wire.MsgTx, script []byte) (*wire.OutPoint, *wire.TxOut, error) { |
|
for i, txOut := range tx.TxOut { |
|
if bytes.Equal(txOut.PkScript, script) { |
|
return &wire.OutPoint{ |
|
Hash: tx.TxHash(), |
|
Index: uint32(i), |
|
}, txOut, nil |
|
} |
|
} |
|
|
|
return nil, nil, fmt.Errorf("unable to find output") |
|
} |
|
|
|
func craftSpendTransaction(outpoint wire.OutPoint, payScript []byte) (*wire.MsgTx, error) { |
|
spendingTx := wire.NewMsgTx(1) |
|
spendingTx.AddTxIn(&wire.TxIn{ |
|
PreviousOutPoint: outpoint, |
|
}) |
|
spendingTx.AddTxOut(&wire.TxOut{ |
|
Value: 1e8, |
|
PkScript: payScript, |
|
}) |
|
sigScript, err := txscript.SignatureScript(spendingTx, 0, payScript, |
|
txscript.SigHashAll, privKey, true) |
|
if err != nil { |
|
return nil, err |
|
} |
|
spendingTx.TxIn[0].SignatureScript = sigScript |
|
|
|
return spendingTx, nil |
|
} |
|
|
|
func assertFilteredBlock(t *testing.T, fb *FilteredBlock, expectedHeight int32, |
|
expectedHash *chainhash.Hash, txns []*chainhash.Hash) { |
|
|
|
_, _, line, _ := runtime.Caller(1) |
|
|
|
if fb.Height != uint32(expectedHeight) { |
|
t.Fatalf("line %v: block height mismatch: expected %v, got %v", |
|
line, expectedHeight, fb.Height) |
|
} |
|
if !bytes.Equal(fb.Hash[:], expectedHash[:]) { |
|
t.Fatalf("line %v: block hash mismatch: expected %v, got %v", |
|
line, expectedHash, fb.Hash) |
|
} |
|
if len(fb.Transactions) != len(txns) { |
|
t.Fatalf("line %v: expected %v transaction in filtered block, instead "+ |
|
"have %v", line, len(txns), len(fb.Transactions)) |
|
} |
|
|
|
expectedTxids := make(map[chainhash.Hash]struct{}) |
|
for _, txn := range txns { |
|
expectedTxids[*txn] = struct{}{} |
|
} |
|
|
|
for _, tx := range fb.Transactions { |
|
txid := tx.TxHash() |
|
delete(expectedTxids, txid) |
|
} |
|
|
|
if len(expectedTxids) != 0 { |
|
t.Fatalf("line %v: missing txids: %v", line, expectedTxids) |
|
} |
|
|
|
} |
|
|
|
func testFilterBlockNotifications(node *rpctest.Harness, |
|
chainView FilteredChainView, chainViewInit chainViewInitFunc, |
|
t *testing.T) { |
|
|
|
// To start the test, we'll create to fresh outputs paying to the |
|
// private key that we generated above. |
|
txid1, err := getTestTXID(node) |
|
if err != nil { |
|
t.Fatalf("unable to get test txid: %v", err) |
|
} |
|
err = waitForMempoolTx(node, txid1) |
|
if err != nil { |
|
t.Fatalf("unable to get test txid in mempool: %v", err) |
|
} |
|
txid2, err := getTestTXID(node) |
|
if err != nil { |
|
t.Fatalf("unable to get test txid: %v", err) |
|
} |
|
err = waitForMempoolTx(node, txid2) |
|
if err != nil { |
|
t.Fatalf("unable to get test txid in mempool: %v", err) |
|
} |
|
|
|
blockChan := chainView.FilteredBlocks() |
|
|
|
// Next we'll mine a block confirming the output generated above. |
|
newBlockHashes, err := node.Client.Generate(1) |
|
if err != nil { |
|
t.Fatalf("unable to generate block: %v", err) |
|
} |
|
|
|
_, currentHeight, err := node.Client.GetBestBlock() |
|
if err != nil { |
|
t.Fatalf("unable to get current height: %v", err) |
|
} |
|
|
|
// We should get an update, however it shouldn't yet contain any |
|
// filtered transaction as the filter hasn't been update. |
|
select { |
|
case filteredBlock := <-blockChan: |
|
assertFilteredBlock(t, filteredBlock, currentHeight, |
|
newBlockHashes[0], []*chainhash.Hash{}) |
|
case <-time.After(time.Second * 20): |
|
t.Fatalf("filtered block notification didn't arrive") |
|
} |
|
|
|
// Now that the block has been mined, we'll fetch the two transactions |
|
// so we can add them to the filter, and also craft transaction |
|
// spending the outputs we created. |
|
tx1, err := node.Client.GetRawTransaction(txid1) |
|
if err != nil { |
|
t.Fatalf("unable to fetch transaction: %v", err) |
|
} |
|
tx2, err := node.Client.GetRawTransaction(txid2) |
|
if err != nil { |
|
t.Fatalf("unable to fetch transaction: %v", err) |
|
} |
|
|
|
targetScript, err := txscript.PayToAddrScript(testAddr) |
|
if err != nil { |
|
t.Fatalf("unable to create target output: %v", err) |
|
} |
|
|
|
// Next, we'll locate the two outputs generated above that pay to use |
|
// so we can properly add them to the filter. |
|
outPoint1, _, err := locateOutput(tx1.MsgTx(), targetScript) |
|
if err != nil { |
|
t.Fatalf("unable to find output: %v", err) |
|
} |
|
outPoint2, _, err := locateOutput(tx2.MsgTx(), targetScript) |
|
if err != nil { |
|
t.Fatalf("unable to find output: %v", err) |
|
} |
|
|
|
_, currentHeight, err = node.Client.GetBestBlock() |
|
if err != nil { |
|
t.Fatalf("unable to get current height: %v", err) |
|
} |
|
|
|
// Now we'll add both outpoints to the current filter. |
|
filter := []channeldb.EdgePoint{ |
|
{FundingPkScript: targetScript, OutPoint: *outPoint1}, |
|
{FundingPkScript: targetScript, OutPoint: *outPoint2}, |
|
} |
|
err = chainView.UpdateFilter(filter, uint32(currentHeight)) |
|
if err != nil { |
|
t.Fatalf("unable to update filter: %v", err) |
|
} |
|
|
|
// With the filter updated, we'll now create two transaction spending |
|
// the outputs we created. |
|
spendingTx1, err := craftSpendTransaction(*outPoint1, targetScript) |
|
if err != nil { |
|
t.Fatalf("unable to create spending tx: %v", err) |
|
} |
|
spendingTx2, err := craftSpendTransaction(*outPoint2, targetScript) |
|
if err != nil { |
|
t.Fatalf("unable to create spending tx: %v", err) |
|
} |
|
|
|
// Now we'll broadcast the first spending transaction and also mine a |
|
// block which should include it. |
|
spendTxid1, err := node.Client.SendRawTransaction(spendingTx1, true) |
|
if err != nil { |
|
t.Fatalf("unable to broadcast transaction: %v", err) |
|
} |
|
err = waitForMempoolTx(node, spendTxid1) |
|
if err != nil { |
|
t.Fatalf("unable to get spending txid in mempool: %v", err) |
|
} |
|
newBlockHashes, err = node.Client.Generate(1) |
|
if err != nil { |
|
t.Fatalf("unable to generate block: %v", err) |
|
} |
|
|
|
// We should receive a notification over the channel. The notification |
|
// should correspond to the current block height and have that single |
|
// filtered transaction. |
|
select { |
|
case filteredBlock := <-blockChan: |
|
assertFilteredBlock(t, filteredBlock, currentHeight+1, |
|
newBlockHashes[0], []*chainhash.Hash{spendTxid1}) |
|
case <-time.After(time.Second * 20): |
|
t.Fatalf("filtered block notification didn't arrive") |
|
} |
|
|
|
// Next, mine the second transaction which spends the second output. |
|
// This should also generate a notification. |
|
spendTxid2, err := node.Client.SendRawTransaction(spendingTx2, true) |
|
if err != nil { |
|
t.Fatalf("unable to broadcast transaction: %v", err) |
|
} |
|
err = waitForMempoolTx(node, spendTxid2) |
|
if err != nil { |
|
t.Fatalf("unable to get spending txid in mempool: %v", err) |
|
} |
|
newBlockHashes, err = node.Client.Generate(1) |
|
if err != nil { |
|
t.Fatalf("unable to generate block: %v", err) |
|
} |
|
|
|
select { |
|
case filteredBlock := <-blockChan: |
|
assertFilteredBlock(t, filteredBlock, currentHeight+2, |
|
newBlockHashes[0], []*chainhash.Hash{spendTxid2}) |
|
case <-time.After(time.Second * 20): |
|
t.Fatalf("filtered block notification didn't arrive") |
|
} |
|
} |
|
|
|
func testUpdateFilterBackTrack(node *rpctest.Harness, |
|
chainView FilteredChainView, chainViewInit chainViewInitFunc, |
|
t *testing.T) { |
|
|
|
// To start, we'll create a fresh output paying to the height generated |
|
// above. |
|
txid, err := getTestTXID(node) |
|
if err != nil { |
|
t.Fatalf("unable to get test txid") |
|
} |
|
err = waitForMempoolTx(node, txid) |
|
if err != nil { |
|
t.Fatalf("unable to get test txid in mempool: %v", err) |
|
} |
|
|
|
// Next we'll mine a block confirming the output generated above. |
|
initBlockHashes, err := node.Client.Generate(1) |
|
if err != nil { |
|
t.Fatalf("unable to generate block: %v", err) |
|
} |
|
|
|
blockChan := chainView.FilteredBlocks() |
|
|
|
_, currentHeight, err := node.Client.GetBestBlock() |
|
if err != nil { |
|
t.Fatalf("unable to get current height: %v", err) |
|
} |
|
|
|
// Consume the notification sent which contains an empty filtered |
|
// block. |
|
select { |
|
case filteredBlock := <-blockChan: |
|
assertFilteredBlock(t, filteredBlock, currentHeight, |
|
initBlockHashes[0], []*chainhash.Hash{}) |
|
case <-time.After(time.Second * 20): |
|
t.Fatalf("filtered block notification didn't arrive") |
|
} |
|
|
|
// Next, create a transaction which spends the output created above, |
|
// mining the spend into a block. |
|
tx, err := node.Client.GetRawTransaction(txid) |
|
if err != nil { |
|
t.Fatalf("unable to fetch transaction: %v", err) |
|
} |
|
outPoint, _, err := locateOutput(tx.MsgTx(), testScript) |
|
if err != nil { |
|
t.Fatalf("unable to find output: %v", err) |
|
} |
|
spendingTx, err := craftSpendTransaction(*outPoint, testScript) |
|
if err != nil { |
|
t.Fatalf("unable to create spending tx: %v", err) |
|
} |
|
spendTxid, err := node.Client.SendRawTransaction(spendingTx, true) |
|
if err != nil { |
|
t.Fatalf("unable to broadcast transaction: %v", err) |
|
} |
|
err = waitForMempoolTx(node, spendTxid) |
|
if err != nil { |
|
t.Fatalf("unable to get spending txid in mempool: %v", err) |
|
} |
|
newBlockHashes, err := node.Client.Generate(1) |
|
if err != nil { |
|
t.Fatalf("unable to generate block: %v", err) |
|
} |
|
|
|
// We should have received another empty filtered block notification. |
|
select { |
|
case filteredBlock := <-blockChan: |
|
assertFilteredBlock(t, filteredBlock, currentHeight+1, |
|
newBlockHashes[0], []*chainhash.Hash{}) |
|
case <-time.After(time.Second * 20): |
|
t.Fatalf("filtered block notification didn't arrive") |
|
} |
|
|
|
// After the block has been mined+notified we'll update the filter with |
|
// a _prior_ height so a "rewind" occurs. |
|
filter := []channeldb.EdgePoint{ |
|
{FundingPkScript: testScript, OutPoint: *outPoint}, |
|
} |
|
err = chainView.UpdateFilter(filter, uint32(currentHeight)) |
|
if err != nil { |
|
t.Fatalf("unable to update filter: %v", err) |
|
} |
|
|
|
// We should now receive a fresh filtered block notification that |
|
// includes the transaction spend we included above. |
|
select { |
|
case filteredBlock := <-blockChan: |
|
assertFilteredBlock(t, filteredBlock, currentHeight+1, |
|
newBlockHashes[0], []*chainhash.Hash{spendTxid}) |
|
case <-time.After(time.Second * 20): |
|
t.Fatalf("filtered block notification didn't arrive") |
|
} |
|
} |
|
|
|
func testFilterSingleBlock(node *rpctest.Harness, chainView FilteredChainView, |
|
chainViewInit chainViewInitFunc, t *testing.T) { |
|
|
|
// In this test, we'll test the manual filtration of blocks, which can |
|
// be used by clients to manually rescan their sub-set of the UTXO set. |
|
|
|
// First, we'll create a block that includes two outputs that we're |
|
// able to spend with the private key generated above. |
|
txid1, err := getTestTXID(node) |
|
if err != nil { |
|
t.Fatalf("unable to get test txid") |
|
} |
|
err = waitForMempoolTx(node, txid1) |
|
if err != nil { |
|
t.Fatalf("unable to get test txid in mempool: %v", err) |
|
} |
|
txid2, err := getTestTXID(node) |
|
if err != nil { |
|
t.Fatalf("unable to get test txid") |
|
} |
|
err = waitForMempoolTx(node, txid2) |
|
if err != nil { |
|
t.Fatalf("unable to get test txid in mempool: %v", err) |
|
} |
|
|
|
blockChan := chainView.FilteredBlocks() |
|
|
|
// Next we'll mine a block confirming the output generated above. |
|
newBlockHashes, err := node.Client.Generate(1) |
|
if err != nil { |
|
t.Fatalf("unable to generate block: %v", err) |
|
} |
|
|
|
_, currentHeight, err := node.Client.GetBestBlock() |
|
if err != nil { |
|
t.Fatalf("unable to get current height: %v", err) |
|
} |
|
|
|
// We should get an update, however it shouldn't yet contain any |
|
// filtered transaction as the filter hasn't been updated. |
|
select { |
|
case filteredBlock := <-blockChan: |
|
assertFilteredBlock(t, filteredBlock, currentHeight, |
|
newBlockHashes[0], []*chainhash.Hash{}) |
|
case <-time.After(time.Second * 20): |
|
t.Fatalf("filtered block notification didn't arrive") |
|
} |
|
|
|
tx1, err := node.Client.GetRawTransaction(txid1) |
|
if err != nil { |
|
t.Fatalf("unable to fetch transaction: %v", err) |
|
} |
|
tx2, err := node.Client.GetRawTransaction(txid2) |
|
if err != nil { |
|
t.Fatalf("unable to fetch transaction: %v", err) |
|
} |
|
|
|
// Next, we'll create a block that includes two transactions, each |
|
// which spend one of the outputs created. |
|
outPoint1, _, err := locateOutput(tx1.MsgTx(), testScript) |
|
if err != nil { |
|
t.Fatalf("unable to find output: %v", err) |
|
} |
|
outPoint2, _, err := locateOutput(tx2.MsgTx(), testScript) |
|
if err != nil { |
|
t.Fatalf("unable to find output: %v", err) |
|
} |
|
spendingTx1, err := craftSpendTransaction(*outPoint1, testScript) |
|
if err != nil { |
|
t.Fatalf("unable to create spending tx: %v", err) |
|
} |
|
spendingTx2, err := craftSpendTransaction(*outPoint2, testScript) |
|
if err != nil { |
|
t.Fatalf("unable to create spending tx: %v", err) |
|
} |
|
txns := []*btcutil.Tx{btcutil.NewTx(spendingTx1), btcutil.NewTx(spendingTx2)} |
|
block, err := node.GenerateAndSubmitBlock(txns, 11, time.Time{}) |
|
if err != nil { |
|
t.Fatalf("unable to generate block: %v", err) |
|
} |
|
|
|
select { |
|
case filteredBlock := <-blockChan: |
|
assertFilteredBlock(t, filteredBlock, currentHeight+1, |
|
block.Hash(), []*chainhash.Hash{}) |
|
case <-time.After(time.Second * 20): |
|
t.Fatalf("filtered block notification didn't arrive") |
|
} |
|
|
|
_, currentHeight, err = node.Client.GetBestBlock() |
|
if err != nil { |
|
t.Fatalf("unable to get current height: %v", err) |
|
} |
|
|
|
// Now we'll manually trigger filtering the block generated above. |
|
// First, we'll add the two outpoints to our filter. |
|
filter := []channeldb.EdgePoint{ |
|
{FundingPkScript: testScript, OutPoint: *outPoint1}, |
|
{FundingPkScript: testScript, OutPoint: *outPoint2}, |
|
} |
|
err = chainView.UpdateFilter(filter, uint32(currentHeight)) |
|
if err != nil { |
|
t.Fatalf("unable to update filter: %v", err) |
|
} |
|
|
|
// We set the filter with the current height, so we shouldn't get any |
|
// notifications. |
|
select { |
|
case <-blockChan: |
|
t.Fatalf("got filter notification, but shouldn't have") |
|
default: |
|
} |
|
|
|
// Now we'll manually rescan that past block. This should include two |
|
// filtered transactions, the spending transactions we created above. |
|
filteredBlock, err := chainView.FilterBlock(block.Hash()) |
|
if err != nil { |
|
t.Fatalf("unable to filter block: %v", err) |
|
} |
|
txn1, txn2 := spendingTx1.TxHash(), spendingTx2.TxHash() |
|
expectedTxns := []*chainhash.Hash{&txn1, &txn2} |
|
assertFilteredBlock(t, filteredBlock, currentHeight, block.Hash(), |
|
expectedTxns) |
|
} |
|
|
|
// testFilterBlockDisconnected triggers a reorg all the way back to genesis, |
|
// and a small 5 block reorg, ensuring the chainView notifies about |
|
// disconnected and connected blocks in the order we expect. |
|
func testFilterBlockDisconnected(node *rpctest.Harness, |
|
chainView FilteredChainView, chainViewInit chainViewInitFunc, |
|
t *testing.T) { |
|
|
|
// Create a node that has a shorter chain than the main chain, so we |
|
// can trigger a reorg. |
|
reorgNode, err := rpctest.New(netParams, nil, []string{"--txindex"}, "") |
|
if err != nil { |
|
t.Fatalf("unable to create mining node: %v", err) |
|
} |
|
defer reorgNode.TearDown() |
|
|
|
// This node's chain will be 105 blocks. |
|
if err := reorgNode.SetUp(true, 5); err != nil { |
|
t.Fatalf("unable to set up mining node: %v", err) |
|
} |
|
|
|
// Init a chain view that has this node as its block source. |
|
cleanUpFunc, reorgView, err := chainViewInit(reorgNode.RPCConfig(), |
|
reorgNode.P2PAddress()) |
|
if err != nil { |
|
t.Fatalf("unable to create chain view: %v", err) |
|
} |
|
defer func() { |
|
if cleanUpFunc != nil { |
|
cleanUpFunc() |
|
} |
|
}() |
|
|
|
if err = reorgView.Start(); err != nil { |
|
t.Fatalf("unable to start btcd chain view: %v", err) |
|
} |
|
defer reorgView.Stop() |
|
|
|
newBlocks := reorgView.FilteredBlocks() |
|
disconnectedBlocks := reorgView.DisconnectedBlocks() |
|
|
|
// If this the neutrino backend, then we'll give it some time to catch |
|
// up, as it's a bit slower to consume new blocks compared to the RPC |
|
// backends. |
|
if _, ok := reorgView.(*CfFilteredChainView); ok { |
|
time.Sleep(time.Second * 3) |
|
} |
|
|
|
_, oldHeight, err := reorgNode.Client.GetBestBlock() |
|
if err != nil { |
|
t.Fatalf("unable to get current height: %v", err) |
|
} |
|
|
|
// Now connect the node with the short chain to the main node, and wait |
|
// for their chains to synchronize. The short chain will be reorged all |
|
// the way back to genesis. |
|
if err := rpctest.ConnectNode(reorgNode, node); err != nil { |
|
t.Fatalf("unable to connect harnesses: %v", err) |
|
} |
|
nodeSlice := []*rpctest.Harness{node, reorgNode} |
|
if err := rpctest.JoinNodes(nodeSlice, rpctest.Blocks); err != nil { |
|
t.Fatalf("unable to join node on blocks: %v", err) |
|
} |
|
|
|
_, newHeight, err := reorgNode.Client.GetBestBlock() |
|
if err != nil { |
|
t.Fatalf("unable to get current height: %v", err) |
|
} |
|
|
|
// We should be getting oldHeight number of blocks marked as |
|
// stale/disconnected. We expect to first get all stale blocks, |
|
// then the new blocks. We also ensure a strict ordering. |
|
for i := int32(0); i < oldHeight+newHeight; i++ { |
|
select { |
|
case block := <-newBlocks: |
|
if i < oldHeight { |
|
t.Fatalf("did not expect to get new block "+ |
|
"in iteration %d, old height: %v", i, |
|
oldHeight) |
|
} |
|
expectedHeight := uint32(i - oldHeight + 1) |
|
if block.Height != expectedHeight { |
|
t.Fatalf("expected to receive connected "+ |
|
"block at height %d, instead got at %d", |
|
expectedHeight, block.Height) |
|
} |
|
case block := <-disconnectedBlocks: |
|
if i >= oldHeight { |
|
t.Fatalf("did not expect to get stale block "+ |
|
"in iteration %d", i) |
|
} |
|
expectedHeight := uint32(oldHeight - i) |
|
if block.Height != expectedHeight { |
|
t.Fatalf("expected to receive disconnected "+ |
|
"block at height %d, instead got at %d", |
|
expectedHeight, block.Height) |
|
} |
|
case <-time.After(10 * time.Second): |
|
t.Fatalf("timeout waiting for block") |
|
} |
|
} |
|
|
|
// Now we trigger a small reorg, by disconnecting the nodes, mining |
|
// a few blocks on each, then connecting them again. |
|
peers, err := reorgNode.Client.GetPeerInfo() |
|
if err != nil { |
|
t.Fatalf("unable to get peer info: %v", err) |
|
} |
|
numPeers := len(peers) |
|
|
|
// Disconnect the nodes. |
|
err = reorgNode.Client.AddNode(node.P2PAddress(), rpcclient.ANRemove) |
|
if err != nil { |
|
t.Fatalf("unable to disconnect mining nodes: %v", err) |
|
} |
|
|
|
// Wait for disconnection |
|
for { |
|
peers, err = reorgNode.Client.GetPeerInfo() |
|
if err != nil { |
|
t.Fatalf("unable to get peer info: %v", err) |
|
} |
|
if len(peers) < numPeers { |
|
break |
|
} |
|
time.Sleep(100 * time.Millisecond) |
|
} |
|
|
|
// Mine 10 blocks on the main chain, 5 on the chain that will be |
|
// reorged out, |
|
if _, err := node.Client.Generate(10); err != nil { |
|
t.Fatalf("unable to generate blocks on main chain: %v", err) |
|
} |
|
if _, err := reorgNode.Client.Generate(5); err != nil { |
|
t.Fatalf("unable to generate blocks on reorged chain: %v", err) |
|
} |
|
|
|
// 5 new blocks should get notified. |
|
for i := uint32(0); i < 5; i++ { |
|
select { |
|
case block := <-newBlocks: |
|
expectedHeight := uint32(newHeight) + i + 1 |
|
if block.Height != expectedHeight { |
|
t.Fatalf("expected to receive connected "+ |
|
"block at height %d, instead got at %d", |
|
expectedHeight, block.Height) |
|
} |
|
case <-disconnectedBlocks: |
|
t.Fatalf("did not expect to get stale block "+ |
|
"in iteration %d", i) |
|
case <-time.After(10 * time.Second): |
|
t.Fatalf("did not get connected block") |
|
} |
|
} |
|
|
|
_, oldHeight, err = reorgNode.Client.GetBestBlock() |
|
if err != nil { |
|
t.Fatalf("unable to get current height: %v", err) |
|
} |
|
|
|
// Now connect the two nodes, and wait for their chains to sync up. |
|
if err := rpctest.ConnectNode(reorgNode, node); err != nil { |
|
t.Fatalf("unable to connect harnesses: %v", err) |
|
} |
|
if err := rpctest.JoinNodes(nodeSlice, rpctest.Blocks); err != nil { |
|
t.Fatalf("unable to join node on blocks: %v", err) |
|
} |
|
|
|
if _, _, err := reorgNode.Client.GetBestBlock(); err != nil { |
|
t.Fatalf("unable to get current height: %v", err) |
|
} |
|
|
|
// We should get 5 disconnected, 10 connected blocks. |
|
for i := uint32(0); i < 15; i++ { |
|
select { |
|
case block := <-newBlocks: |
|
if i < 5 { |
|
t.Fatalf("did not expect to get new block "+ |
|
"in iteration %d", i) |
|
} |
|
// The expected height for the connected block will be |
|
// oldHeight - 5 (the 5 disconnected blocks) + (i-5) |
|
// (subtract 5 since the 5 first iterations consumed |
|
// disconnected blocks) + 1 |
|
expectedHeight := uint32(oldHeight) - 9 + i |
|
if block.Height != expectedHeight { |
|
t.Fatalf("expected to receive connected "+ |
|
"block at height %d, instead got at %d", |
|
expectedHeight, block.Height) |
|
} |
|
case block := <-disconnectedBlocks: |
|
if i >= 5 { |
|
t.Fatalf("did not expect to get stale block "+ |
|
"in iteration %d", i) |
|
} |
|
expectedHeight := uint32(oldHeight) - i |
|
if block.Height != expectedHeight { |
|
t.Fatalf("expected to receive disconnected "+ |
|
"block at height %d, instead got at %d", |
|
expectedHeight, block.Height) |
|
} |
|
case <-time.After(10 * time.Second): |
|
t.Fatalf("did not get disconnected block") |
|
} |
|
} |
|
|
|
// Time for db access to finish between testcases. |
|
time.Sleep(time.Millisecond * 500) |
|
} |
|
|
|
type chainViewInitFunc func(rpcInfo rpcclient.ConnConfig, |
|
p2pAddr string) (func(), FilteredChainView, error) |
|
|
|
type testCase struct { |
|
name string |
|
test func(*rpctest.Harness, FilteredChainView, chainViewInitFunc, |
|
*testing.T) |
|
} |
|
|
|
var chainViewTests = []testCase{ |
|
{ |
|
name: "filtered block ntfns", |
|
test: testFilterBlockNotifications, |
|
}, |
|
{ |
|
name: "update filter back track", |
|
test: testUpdateFilterBackTrack, |
|
}, |
|
{ |
|
name: "filter single block", |
|
test: testFilterSingleBlock, |
|
}, |
|
{ |
|
name: "filter block disconnected", |
|
test: testFilterBlockDisconnected, |
|
}, |
|
} |
|
|
|
var interfaceImpls = []struct { |
|
name string |
|
chainViewInit chainViewInitFunc |
|
}{ |
|
{ |
|
name: "bitcoind_zmq", |
|
chainViewInit: func(_ rpcclient.ConnConfig, p2pAddr string) (func(), FilteredChainView, error) { |
|
// Start a bitcoind instance. |
|
tempBitcoindDir, err := ioutil.TempDir("", "bitcoind") |
|
if err != nil { |
|
return nil, nil, err |
|
} |
|
zmqBlockHost := "ipc:///" + tempBitcoindDir + "/blocks.socket" |
|
zmqTxHost := "ipc:///" + tempBitcoindDir + "/tx.socket" |
|
cleanUp1 := func() { |
|
os.RemoveAll(tempBitcoindDir) |
|
} |
|
rpcPort := rand.Int()%(65536-1024) + 1024 |
|
bitcoind := exec.Command( |
|
"bitcoind", |
|
"-datadir="+tempBitcoindDir, |
|
"-regtest", |
|
"-connect="+p2pAddr, |
|
"-txindex", |
|
"-rpcauth=weks:469e9bb14ab2360f8e226efed5ca6f"+ |
|
"d$507c670e800a95284294edb5773b05544b"+ |
|
"220110063096c221be9933c82d38e1", |
|
fmt.Sprintf("-rpcport=%d", rpcPort), |
|
"-disablewallet", |
|
"-zmqpubrawblock="+zmqBlockHost, |
|
"-zmqpubrawtx="+zmqTxHost, |
|
) |
|
err = bitcoind.Start() |
|
if err != nil { |
|
cleanUp1() |
|
return nil, nil, err |
|
} |
|
cleanUp2 := func() { |
|
bitcoind.Process.Kill() |
|
bitcoind.Wait() |
|
cleanUp1() |
|
} |
|
|
|
// Wait for the bitcoind instance to start up. |
|
time.Sleep(time.Second) |
|
|
|
host := fmt.Sprintf("127.0.0.1:%d", rpcPort) |
|
chainConn, err := chain.NewBitcoindConn( |
|
&chaincfg.RegressionNetParams, host, "weks", |
|
"weks", zmqBlockHost, zmqTxHost, |
|
100*time.Millisecond, |
|
) |
|
if err != nil { |
|
return cleanUp2, nil, fmt.Errorf("unable to "+ |
|
"establish connection to bitcoind: %v", |
|
err) |
|
} |
|
if err := chainConn.Start(); err != nil { |
|
return cleanUp2, nil, fmt.Errorf("unable to "+ |
|
"establish connection to bitcoind: %v", |
|
err) |
|
} |
|
cleanUp3 := func() { |
|
chainConn.Stop() |
|
cleanUp2() |
|
} |
|
|
|
chainView := NewBitcoindFilteredChainView(chainConn) |
|
|
|
return cleanUp3, chainView, nil |
|
}, |
|
}, |
|
{ |
|
name: "p2p_neutrino", |
|
chainViewInit: func(_ rpcclient.ConnConfig, p2pAddr string) (func(), FilteredChainView, error) { |
|
spvDir, err := ioutil.TempDir("", "neutrino") |
|
if err != nil { |
|
return nil, nil, err |
|
} |
|
|
|
dbName := filepath.Join(spvDir, "neutrino.db") |
|
spvDatabase, err := walletdb.Create( |
|
"bdb", dbName, true, kvdb.DefaultDBTimeout, |
|
) |
|
if err != nil { |
|
return nil, nil, err |
|
} |
|
|
|
spvConfig := neutrino.Config{ |
|
DataDir: spvDir, |
|
Database: spvDatabase, |
|
ChainParams: *netParams, |
|
ConnectPeers: []string{p2pAddr}, |
|
} |
|
|
|
spvNode, err := neutrino.NewChainService(spvConfig) |
|
if err != nil { |
|
return nil, nil, err |
|
} |
|
spvNode.Start() |
|
|
|
// Wait until the node has fully synced up to the local |
|
// btcd node. |
|
for !spvNode.IsCurrent() { |
|
time.Sleep(time.Millisecond * 100) |
|
} |
|
|
|
cleanUp := func() { |
|
spvDatabase.Close() |
|
spvNode.Stop() |
|
os.RemoveAll(spvDir) |
|
} |
|
|
|
chainView, err := NewCfFilteredChainView(spvNode) |
|
if err != nil { |
|
return nil, nil, err |
|
} |
|
|
|
return cleanUp, chainView, nil |
|
}, |
|
}, |
|
{ |
|
name: "btcd_websockets", |
|
chainViewInit: func(config rpcclient.ConnConfig, _ string) (func(), FilteredChainView, error) { |
|
chainView, err := NewBtcdFilteredChainView(config) |
|
if err != nil { |
|
return nil, nil, err |
|
} |
|
|
|
return nil, chainView, err |
|
}, |
|
}, |
|
} |
|
|
|
func TestFilteredChainView(t *testing.T) { |
|
// Initialize the harness around a btcd node which will serve as our |
|
// dedicated miner to generate blocks, cause re-orgs, etc. We'll set up |
|
// this node with a chain length of 125, so we have plenty of BTC to |
|
// play around with. |
|
miner, err := rpctest.New(netParams, nil, []string{"--txindex"}, "") |
|
if err != nil { |
|
t.Fatalf("unable to create mining node: %v", err) |
|
} |
|
defer miner.TearDown() |
|
if err := miner.SetUp(true, 25); err != nil { |
|
t.Fatalf("unable to set up mining node: %v", err) |
|
} |
|
|
|
rpcConfig := miner.RPCConfig() |
|
p2pAddr := miner.P2PAddress() |
|
|
|
for _, chainViewImpl := range interfaceImpls { |
|
t.Logf("Testing '%v' implementation of FilteredChainView", |
|
chainViewImpl.name) |
|
|
|
cleanUpFunc, chainView, err := chainViewImpl.chainViewInit(rpcConfig, p2pAddr) |
|
if err != nil { |
|
t.Fatalf("unable to make chain view: %v", err) |
|
} |
|
|
|
if err := chainView.Start(); err != nil { |
|
t.Fatalf("unable to start chain view: %v", err) |
|
} |
|
for _, chainViewTest := range chainViewTests { |
|
testName := fmt.Sprintf("%v: %v", chainViewImpl.name, |
|
chainViewTest.name) |
|
success := t.Run(testName, func(t *testing.T) { |
|
chainViewTest.test(miner, chainView, |
|
chainViewImpl.chainViewInit, t) |
|
}) |
|
|
|
if !success { |
|
break |
|
} |
|
} |
|
|
|
if err := chainView.Stop(); err != nil { |
|
t.Fatalf("unable to stop chain view: %v", err) |
|
} |
|
|
|
if cleanUpFunc != nil { |
|
cleanUpFunc() |
|
} |
|
} |
|
}
|
|
|