Merge pull request #2197 from Crypt-iQ/2183_eugene

main, rpcclient, integration: add rpccalls for invalidate and reconsiderblock
This commit is contained in:
Olaoluwa Osuntokun 2024-06-20 15:28:08 -07:00 committed by GitHub
commit 93e729194e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 327 additions and 2 deletions

View file

@ -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)
}
}

View file

@ -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()
}

View file

@ -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.

View file

@ -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,