diff --git a/integration/getchaintips_test.go b/integration/getchaintips_test.go new file mode 100644 index 00000000..1570ba74 --- /dev/null +++ b/integration/getchaintips_test.go @@ -0,0 +1,350 @@ +package integration + +import ( + "encoding/hex" + "fmt" + "testing" + + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/integration/rpctest" + "github.com/stretchr/testify/require" +) + +func getBlockFromString(t *testing.T, hexStr string) *btcutil.Block { + t.Helper() + + serializedBlock, err := hex.DecodeString(hexStr) + if err != nil { + t.Fatalf("couldn't decode hex string of %s", hexStr) + } + + block, err := btcutil.NewBlockFromBytes(serializedBlock) + if err != nil { + t.Fatalf("couldn't make a new block from bytes. "+ + "Decoded hex string: %s", hexStr) + } + + return block +} + +// compareMultipleChainTips checks that all the expected chain tips are included in got chain tips and +// verifies that the got chain tip matches the expected chain tip. +func compareMultipleChainTips(t *testing.T, gotChainTips, expectedChainTips []*btcjson.GetChainTipsResult) error { + if len(gotChainTips) != len(expectedChainTips) { + return fmt.Errorf("Expected %d chaintips but got %d", len(expectedChainTips), len(gotChainTips)) + } + + gotChainTipsMap := make(map[string]btcjson.GetChainTipsResult) + for _, gotChainTip := range gotChainTips { + gotChainTipsMap[gotChainTip.Hash] = *gotChainTip + } + + for _, expectedChainTip := range expectedChainTips { + gotChainTip, found := gotChainTipsMap[expectedChainTip.Hash] + if !found { + return fmt.Errorf("Couldn't find expected chaintip with hash %s", expectedChainTip.Hash) + } + + require.Equal(t, gotChainTip, *expectedChainTip) + } + + return nil +} + +func TestGetChainTips(t *testing.T) { + // block1Hex is a block that builds on top of the regtest genesis block. + // Has blockhash of "36c056247e8c0589f6307995e4e13acf2b2b79cad9ecd5a4eeab2131ed0ecde5". + block1Hex := "0000002006226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf18891" + + "0f71881025ae0d41ce8748b79ac40e5f3197af3bb83a594def7943aff0fce504c638ea6d63f" + + "fff7f2000000000010200000000010100000000000000000000000000000000000000000000" + + "00000000000000000000ffffffff025100ffffffff0200f2052a010000001600149b0f9d020" + + "8b3b425246e16830562a63bf1c701180000000000000000266a24aa21a9ede2f61c3f71d1de" + + "fd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000" + + "000000000000000000000000000000000000000000000000000" + + // block2Hex is a block that builds on top of block1Hex. + // Has blockhash of "664b51334782a4ad16e8471b530dcd0027c75b8c25187b41dfc85ecd353295c6". + block2Hex := "00000020e5cd0eed3121abeea4d5ecd9ca792b2bcf3ae1e4957930f689058c7e2456c0" + + "362a78a11b875d31af2ea493aa5b6b623e0d481f11e69f7147ab974be9da087f3e24696f63f" + + "fff7f2001000000010200000000010100000000000000000000000000000000000000000000" + + "00000000000000000000ffffffff025200ffffffff0200f2052a0100000016001470fea1feb" + + "4969c1f237753ae29c0217c6637835c0000000000000000266a24aa21a9ede2f61c3f71d1de" + + "fd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000" + + "000000000000000000000000000000000000000000000000000" + + // block3Hex is a block that builds on top of block2Hex. + // Has blockhash of "17a5c5cb90ecde5a46dd195d434eea46b653e35e4517070eade429db3ac83944". + block3Hex := "00000020c6953235cd5ec8df417b18258c5bc72700cd0d531b47e816ada4824733514b" + + "66c3ad4d567a36c20df07ea0b7fce1e4b4ee5be3eaf0b946b0ae73f3a74d47f0cf99696f63f" + + "fff7f2000000000010200000000010100000000000000000000000000000000000000000000" + + "00000000000000000000ffffffff025300ffffffff0200f2052a010000001600140e835869b" + + "154f647d11376634b5e8c785e7d21060000000000000000266a24aa21a9ede2f61c3f71d1de" + + "fd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000" + + "000000000000000000000000000000000000000000000000000" + + // block4Hex is a block that builds on top of block3Hex. + // Has blockhash of "7b357f3073c4397d6d069a32a09141c32560f3c62233ca138eb5e03c5991f45c". + block4Hex := "000000204439c83adb29e4ad0e0717455ee353b646ea4e435d19dd465adeec90cbc5a5" + + "17ab639a5dd622e90f5f9feffc1c7c28f47a2caf85c21d7dd52cd223a7164619e37a6a6f63f" + + "fff7f2004000000010200000000010100000000000000000000000000000000000000000000" + + "00000000000000000000ffffffff025400ffffffff0200f2052a01000000160014a157c74b4" + + "42a3e11b45cf5273f8c0c032c5a40ed0000000000000000266a24aa21a9ede2f61c3f71d1de" + + "fd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000" + + "000000000000000000000000000000000000000000000000000" + + // block2aHex is a block that builds on top of block1Hex. + // Has blockhash of "5181a4e34cc23ed95c69749dedf4cc7ebd659243bc1683372f8940c8cd8f9b68". + block2aHex := "00000020e5cd0eed3121abeea4d5ecd9ca792b2bcf3ae1e4957930f689058c7e2456c" + + "036f7d4ebe524260c9b6c2b5e3d105cad0b7ddfaeaa29971363574fc1921a3f2f7ad66b6f63" + + "ffff7f200100000001020000000001010000000000000000000000000000000000000000000" + + "000000000000000000000ffffffff025200ffffffff0200f2052a0100000016001466fca22d" + + "0e4679d119ea1e127c984746a1f7e66c0000000000000000266a24aa21a9ede2f61c3f71d1d" + + "efd3fa999dfa36953755c690689799962b48bebd836974e8cf9012000000000000000000000" + + "0000000000000000000000000000000000000000000000000000" + + // block3aHex is a block that builds on top of block2aHex. + // Has blockhash of "0b0216936d1a5c01362256d06a9c9a2b13768fa2f2748549a71008af36dd167f". + block3aHex := "00000020689b8fcdc840892f378316bc439265bd7eccf4ed9d74695cd93ec24ce3a48" + + "15161a430ce5cae955b1254b753bc95854d942947855d3ae59002de9773b7fe65fdf16b6f63" + + "ffff7f200100000001020000000001010000000000000000000000000000000000000000000" + + "000000000000000000000ffffffff025300ffffffff0200f2052a0100000016001471da0afb" + + "883c228b18af6bd0cabc471aebe8d1750000000000000000266a24aa21a9ede2f61c3f71d1d" + + "efd3fa999dfa36953755c690689799962b48bebd836974e8cf9012000000000000000000000" + + "0000000000000000000000000000000000000000000000000000" + + // block4aHex is a block that builds on top of block3aHex. + // Has blockhash of "65a00a026eaa83f6e7a7f4a920faa090f3f9d3565a56df2362db2ab2fa14ccec". + block4aHex := "000000207f16dd36af0810a7498574f2a28f76132b9a9c6ad0562236015c1a6d93160" + + "20b951fa5ee5072d88d6aef9601999307dbd8d96dad067b80bfe04afe81c7a8c21beb706f63" + + "ffff7f200000000001020000000001010000000000000000000000000000000000000000000" + + "000000000000000000000ffffffff025400ffffffff0200f2052a01000000160014fd1f118c" + + "95a712b8adef11c3cc0643bcb6b709f10000000000000000266a24aa21a9ede2f61c3f71d1d" + + "efd3fa999dfa36953755c690689799962b48bebd836974e8cf9012000000000000000000000" + + "0000000000000000000000000000000000000000000000000000" + + // block5aHex is a block that builds on top of block4aHex. + // Has blockhash of "5c8814bc034a4c37fa5ccdc05e09b45a771bd7505d68092f21869a912737ee10". + block5aHex := "00000020eccc14fab22adb6223df565a56d3f9f390a0fa20a9f4a7e7f683aa6e020aa" + + "0656331bd4fcd3db611de7fbf72ef3dff0b85b244b5a983d5c0270e728214f67f9aaa766f63" + + "ffff7f200600000001020000000001010000000000000000000000000000000000000000000" + + "000000000000000000000ffffffff025500ffffffff0200f2052a0100000016001438335896" + + "ad1d087e3541436a5b293c0d23ad27e60000000000000000266a24aa21a9ede2f61c3f71d1d" + + "efd3fa999dfa36953755c690689799962b48bebd836974e8cf9012000000000000000000000" + + "0000000000000000000000000000000000000000000000000000" + + // block4bHex is a block that builds on top of block3aHex. + // Has blockhash of "130458e795cc46f2759195e92737426fb0ada2a07f98434551ffb7500b23c161". + block4bHex := "000000207f16dd36af0810a7498574f2a28f76132b9a9c6ad0562236015c1a6d93160" + + "20b14f9ce93d0144c383fea72f408b06b268a1523a029b825a1edfa15b367f6db2cfd7d6f63" + + "ffff7f200200000001020000000001010000000000000000000000000000000000000000000" + + "000000000000000000000ffffffff025400ffffffff0200f2052a0100000016001405b5ba2d" + + "1e549c4c84a623de3575948d3ef8a27f0000000000000000266a24aa21a9ede2f61c3f71d1d" + + "efd3fa999dfa36953755c690689799962b48bebd836974e8cf9012000000000000000000000" + + "0000000000000000000000000000000000000000000000000000" + + // Set up regtest chain. + r, err := rpctest.New(&chaincfg.RegressionNetParams, nil, nil, "") + if err != nil { + t.Fatal("TestGetChainTips fail. Unable to create primary harness: ", err) + } + if err := r.SetUp(true, 0); err != nil { + t.Fatalf("TestGetChainTips fail. Unable to setup test chain: %v", err) + } + defer r.TearDown() + + // Immediately call getchaintips after setting up regtest. + gotChainTips, err := r.Client.GetChainTips() + if err != nil { + t.Fatal(err) + } + // We expect a single genesis block. + expectedChainTips := []*btcjson.GetChainTipsResult{ + { + Height: 0, + Hash: chaincfg.RegressionNetParams.GenesisHash.String(), + BranchLen: 0, + Status: "active", + }, + } + err = compareMultipleChainTips(t, gotChainTips, expectedChainTips) + if err != nil { + t.Fatalf("TestGetChainTips fail. Error: %v", err) + } + + // Submit 4 blocks. + // + // Our chain view looks like so: + // (genesis block) -> 1 -> 2 -> 3 -> 4 + blockStrings := []string{block1Hex, block2Hex, block3Hex, block4Hex} + for _, blockString := range blockStrings { + block := getBlockFromString(t, blockString) + err = r.Client.SubmitBlock(block, nil) + if err != nil { + t.Fatal(err) + } + } + + gotChainTips, err = r.Client.GetChainTips() + if err != nil { + t.Fatal(err) + } + expectedChainTips = []*btcjson.GetChainTipsResult{ + { + Height: 4, + Hash: getBlockFromString(t, blockStrings[len(blockStrings)-1]).Hash().String(), + BranchLen: 0, + Status: "active", + }, + } + err = compareMultipleChainTips(t, gotChainTips, expectedChainTips) + if err != nil { + t.Fatalf("TestGetChainTips fail. Error: %v", err) + } + + // Submit 2 blocks that don't build on top of the current active tip. + // + // Our chain view looks like so: + // (genesis block) -> 1 -> 2 -> 3 -> 4 (active) + // \ -> 2a -> 3a (valid-fork) + blockStrings = []string{block2aHex, block3aHex} + for _, blockString := range blockStrings { + block := getBlockFromString(t, blockString) + err = r.Client.SubmitBlock(block, nil) + if err != nil { + t.Fatal(err) + } + } + + gotChainTips, err = r.Client.GetChainTips() + if err != nil { + t.Fatal(err) + } + expectedChainTips = []*btcjson.GetChainTipsResult{ + { + Height: 4, + Hash: getBlockFromString(t, block4Hex).Hash().String(), + BranchLen: 0, + Status: "active", + }, + { + Height: 3, + Hash: getBlockFromString(t, block3aHex).Hash().String(), + BranchLen: 2, + Status: "valid-fork", + }, + } + err = compareMultipleChainTips(t, gotChainTips, expectedChainTips) + if err != nil { + t.Fatalf("TestGetChainTips fail. Error: %v", err) + } + + // Submit a single block that don't build on top of the current active tip. + // + // Our chain view looks like so: + // (genesis block) -> 1 -> 2 -> 3 -> 4 (active) + // \ -> 2a -> 3a -> 4a (valid-fork) + block := getBlockFromString(t, block4aHex) + err = r.Client.SubmitBlock(block, nil) + if err != nil { + t.Fatal(err) + } + + gotChainTips, err = r.Client.GetChainTips() + if err != nil { + t.Fatal(err) + } + expectedChainTips = []*btcjson.GetChainTipsResult{ + { + Height: 4, + Hash: getBlockFromString(t, block4Hex).Hash().String(), + BranchLen: 0, + Status: "active", + }, + { + Height: 4, + Hash: getBlockFromString(t, block4aHex).Hash().String(), + BranchLen: 3, + Status: "valid-fork", + }, + } + err = compareMultipleChainTips(t, gotChainTips, expectedChainTips) + if err != nil { + t.Fatalf("TestGetChainTips fail. Error: %v", err) + } + + // Submit a single block that changes the active branch to 5a. + // + // Our chain view looks like so: + // (genesis block) -> 1 -> 2 -> 3 -> 4 (valid-fork) + // \ -> 2a -> 3a -> 4a -> 5a (active) + block = getBlockFromString(t, block5aHex) + err = r.Client.SubmitBlock(block, nil) + if err != nil { + t.Fatal(err) + } + gotChainTips, err = r.Client.GetChainTips() + if err != nil { + t.Fatal(err) + } + expectedChainTips = []*btcjson.GetChainTipsResult{ + { + Height: 4, + Hash: getBlockFromString(t, block4Hex).Hash().String(), + BranchLen: 3, + Status: "valid-fork", + }, + { + Height: 5, + Hash: getBlockFromString(t, block5aHex).Hash().String(), + BranchLen: 0, + Status: "active", + }, + } + err = compareMultipleChainTips(t, gotChainTips, expectedChainTips) + if err != nil { + t.Fatalf("TestGetChainTips fail. Error: %v", err) + } + + // Submit a single block that builds on top of 3a. + // + // Our chain view looks like so: + // (genesis block) -> 1 -> 2 -> 3 -> 4 (valid-fork) + // \ -> 2a -> 3a -> 4a -> 5a (active) + // \ -> 4b (valid-fork) + block = getBlockFromString(t, block4bHex) + err = r.Client.SubmitBlock(block, nil) + if err != nil { + t.Fatal(err) + } + gotChainTips, err = r.Client.GetChainTips() + if err != nil { + t.Fatal(err) + } + expectedChainTips = []*btcjson.GetChainTipsResult{ + { + Height: 4, + Hash: getBlockFromString(t, block4Hex).Hash().String(), + BranchLen: 3, + Status: "valid-fork", + }, + { + Height: 5, + Hash: getBlockFromString(t, block5aHex).Hash().String(), + BranchLen: 0, + Status: "active", + }, + { + Height: 4, + Hash: getBlockFromString(t, block4bHex).Hash().String(), + BranchLen: 1, + Status: "valid-fork", + }, + } + + err = compareMultipleChainTips(t, gotChainTips, expectedChainTips) + if err != nil { + t.Fatalf("TestGetChainTips fail. Error: %v", err) + } +} diff --git a/rpcclient/chain.go b/rpcclient/chain.go index a97543fd..b2166599 100644 --- a/rpcclient/chain.go +++ b/rpcclient/chain.go @@ -685,6 +685,44 @@ func (c *Client) GetBlockHeaderVerbose(blockHash *chainhash.Hash) (*btcjson.GetB return c.GetBlockHeaderVerboseAsync(blockHash).Receive() } +// FutureGetChainTipsResult is a future promise to deliver the result of a +// GetChainTips RPC invocation (or an applicable error). +type FutureGetChainTipsResult chan *Response + +// Receive waits for the Response promised by the future and returns the +// data structure of all the chain tips the node is aware of. +func (r FutureGetChainTipsResult) Receive() ([]*btcjson.GetChainTipsResult, error) { + res, err := ReceiveFuture(r) + if err != nil { + return nil, err + } + + // Unmarshal result as a string. + var chainTips []*btcjson.GetChainTipsResult + err = json.Unmarshal(res, &chainTips) + if err != nil { + return nil, err + } + + return chainTips, nil +} + +// GetChainTipsAsync returns an instance of a type that can be used to get the +// result of the RPC at some future time by invoking the Receive function on the +// returned instance. +// +// See GetChainTips for the blocking version and more details. +func (c *Client) GetChainTipsAsync() FutureGetChainTipsResult { + cmd := btcjson.NewGetChainTipsCmd() + return c.SendCmd(cmd) +} + +// GetChainTips returns a slice of data structure with information about all the +// current chain tips that this node is aware of. +func (c *Client) GetChainTips() ([]*btcjson.GetChainTipsResult, error) { + return c.GetChainTipsAsync().Receive() +} + // FutureGetMempoolEntryResult is a future promise to deliver the result of a // GetMempoolEntryAsync RPC invocation (or an applicable error). type FutureGetMempoolEntryResult chan *Response