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

main, rpcclient, integration: add rpccalls for invalidate and reconsiderblock #2197

Merged
merged 2 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 244 additions & 0 deletions integration/invalidate_reconsider_block_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
35 changes: 35 additions & 0 deletions rpcclient/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
40 changes: 38 additions & 2 deletions rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -241,9 +243,7 @@ var rpcUnimplemented = map[string]struct{}{
"getmempoolentry": {},
"getnetworkinfo": {},
"getwork": {},
"invalidateblock": {},
"preciousblock": {},
"reconsiderblock": {},
}

// Commands that are available to a limited user
Expand Down Expand Up @@ -284,6 +284,8 @@ var rpcLimited = map[string]struct{}{
"getrawmempool": {},
"getrawtransaction": {},
"gettxout": {},
"invalidateblock": {},
"reconsiderblock": {},
"searchrawtransactions": {},
"sendrawtransaction": {},
"submitblock": {},
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions rpcserverhelp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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" +
Expand Down Expand Up @@ -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,
Expand Down
Loading