diff --git a/integration/invalidate_reconsider_block_test.go b/integration/invalidate_reconsider_block_test.go new file mode 100644 index 0000000000..4fe6ff0012 --- /dev/null +++ b/integration/invalidate_reconsider_block_test.go @@ -0,0 +1,244 @@ +package integration + +import ( + "testing" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/integration/rpctest" +) + +func TestInvalidateAndReconsiderBlock(t *testing.T) { + // Set up regtest chain. + r, err := rpctest.New(&chaincfg.RegressionNetParams, nil, nil, "") + if err != nil { + t.Fatalf("TestInvalidateAndReconsiderBlock fail."+ + "Unable to create primary harness: %v", err) + } + if err := r.SetUp(true, 0); err != nil { + t.Fatalf("TestInvalidateAndReconsiderBlock fail. "+ + "Unable to setup test chain: %v", err) + } + defer r.TearDown() + + // Generate 4 blocks. + // + // Our chain view looks like so: + // (genesis block) -> 1 -> 2 -> 3 -> 4 + _, err = r.Client.Generate(4) + if err != nil { + t.Fatal(err) + } + + // Cache the active tip hash. + block4ActiveTipHash, err := r.Client.GetBestBlockHash() + if err != nil { + t.Fatal(err) + } + + // Cache block 1 hash as this will be our chaintip after we invalidate block 2. + block1Hash, err := r.Client.GetBlockHash(1) + if err != nil { + t.Fatal(err) + } + + // Invalidate block 2. + // + // Our chain view looks like so: + // (genesis block) -> 1 (active) + // \ -> 2 -> 3 -> 4 (invalid) + block2Hash, err := r.Client.GetBlockHash(2) + if err != nil { + t.Fatal(err) + } + err = r.Client.InvalidateBlock(block2Hash) + if err != nil { + t.Fatal(err) + } + + // Assert that block 1 is the active chaintip. + bestHash, err := r.Client.GetBestBlockHash() + if *bestHash != *block1Hash { + t.Fatalf("TestInvalidateAndReconsiderBlock fail. Expected the "+ + "best block hash to be block 1 with hash %s but got %s", + block1Hash.String(), bestHash.String()) + } + + // Generate 2 blocks. + // + // Our chain view looks like so: + // (genesis block) -> 1 -> 2a -> 3a (active) + // \ -> 2 -> 3 -> 4 (invalid) + _, err = r.Client.Generate(2) + if err != nil { + t.Fatal(err) + } + + // Cache the active tip hash for the current active tip. + block3aActiveTipHash, err := r.Client.GetBestBlockHash() + if err != nil { + t.Fatal(err) + } + + tips, err := r.Client.GetChainTips() + if err != nil { + t.Fatal(err) + } + + // Assert that there are two branches. + if len(tips) != 2 { + t.Fatalf("TestInvalidateAndReconsiderBlock fail. "+ + "Expected 2 chaintips but got %d", len(tips)) + } + + for _, tip := range tips { + if tip.Hash == block4ActiveTipHash.String() && + tip.Status != "invalid" { + t.Fatalf("TestInvalidateAndReconsiderBlock fail. Expected "+ + "invalidated branch tip of %s to be invalid but got %s", + tip.Hash, tip.Status) + } + } + + // Reconsider the invalidated block 2. + // + // Our chain view looks like so: + // (genesis block) -> 1 -> 2a -> 3a (valid-fork) + // \ -> 2 -> 3 -> 4 (active) + err = r.Client.ReconsiderBlock(block2Hash) + if err != nil { + t.Fatal(err) + } + + tips, err = r.Client.GetChainTips() + if err != nil { + t.Fatal(err) + } + // Assert that there are two branches. + if len(tips) != 2 { + t.Fatalf("TestInvalidateAndReconsiderBlock fail. "+ + "Expected 2 chaintips but got %d", len(tips)) + } + + var checkedTips int + for _, tip := range tips { + if tip.Hash == block4ActiveTipHash.String() { + if tip.Status != "active" { + t.Fatalf("TestInvalidateAndReconsiderBlock fail. Expected "+ + "the reconsidered branch tip of %s to be active but got %s", + tip.Hash, tip.Status) + } + + checkedTips++ + } + + if tip.Hash == block3aActiveTipHash.String() { + if tip.Status != "valid-fork" { + t.Fatalf("TestInvalidateAndReconsiderBlock fail. Expected "+ + "invalidated branch tip of %s to be valid-fork but got %s", + tip.Hash, tip.Status) + } + checkedTips++ + } + } + + if checkedTips != 2 { + t.Fatalf("TestInvalidateAndReconsiderBlock fail. "+ + "Expected to check %d chaintips, checked %d", 2, checkedTips) + } + + // Invalidate block 3a. + // + // Our chain view looks like so: + // (genesis block) -> 1 -> 2a -> 3a (invalid) + // \ -> 2 -> 3 -> 4 (active) + err = r.Client.InvalidateBlock(block3aActiveTipHash) + if err != nil { + t.Fatal(err) + } + + tips, err = r.Client.GetChainTips() + if err != nil { + t.Fatal(err) + } + + // Assert that there are two branches. + if len(tips) != 2 { + t.Fatalf("TestInvalidateAndReconsiderBlock fail. "+ + "Expected 2 chaintips but got %d", len(tips)) + } + + checkedTips = 0 + for _, tip := range tips { + if tip.Hash == block4ActiveTipHash.String() { + if tip.Status != "active" { + t.Fatalf("TestInvalidateAndReconsiderBlock fail. Expected "+ + "an active branch tip of %s but got %s", + tip.Hash, tip.Status) + } + + checkedTips++ + } + + if tip.Hash == block3aActiveTipHash.String() { + if tip.Status != "invalid" { + t.Fatalf("TestInvalidateAndReconsiderBlock fail. Expected "+ + "the invalidated tip of %s to be invalid but got %s", + tip.Hash, tip.Status) + } + checkedTips++ + } + } + + if checkedTips != 2 { + t.Fatalf("TestInvalidateAndReconsiderBlock fail. "+ + "Expected to check %d chaintips, checked %d", 2, checkedTips) + } + + // Reconsider block 3a. + // + // Our chain view looks like so: + // (genesis block) -> 1 -> 2a -> 3a (valid-fork) + // \ -> 2 -> 3 -> 4 (active) + err = r.Client.ReconsiderBlock(block3aActiveTipHash) + if err != nil { + t.Fatal(err) + } + + tips, err = r.Client.GetChainTips() + if err != nil { + t.Fatal(err) + } + + // Assert that there are two branches. + if len(tips) != 2 { + t.Fatalf("TestInvalidateAndReconsiderBlock fail. "+ + "Expected 2 chaintips but got %d", len(tips)) + } + + checkedTips = 0 + for _, tip := range tips { + if tip.Hash == block4ActiveTipHash.String() { + if tip.Status != "active" { + t.Fatalf("TestInvalidateAndReconsiderBlock fail. Expected "+ + "an active branch tip of %s but got %s", + tip.Hash, tip.Status) + } + + checkedTips++ + } + + if tip.Hash == block3aActiveTipHash.String() { + if tip.Status != "valid-fork" { + t.Fatalf("TestInvalidateAndReconsiderBlock fail. Expected "+ + "the reconsidered tip of %s to be a valid-fork but got %s", + tip.Hash, tip.Status) + } + checkedTips++ + } + } + + if checkedTips != 2 { + t.Fatalf("TestInvalidateAndReconsiderBlock fail. "+ + "Expected to check %d chaintips, checked %d", 2, checkedTips) + } +} diff --git a/rpcclient/chain.go b/rpcclient/chain.go index 1d0a8be2a9..c8562b8e65 100644 --- a/rpcclient/chain.go +++ b/rpcclient/chain.go @@ -1419,3 +1419,38 @@ func (c *Client) GetDescriptorInfoAsync(descriptor string) FutureGetDescriptorIn func (c *Client) GetDescriptorInfo(descriptor string) (*btcjson.GetDescriptorInfoResult, error) { return c.GetDescriptorInfoAsync(descriptor).Receive() } + +// FutureReconsiderBlockResult is a future promise to deliver the result of a +// ReconsiderBlockAsync RPC invocation (or an applicable error). +type FutureReconsiderBlockResult chan *Response + +// Receive waits for the Response promised by the future and returns the raw +// block requested from the server given its hash. +func (r FutureReconsiderBlockResult) Receive() error { + _, err := ReceiveFuture(r) + return err +} + +// ReconsiderBlockAsync 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 ReconsiderBlock for the blocking version and more details. +func (c *Client) ReconsiderBlockAsync( + blockHash *chainhash.Hash) FutureReconsiderBlockResult { + + hash := "" + if blockHash != nil { + hash = blockHash.String() + } + + cmd := btcjson.NewReconsiderBlockCmd(hash) + return c.SendCmd(cmd) +} + +// ReconsiderBlock reconsiders an verifies a specific block and the branch that +// the block is included in. If the block is valid on reconsideration, the chain +// will reorg to that block if it has more PoW than the current tip. +func (c *Client) ReconsiderBlock(blockHash *chainhash.Hash) error { + return c.ReconsiderBlockAsync(blockHash).Receive() +} diff --git a/rpcserver.go b/rpcserver.go index f82b95ca63..a3c4062bcc 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -170,8 +170,10 @@ var rpcHandlersBeforeInit = map[string]commandHandler{ "getrawtransaction": handleGetRawTransaction, "gettxout": handleGetTxOut, "help": handleHelp, + "invalidateblock": handleInvalidateBlock, "node": handleNode, "ping": handlePing, + "reconsiderblock": handleReconsiderBlock, "searchrawtransactions": handleSearchRawTransactions, "sendrawtransaction": handleSendRawTransaction, "setgenerate": handleSetGenerate, @@ -241,9 +243,7 @@ var rpcUnimplemented = map[string]struct{}{ "getmempoolentry": {}, "getnetworkinfo": {}, "getwork": {}, - "invalidateblock": {}, "preciousblock": {}, - "reconsiderblock": {}, } // Commands that are available to a limited user @@ -284,6 +284,8 @@ var rpcLimited = map[string]struct{}{ "getrawmempool": {}, "getrawtransaction": {}, "gettxout": {}, + "invalidateblock": {}, + "reconsiderblock": {}, "searchrawtransactions": {}, "sendrawtransaction": {}, "submitblock": {}, @@ -2850,6 +2852,23 @@ func handleGetTxOut(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (i return txOutReply, nil } +// handleInvalidateBlock implements the invalidateblock command. +func handleInvalidateBlock(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { + c := cmd.(*btcjson.InvalidateBlockCmd) + + invalidateHash, err := chainhash.NewHashFromStr(c.BlockHash) + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCDeserialization, + Message: fmt.Sprintf("Failed to deserialize blockhash from string of %s", + invalidateHash), + } + } + + err = s.cfg.Chain.InvalidateBlock(invalidateHash) + return nil, err +} + // handleHelp implements the help command. func handleHelp(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.HelpCmd) @@ -3123,6 +3142,23 @@ func fetchMempoolTxnsForAddress(s *rpcServer, addr btcutil.Address, numToSkip, n return mpTxns[numToSkip:rangeEnd], numToSkip } +// handleReconsiderBlock implements the reconsiderblock command. +func handleReconsiderBlock(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { + c := cmd.(*btcjson.ReconsiderBlockCmd) + + reconsiderHash, err := chainhash.NewHashFromStr(c.BlockHash) + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCDeserialization, + Message: fmt.Sprintf("Failed to deserialize blockhash from string of %s", + reconsiderHash), + } + } + + err = s.cfg.Chain.ReconsiderBlock(reconsiderHash) + return nil, err +} + // handleSearchRawTransactions implements the searchrawtransactions command. func handleSearchRawTransactions(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { // Respond with an error if the address index is not enabled. diff --git a/rpcserverhelp.go b/rpcserverhelp.go index 1f8451a530..71f96e99fd 100644 --- a/rpcserverhelp.go +++ b/rpcserverhelp.go @@ -544,6 +544,10 @@ var helpDescsEnUS = map[string]string{ "gettxout-vout": "The index of the output", "gettxout-includemempool": "Include the mempool when true", + // InvalidateBlockCmd help. + "invalidateblock--synopsis": "Invalidates the block of the given block hash. To re-validate the invalidated block, use the reconsiderblock rpc", + "invalidateblock-blockhash": "The block hash of the block to invalidate", + // HelpCmd help. "help--synopsis": "Returns a list of all commands or help for a specified command.", "help-command": "The command to retrieve help for", @@ -681,6 +685,10 @@ var helpDescsEnUS = map[string]string{ "loadtxfilter-addresses": "Array of addresses to add to the transaction filter", "loadtxfilter-outpoints": "Array of outpoints to add to the transaction filter", + // ReconsiderBlockCmd help. + "reconsiderblock--synopsis": "Reconsiders the block of the given block hash. Can be used to re-validate blocks invalidated with invalidateblock", + "reconsiderblock-blockhash": "The block hash of the block to reconsider", + // Rescan help. "rescan--synopsis": "Rescan block chain for transactions to addresses.\n" + "When the endblock parameter is omitted, the rescan continues through the best block in the main chain.\n" + @@ -788,7 +796,9 @@ var rpcResultTypes = map[string][]interface{}{ "gettxout": {(*btcjson.GetTxOutResult)(nil)}, "node": nil, "help": {(*string)(nil), (*string)(nil)}, + "invalidateblock": nil, "ping": nil, + "reconsiderblock": nil, "searchrawtransactions": {(*string)(nil), (*[]btcjson.SearchRawTransactionsResult)(nil)}, "sendrawtransaction": {(*string)(nil)}, "setgenerate": nil,