From fd92d7c85a11479ec1ee3f97beb81aadfda34976 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Tue, 29 Dec 2020 20:40:45 -0600 Subject: [PATCH 1/4] blockchain: Add invalidate/reconsider infrastruct. This adds infrastructure to support invalidating arbitrary blocks as if they had violated a consensus rule as well as reconsidering arbitrary blocks for validation under the current consensus rules. It also includes comprehensive tests to ensure proper functionality. Example use cases this enables: - Revalidating blocks under new consensus rules - Consider the case of old software rejecting blocks due to new consensus rules activating on the network and then upgrading to a new version that supports the new rules - Possibility of manually generating snapshots of historical state such as live tickets and available utxos - Manually recovering from unexpected circumstances - Easier chain reorganization testing --- blockchain/common_test.go | 47 +++ blockchain/error.go | 4 + blockchain/error_test.go | 1 + blockchain/process.go | 263 ++++++++++++++ blockchain/process_test.go | 705 +++++++++++++++++++++++++++++++++++++ 5 files changed, 1020 insertions(+) diff --git a/blockchain/common_test.go b/blockchain/common_test.go index 60d2595c56..7c323dcc97 100644 --- a/blockchain/common_test.go +++ b/blockchain/common_test.go @@ -948,6 +948,53 @@ func (g *chaingenHarness) ForceTipReorg(fromTipName, toTipName string) { } } +// InvalidateBlockAndExpectTip marks the block associated with the given name in +// the harness generator as invalid and expects the provided error along with +// the resulting current best chain tip to be the block associated with the +// given tip name. +func (g *chaingenHarness) InvalidateBlockAndExpectTip(blockName string, wantErr error, tipName string) { + g.t.Helper() + + msgBlock := g.BlockByName(blockName) + blockHeight := msgBlock.Header.Height + block := dcrutil.NewBlock(msgBlock) + g.t.Logf("Testing invalidate block %q (hash %s, height %d) with expected "+ + "error %v", blockName, block.Hash(), blockHeight, wantErr) + + err := g.chain.InvalidateBlock(block.Hash()) + if !errors.Is(err, wantErr) { + g.t.Fatalf("invalidate block %q (hash %s, height %d) does not have "+ + "expected error -- got %q, want %v", blockName, block.Hash(), + blockHeight, err, wantErr) + } + + g.ExpectTip(tipName) +} + +// ReconsiderBlockAndExpectTip reconsiders the block associated with the given +// name in the harness generator and expects the provided error along with the +// resulting current best chain tip to be the block associated with the given +// tip name. +func (g *chaingenHarness) ReconsiderBlockAndExpectTip(blockName string, wantErr error, tipName string) { + g.t.Helper() + + msgBlock := g.BlockByName(blockName) + blockHeight := msgBlock.Header.Height + block := dcrutil.NewBlock(msgBlock) + + g.t.Logf("Testing reconsider block %q (hash %s, height %d) with expected "+ + "error %v", blockName, block.Hash(), blockHeight, wantErr) + + err := g.chain.ReconsiderBlock((block.Hash())) + if !errors.Is(err, wantErr) { + g.t.Fatalf("reconsider block %q (hash %s, height %d) does not have "+ + "expected error -- got %q, want %v", blockName, block.Hash(), + blockHeight, err, wantErr) + } + + g.ExpectTip(tipName) +} + // minUint32 is a helper function to return the minimum of two uint32s. // This avoids a math import and the need to cast to floats. func minUint32(a, b uint32) uint32 { diff --git a/blockchain/error.go b/blockchain/error.go index ead3ff4f6a..6420f4234a 100644 --- a/blockchain/error.go +++ b/blockchain/error.go @@ -599,6 +599,10 @@ const ( // ErrNoTreasuryBalance indicates the treasury balance for a given block // hash does not exist. ErrNoTreasuryBalance = ErrorKind("ErrNoTreasuryBalance") + + // ErrInvalidateGenesisBlock indicates an attempt to invalidate the genesis + // block which is not allowed. + ErrInvalidateGenesisBlock = ErrorKind("ErrInvalidateGenesisBlock") ) // Error satisfies the error interface and prints human-readable errors. diff --git a/blockchain/error_test.go b/blockchain/error_test.go index f2dbb4733f..96c0cab743 100644 --- a/blockchain/error_test.go +++ b/blockchain/error_test.go @@ -155,6 +155,7 @@ func TestErrorKindStringer(t *testing.T) { {ErrUnknownBlock, "ErrUnknownBlock"}, {ErrNoFilter, "ErrNoFilter"}, {ErrNoTreasuryBalance, "ErrNoTreasuryBalance"}, + {ErrInvalidateGenesisBlock, "ErrInvalidateGenesisBlock"}, } t.Logf("Running %d tests", len(tests)) diff --git a/blockchain/process.go b/blockchain/process.go index 7f958b688b..68f524212b 100644 --- a/blockchain/process.go +++ b/blockchain/process.go @@ -10,6 +10,7 @@ import ( "fmt" "github.com/decred/dcrd/blockchain/stake/v4" + "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/database/v2" "github.com/decred/dcrd/dcrutil/v4" "github.com/decred/dcrd/wire" @@ -582,3 +583,265 @@ func (b *BlockChain) ProcessBlock(block *dcrutil.Block, flags BehaviorFlags) (in } return forkLen, finalErr } + +// InvalidateBlock manually invalidates the provided block as if the block had +// violated a consensus rule and marks all of its descendants as having a known +// invalid ancestor. It then reorganizes the chain as necessary so the branch +// with the most cumulative proof of work that is still valid becomes the main +// chain. +func (b *BlockChain) InvalidateBlock(hash *chainhash.Hash) error { + b.processLock.Lock() + defer b.processLock.Unlock() + + // Unable to invalidate a block that does not exist. + node := b.index.LookupNode(hash) + if node == nil { + return unknownBlockError(hash) + } + + // Disallow invalidation of the genesis block. + if node.height == 0 { + str := "invalidating the genesis block is not allowed" + return contextError(ErrInvalidateGenesisBlock, str) + } + + // Nothing to do if the block is already known to have failed validation. + // Notice that this is intentionally not considering the case when the block + // is marked invalid due to having a known invalid ancestor so the block is + // still manually marked as having failed validation in that case. + if b.index.NodeStatus(node).KnownValidateFailed() { + return nil + } + + // Simply mark the block being invalidated as having failed validation and + // all of its descendants as having an invalid ancestor when it is not part + // of the current best chain. + b.recentContextChecks.Delete(node.hash) + if !b.bestChain.Contains(node) { + b.index.MarkBlockFailedValidation(node) + b.chainLock.Lock() + b.flushBlockIndexWarnOnly() + b.chainLock.Unlock() + return nil + } + + log.Infof("Rolling the chain back to block %s (height %d) due to manual "+ + "invalidation", node.parent.hash, node.parent.height) + + // At this point, the invalidated block is part of the current best chain, + // so start by reorganizing the chain back to its parent and marking it as + // having failed validation along with all of its descendants as having an + // invalid ancestor. + b.chainLock.Lock() + if err := b.reorganizeChain(node.parent); err != nil { + b.flushBlockIndexWarnOnly() + b.chainLock.Unlock() + return err + } + b.index.MarkBlockFailedValidation(node) + + // Reset whether or not the chain believes it is current since the best + // chain was just invalidated. + newTip := b.bestChain.Tip() + b.isCurrentLatch = false + b.maybeUpdateIsCurrent(newTip) + b.chainLock.Unlock() + + // The new best chain tip is probably no longer in the best chain candidates + // since it was likely removed due to previously having less work, so scour + // the block tree in order repopulate the best chain candidates. + b.index.Lock() + b.index.addBestChainCandidate(newTip) + b.index.forEachChainTip(func(tip *blockNode) error { + // Chain tips that have less work than the new tip are not best chain + // candidates nor are any of their ancestors since they have even less + // work. + if tip.workSum.Cmp(newTip.workSum) < 0 { + return nil + } + + // Find the first ancestor of the tip that is not known to be invalid + // and can be validated. Then add it as a candidate to potentially + // become the best chain tip if it has the same or more work than the + // current one. + n := tip + for n != nil && (n.status.KnownInvalid() || !b.index.canValidate(n)) { + n = n.parent + } + if n != nil && n != newTip && n.workSum.Cmp(newTip.workSum) >= 0 { + b.index.addBestChainCandidate(n) + } + + return nil + }) + b.index.Unlock() + + // Find the current best chain candidate and attempt to reorganize the chain + // to it. The most common case is for the candidate to extend the current + // best chain, however, it might also be a candidate that would cause a + // reorg or be the current main chain tip, which will be the case when the + // passed block is on a side chain. + b.chainLock.Lock() + targetTip := b.index.FindBestChainCandidate() + err := b.reorganizeChain(targetTip) + b.flushBlockIndexWarnOnly() + b.chainLock.Unlock() + return err +} + +// blockNodeInSlice return whether a given block node is an element in a slice +// of them. +func blockNodeInSlice(node *blockNode, slice []*blockNode) bool { + for _, child := range slice { + if child == node { + return true + } + } + return false +} + +// ReconsiderBlock removes the known invalid status of the provided block and +// all of its ancestors along with the known invalid ancestor status from all of +// its descendants that are neither themselves marked as having failed +// validation nor descendants of another such block. Therefore, it allows the +// affected blocks to be reconsidered under the current consensus rules. It +// then potentially reorganizes the chain as necessary so the block with the +// most cumulative proof of work that is valid becomes the tip of the main +// chain. +func (b *BlockChain) ReconsiderBlock(hash *chainhash.Hash) error { + b.processLock.Lock() + defer b.processLock.Unlock() + + // Unable to reconsider a block that does not exist. + node := b.index.LookupNode(hash) + if node == nil { + return unknownBlockError(hash) + } + + log.Infof("Reconsidering block %s (height %d)", node.hash, node.height) + + // Remove invalidity flags from the block to be reconsidered and all of its + // ancestors while tracking the earliest such block that is marked as having + // failed validation since all descendants of that block need to have their + // invalid ancestor flag removed. + // + // Also, add any that are eligible for validation as candidates to + // potentially become the best chain when they have the same or more work + // than the current best chain tip and remove any cached validation-related + // state for them to ensure they undergo full revalidation should it be + // necessary. + // + // Finally, add any that are not already fully linked and have their data + // available to the map of unlinked blocks that are eligible for connection + // when they are not already present. + curBestTip := b.bestChain.Tip() + vfNode := node + b.index.Lock() + for n := node; n != nil && n.height > 0; n = n.parent { + if n.status.KnownInvalid() { + if n.status.KnownValidateFailed() { + vfNode = n + } + b.index.unsetStatusFlags(n, statusValidateFailed|statusInvalidAncestor) + b.recentContextChecks.Delete(n.hash) + } + + if b.index.canValidate(n) && n.workSum.Cmp(curBestTip.workSum) >= 0 { + b.index.addBestChainCandidate(n) + } + + if !n.isFullyLinked && n.status.HaveData() && n.parent != nil { + unlinked := b.index.unlinkedChildrenOf[n.parent] + if !blockNodeInSlice(n, unlinked) { + b.index.unlinkedChildrenOf[n.parent] = append(unlinked, n) + } + } + } + + // Remove the known invalid ancestor flag from all blocks that descend from + // the earliest failed block to be reconsidered that are neither themselves + // marked as having failed validation nor descendants of another such block. + // + // Also, add any that are eligible for validation as candidates to + // potentially become the best chain when they have the same or more work + // than the current best chain tip and remove any cached validation-related + // state for them to ensure they undergo full revalidation should it be + // necessary. + // + // Finally, add any that are not already fully linked and have their data + // available to the map of unlinked blocks that are eligible for connection + // when they are not already present. + // + // Chain tips at the same or lower heights than the earliest failed block to + // be reconsidered can't possibly be descendants of it, so use it as the + // lower height bound filter when iterating chain tips. + b.index.forEachChainTipAfterHeight(vfNode, func(tip *blockNode) error { + // Nothing to do if the earliest failed block to be reconsidered is not + // an ancestor of this chain tip. + if tip.Ancestor(vfNode.height) != vfNode { + return nil + } + + // Find the final descendant that is not known to descend from another + // one that failed validation since all descendants after that point + // need to retain their known invalid ancestor status. + finalNotKnownInvalidDescendant := tip + for n := tip; n != vfNode; n = n.parent { + if n.status.KnownValidateFailed() { + finalNotKnownInvalidDescendant = n.parent + } + } + + for n := finalNotKnownInvalidDescendant; n != vfNode; n = n.parent { + b.index.unsetStatusFlags(n, statusInvalidAncestor) + b.recentContextChecks.Delete(n.hash) + if b.index.canValidate(n) && n.workSum.Cmp(curBestTip.workSum) >= 0 { + b.index.addBestChainCandidate(n) + } + + if !n.isFullyLinked && n.status.HaveData() && n.parent != nil { + unlinked := b.index.unlinkedChildrenOf[n.parent] + if !blockNodeInSlice(n, unlinked) { + b.index.unlinkedChildrenOf[n.parent] = append(unlinked, n) + } + } + } + + return nil + }) + + // Update the best known invalid block (as determined by having the most + // cumulative work) and best header that is not known to be invalid as + // needed. + // + // Note this is separate from the above iteration because all tips must be + // considered as opposed to just those that are possible descendants of the + // node being reconsidered. + b.index.bestInvalid = nil + b.index.forEachChainTip(func(tip *blockNode) error { + if tip.status.KnownInvalid() { + b.index.maybeUpdateBestInvalid(tip) + } + b.index.maybeUpdateBestHeaderForTip(tip) + return nil + }) + b.index.Unlock() + + // Reset whether or not the chain believes it is current, find the best + // chain candidate, and attempt to reorganize the chain to it. + b.chainLock.Lock() + b.isCurrentLatch = false + targetTip := b.index.FindBestChainCandidate() + err := b.reorganizeChain(targetTip) + b.flushBlockIndexWarnOnly() + b.chainLock.Unlock() + + // Force pruning of the cached chain tips since it's fairly likely the best + // tip has experienced a sudden change and is higher given how this function + // is typically used and the logic which only periodically prunes tips is + // optimized for steady state operation. + b.index.Lock() + b.index.pruneCachedTips(b.bestChain.Tip()) + b.index.Unlock() + return err +} diff --git a/blockchain/process_test.go b/blockchain/process_test.go index 5bb5978a97..836ada733d 100644 --- a/blockchain/process_test.go +++ b/blockchain/process_test.go @@ -1172,3 +1172,708 @@ func TestProcessLogic(t *testing.T) { g.AcceptBlockDataWithExpectedTip("b11", "b11") g.ExpectIsCurrent(true) } + +// TestInvalidateReconsider ensures that manually invalidating blocks and +// reconsidering them works as expected under a wide variety of scenarios. +func TestInvalidateReconsider(t *testing.T) { + // Generate or reuse a shared chain generator with a set of blocks that form + // a fairly complex overall block tree including multiple forks such that + // some branches are valid and others contain invalid headers and/or blocks + // with multiple valid descendants as well as further forks at various + // heights from those invalid branches. + sharedGen, err := genSharedProcessTestBlocks(t) + if err != nil { + t.Fatalf("Failed to create generator: %v", err) + } + + // Create a new database and chain instance to run tests against. + g, teardownFunc := newChaingenHarnessWithGen(t, "invalidatetest", sharedGen) + defer teardownFunc() + + // Shorter versions of useful params for convenience. + params := g.Params() + coinbaseMaturity := params.CoinbaseMaturity + stakeEnabledHeight := params.StakeEnabledHeight + stakeValidationHeight := params.StakeValidationHeight + + // ------------------------------------------------------------------------- + // Accept all headers in the initial test chain through stake validation + // height and the base maturity blocks. + // + // genesis -> bfb -> bm0 -> ... -> bm# -> bse0 -> ... -> bse# -> ... + // + // ... bsv0 -> ... -> bsv# -> bbm0 -> ... -> bbm# + // ------------------------------------------------------------------------- + + g.AcceptHeader("bfb") + for i := uint16(0); i < coinbaseMaturity; i++ { + blockName := fmt.Sprintf("bm%d", i) + g.AcceptHeader(blockName) + } + tipHeight := int64(coinbaseMaturity) + 1 + for i := int64(0); tipHeight < stakeEnabledHeight; i++ { + blockName := fmt.Sprintf("bse%d", i) + g.AcceptHeader(blockName) + tipHeight++ + } + for i := int64(0); tipHeight < stakeValidationHeight; i++ { + blockName := fmt.Sprintf("bsv%d", i) + g.AcceptHeader(blockName) + tipHeight++ + } + for i := uint16(0); i < coinbaseMaturity; i++ { + blockName := fmt.Sprintf("bbm%d", i) + g.AcceptHeader(blockName) + } + + // ------------------------------------------------------------------------- + // Initial setup of headers and block data needed for the invalidate and + // reconsider tests below. + // ------------------------------------------------------------------------- + + // Accept block headers for the following tree structure: + // + // Note that the ! below indicates a block that is invalid due to violating + // a consensus rule during the contextual checks prior to connection and the + // @ indicates a block that is invalid due to violating a consensus rule + // during block connection. + // + // ... -> b1 -> b2 -> ... + // + // ... -> b3 -> b4 -> b5 -> b6 -> b7 -> b8 -> b9 -> b10 -> b11 + // \-> b4b -> b5b! -> b6b -> b7b -> b8b \-> b10a + // \-> b4c | \-> b8c -> b9c + // | \-> b7d -> b8d + // | \-> b7e -> b8e + // \-> b4f -> b5f -> b6f -> b7f -> b8f -> b9f -> b10f + // \-> b4i -> b5i -> b6i@ + // + g.AcceptHeader("b1") + g.AcceptHeader("b2") + g.AcceptHeader("b3") + g.AcceptHeader("b4") + g.AcceptHeader("b4b") + g.AcceptHeader("b4c") + g.AcceptHeader("b4f") + g.AcceptHeader("b4i") + g.AcceptHeader("b5") + g.AcceptHeader("b5b") // Invalid block, but header valid. + g.AcceptHeader("b5f") + g.AcceptHeader("b5i") + g.AcceptHeader("b6") + g.AcceptHeader("b6b") + g.AcceptHeader("b6f") + g.AcceptHeader("b6i") + g.AcceptHeader("b7") + g.AcceptHeader("b7b") + g.AcceptHeader("b7d") + g.AcceptHeader("b7e") + g.AcceptHeader("b7f") + g.AcceptHeader("b8") + g.AcceptHeader("b8b") + g.AcceptHeader("b8c") + g.AcceptHeader("b8d") + g.AcceptHeader("b8e") + g.AcceptHeader("b8f") + g.AcceptHeader("b9") + g.AcceptHeader("b9c") + g.AcceptHeader("b9f") + g.AcceptHeader("b10") + g.AcceptHeader("b10a") + g.AcceptHeader("b10f") + g.AcceptHeader("b11") + g.ExpectBestHeader("b11") + + // Accept all of the block data in the test chain through stake validation + // height and the base maturity blocks to reach the point where the more + // complicated branching structure starts. + // + // ... -> bfb -> bm0 -> ... -> bm# -> bse0 -> ... -> bse# -> ... + // + // ... bsv0 -> ... -> bsv# -> bbm0 -> ... -> bbm# + g.AcceptBlock("bfb") + for i := uint16(0); i < coinbaseMaturity; i++ { + blockName := fmt.Sprintf("bm%d", i) + g.AcceptBlock(blockName) + } + tipHeight = int64(coinbaseMaturity) + 1 + for i := int64(0); tipHeight < stakeEnabledHeight; i++ { + blockName := fmt.Sprintf("bse%d", i) + g.AcceptBlock(blockName) + tipHeight++ + } + for i := int64(0); tipHeight < stakeValidationHeight; i++ { + blockName := fmt.Sprintf("bsv%d", i) + g.AcceptBlock(blockName) + tipHeight++ + } + for i := uint16(0); i < coinbaseMaturity; i++ { + blockName := fmt.Sprintf("bbm%d", i) + g.AcceptBlock(blockName) + } + + // Accept the block data for several blocks in the main branch of the test + // data. + // + // ... -> b1 -> b2 -> b3 -> b4 -> b5 -> b6 -> b7 + g.AcceptBlock("b1") + g.AcceptBlock("b2") + g.AcceptBlock("b3") + g.AcceptBlock("b4") + g.AcceptBlock("b5") + g.AcceptBlock("b6") + g.AcceptBlock("b7") + + // Accept the block data for several blocks in a branch of the test data + // that contains a bad block such that the invalid branch has more work. + // Notice that the bad block is only processed AFTER all of the data for its + // descendants is added since they would otherwise be rejected due to being + // part of a known invalid branch. + // + // ... -> b1 -> b2 -> b3 -> b4 -> b5 -> b6 -> b7 + // \-> b4b -> b5b -> b6b -> b7b -> b8b + // --- + // ^ (invalid block) + g.AcceptBlockData("b4b") + g.AcceptBlockData("b6b") + g.AcceptBlockData("b7b") + g.AcceptBlockData("b8b") + g.RejectBlock("b5b", ErrTicketUnavailable) + g.ExpectTip("b7") + g.ExpectBestInvalidHeader("b9c") + g.ExpectIsCurrent(false) + + // ------------------------------------------------------------------------- + // Invalidating and reconsidering an unknown block must error. + // ------------------------------------------------------------------------- + + g.InvalidateBlockAndExpectTip("b1bad", ErrUnknownBlock, "b7") + g.ReconsiderBlockAndExpectTip("b1bad", ErrUnknownBlock, "b7") + + // ------------------------------------------------------------------------- + // The genesis block is not allowed to be invalidated, but it can be + // reconsidered. + // ------------------------------------------------------------------------- + + g.InvalidateBlockAndExpectTip("genesis", ErrInvalidateGenesisBlock, "b7") + g.ReconsiderBlockAndExpectTip("genesis", nil, "b7") + g.ExpectBestHeader("b11") + g.ExpectBestInvalidHeader("b9c") + g.ExpectIsCurrent(false) + + // ------------------------------------------------------------------------- + // Invalidating a block that is already known to have failed validation has + // no effect. + // ------------------------------------------------------------------------- + + g.InvalidateBlockAndExpectTip("b5b", nil, "b7") + g.ExpectBestHeader("b11") + g.ExpectBestInvalidHeader("b9c") + g.ExpectIsCurrent(false) + + // ------------------------------------------------------------------------- + // Reconsider a block that is actually invalid when that block is located in + // a side chain that has more work than the current best chain. Since the + // block is actually invalid, that side chain should remain invalid and + // therefore NOT become the best chain even though it has more work. + // + // Note that the * indicates blocks that are marked as failed validation and + // the # indicates blocks that are marked as having an invalid ancestor. + // + // Before reconsider: + // + // ... -> b1 -> b2 -> b3 -> b4 -> b5 -> b6 -> b7 + // | -- + // | ^ (best chain tip) + // \-> b4b -> b5b* -> b6b# -> b7b# -> b8b# + // --- + // ^ (invalid block, reconsider) + // + // After reconsider: + // + // ... -> b1 -> b2 -> b3 -> b4 -> b5 -> b6 -> b7 + // | -- + // | ^ (best chain tip) + // \-> b4b -> b5b* -> b6b# -> b7b# -> b8b# + // --- + // ^ (still invalid block) + // ------------------------------------------------------------------------- + + g.ReconsiderBlockAndExpectTip("b5b", ErrTicketUnavailable, "b7") + g.ExpectBestHeader("b11") + g.ExpectBestInvalidHeader("b9c") + g.ExpectIsCurrent(false) + + // ------------------------------------------------------------------------- + // Reconsider a block that is a descendant of a block that is actually + // invalid when that block is located in a side chain that has more work + // than the current best chain. Since an ancestor block is actually + // invalid, that side chain should remain invalid and therefore NOT become + // the best chain even though it has more work. + // + // Note that the * indicates blocks that are marked as failed validation and + // the # indicates blocks that are marked as having an invalid ancestor. + // + // Before reconsider: + // + // ... -> b1 -> b2 -> b3 -> b4 -> b5 -> b6 -> b7 + // | -- + // | ^ (best chain tip) + // \-> b4b -> b5b* -> b6b# -> b7b# -> b8b# + // --- ---- + // (invalid block) ^ ^ (reconsider) + // + // After reconsider: + // + // ... -> b1 -> b2 -> b3 -> b4 -> b5 -> b6 -> b7 + // | -- + // | ^ (best chain tip) + // \-> b4b -> b5b* -> b6b# -> b7b# -> b8b# + // --- + // ^ (still invalid block) + // ------------------------------------------------------------------------- + + g.ReconsiderBlockAndExpectTip("b6b", ErrTicketUnavailable, "b7") + g.ExpectBestHeader("b11") + g.ExpectBestInvalidHeader("b9c") + g.ExpectIsCurrent(false) + + // ------------------------------------------------------------------------- + // Invalidate a multi-hop descendant of a block that is actually invalid + // when that block is located in a side chain that has more work than the + // current best chain. Then, reconsider a block that is a descendant of + // the block that is actually invalid, but an ancestor of the invalidated + // multi-hop descendant. + // + // Since the part of that side chain that is then potentially valid has less + // work than the current best chain, no attempt to reorg to that side chain + // should be made. Therefore, no error is expected and the actually invalid + // block along with its descendants that are also ancestors of the multi-hop + // descendant should no longer be known to be invalid. + // + // Note that the * indicates blocks that are marked as failed validation and + // the # indicates blocks that are marked as having an invalid ancestor. + // + // Before reconsider: + // + // ... -> b1 -> b2 -> b3 -> b4 -> b5 -> b6 -> b7 + // | -- + // | ^ (best chain tip) + // \-> b4b -> b5b* -> b6b# -> b7b* -> b8b# + // --- ---- ---- + // | | ^ (marked invalid) + // (invalid block) ^ ^ (reconsider) + // + // After reconsider: + // + // ... -> b1 -> b2 -> b3 -> b4 -> b5 -> b6 -> b7 + // | -- + // | ^ (best chain tip) + // \-> b4b -> b5b -> b6b -> b7b* -> b8b# + // ----------- ---- + // (no longer known to be invalid) ^ ^ (still marked invalid) + // ------------------------------------------------------------------------- + + g.InvalidateBlockAndExpectTip("b7b", nil, "b7") + g.ExpectBestHeader("b11") + g.ExpectBestInvalidHeader("b9c") + g.ExpectIsCurrent(false) + + g.ReconsiderBlockAndExpectTip("b6b", nil, "b7") + g.ExpectBestHeader("b11") + g.ExpectBestInvalidHeader("b9c") + g.ExpectIsCurrent(false) + + // ------------------------------------------------------------------------- + // Reconsider a descendant of the manually invalidated multi-hop descendant + // from the previous test. Since the part of the side chain that is + // then potentially valid has more work than the current chain, an attempt + // to reorg to that branch should be made and fail due to the actually + // invalid ancestor on that branch. + // + // Note that the * indicates blocks that are marked as failed validation and + // the # indicates blocks that are marked as having an invalid ancestor. + // + // Before reconsider: + // + // ... -> b1 -> b2 -> b3 -> b4 -> b5 -> b6 -> b7 + // | -- + // | ^ (best chain tip) + // \-> b4b -> b5b -> b6b -> b7b* -> b8b# + // --- ---- ---- + // (invalid, but not known as such) ^ ^ ^ (reconsider) + // (marked invalid) + // After reconsider: + // + // ... -> b1 -> b2 -> b3 -> b4 -> b5 -> b6 -> b7 + // | -- + // | ^ (best chain tip) + // \-> b4b -> b5b* -> b6b# -> b7b# -> b8b# + // --- + // ^ (invalid block) + // ------------------------------------------------------------------------- + + g.ReconsiderBlockAndExpectTip("b8b", ErrTicketUnavailable, "b7") + g.ExpectBestHeader("b11") + g.ExpectBestInvalidHeader("b9c") + g.ExpectIsCurrent(false) + + // ------------------------------------------------------------------------- + // Invalidate and reconsider block one. After the invalidate, the genesis + // block should become the best chain tip and the best header not known to + // be invalid should become the best header known to be invalid while the + // former becomes the genesis block header. + // + // Reconsidering the block should return the chain back to the original + // state and notably should NOT have any validation errors because the + // branches with the block that is actually invalid should still be known to + // be invalid despite it being a descendant of the reconsidered block and + // therefore no attempt to reorg should happen. + // + // Note that the * indicates blocks that are marked as failed validation and + // the # indicates blocks that are marked as having an invalid ancestor. + // + // Before invalidate: + // + // genesis -> bfb -> ...-> b3 -> b4 -> b5 -> b6 -> b7 + // | -- + // | ^ (best chain tip) + // \-> b4b -> b5b* -> b6b# -> b7b# -> b8b# + // --- + // ^ (invalid block) + // + // After invalidate: + // + // genesis -> bfb* -> ...-> b3# -> b4# -> b5# -> b6# -> b7# + // ------- | + // ^ (best chain tip) | + // \-> b4b#-> b5b* -> b6b# -> b7b# -> b8b# + // --- + // ^ (invalid block) + // After reconsider: + // + // genesis -> bfb -> ...-> b3 -> b4 -> b5 -> b6 -> b7 + // | -- + // | ^ (best chain tip) + // \-> b4b -> b5b* -> b6b# -> b7b# -> b8b# + // --- + // ^ (invalid block) + // ------------------------------------------------------------------------- + + // Note that the genesis block is too far in the past to be considered + // current. + g.InvalidateBlockAndExpectTip("bfb", nil, "genesis") + g.ExpectBestHeader("genesis") + g.ExpectBestInvalidHeader("b11") + g.ExpectIsCurrent(false) + + g.ReconsiderBlockAndExpectTip("bfb", nil, "b7") + g.ExpectBestHeader("b11") + g.ExpectBestInvalidHeader("b9c") + g.ExpectIsCurrent(false) + + // ------------------------------------------------------------------------- + // Reconsidering a block that is actually invalid upon connection when it is + // in a side chain that has had an ancestor manually invalidated and where + // the parent of that invalid block has more work than the current best + // chain should cause a reorganize to that parent block. + // + // In other words, ancestors of reconsidered blocks, even those that are + // actually invalid, should have their invalidity status cleared and become + // eligible for best chain selection. + // + // Note that the * indicates blocks that are marked as failed validation and + // the # indicates blocks that are marked as having an invalid ancestor. + // + // Before reconsider: + // + // genesis -> bfb -> ...-> b3 -> b4 -> b5* -> b6# -> b7# + // | -- + // | ^ (best chain tip) + // \-> b4i -> b5i* -> b6i* + // --- + // (invalid block, reconsider) ^ + // + // After reconsider: + // + // genesis -> bfb -> ...-> b3 -> b4 -> b5* -> b6# -> b7# + // \-> b4i -> b5i -> b6i* + // --- --- + // (best chain tip) ^ ^ (invalid block) + // ------------------------------------------------------------------------- + + // The data for b6i should be accepted because the best chain tip has more + // work at this point and thus there is no attempt to connect it. + g.AcceptBlockData("b4i") + g.AcceptBlockData("b5i") + g.AcceptBlockData("b6i") + + // Invalidate the ancestor in the side chain first to ensure there is no + // reorg when the main chain is invalidated for the purposes of making it + // have less work when the side chain is reconsidered. + g.InvalidateBlockAndExpectTip("b5i", nil, "b7") + g.InvalidateBlockAndExpectTip("b5", nil, "b4") + g.ExpectBestHeader("b10f") + g.ExpectBestInvalidHeader("b11") + g.ExpectIsCurrent(false) + + g.ReconsiderBlockAndExpectTip("b6i", ErrMissingTxOut, "b5i") + g.ExpectBestHeader("b10f") + g.ExpectBestInvalidHeader("b11") + g.ExpectIsCurrent(false) + + // ------------------------------------------------------------------------- + // Undo the main chain invalidation for tests below. + // ------------------------------------------------------------------------- + + g.ReconsiderBlockAndExpectTip("b5", nil, "b7") + g.ExpectBestHeader("b11") + g.ExpectBestInvalidHeader("b9c") + g.ExpectIsCurrent(false) + + // ------------------------------------------------------------------------- + // Make the primary branch have the most cumulative work and then invalidate + // and reconsider a block that is an ancestor of the current best chain tip, + // the best header that is NOT known to be invalid, the best header that is + // known to be invalid, as well as several other branches. + // + // After the invalidation, the best invalid header should become the one + // that was previously the best NOT known to be invalid and the best chain + // tip and header should coincide which also means the chain should latch to + // current. + // + // After reconsideration, everything should revert to the previous state, + // including the chain believing it is not current again. + // + // Note that the * indicates blocks that are marked as failed validation and + // the # indicates blocks that are marked as having an invalid ancestor. + // + // After invalidate: + // + // v (best chain tip, best header) + // -- + // ... -> b2 -> ... + // (best invalid)v + // ---- + // ... -> b3* -> b4# -> b5# -> b6# -> b7# -> b8# -> b9# -> b10# -> b11# + // \-> b4b# -> b5b* -> b6b# -> b7b# -> b8b# \-> b10a# + // \-> b4c# | \-> b8c# -> b9c# + // | \-> b7d# -> b8d# + // | \-> b7e# -> b8e# + // \-> b4f# -> b5f# -> b6f# -> b7f# -> b8f# -> b9f# -> b10f# + // \-> b4i# -> b5i# -> b6i* + // + // After reconsider: + // + // (best chain tip) v (best header) v + // -- --- + // ... -> b3 -> b4 -> b5 -> b6 -> b7 -> b8 -> b9 -> b10 -> b11 + // \-> b4b -> b5b* -> b6b# -> b7b# -> b8b# \-> b10a + // \-> b4c | \-> b8c# -> b9c# + // | | ---- + // | | ^ (best invalid) + // | \-> b7d# -> b8d# + // | \-> b7e# -> b8e# + // \-> b4f -> b5f -> b6f -> b7f -> b8f -> b9f -> b10f + // \-> b4i -> b5i -> b6i* + // ------------------------------------------------------------------------- + + g.AcceptBlock("b8") + g.AcceptBlock("b9") + g.InvalidateBlockAndExpectTip("b3", nil, "b2") + g.ExpectBestHeader("b2") + g.ExpectBestInvalidHeader("b11") + g.ExpectIsCurrent(true) + + g.ReconsiderBlockAndExpectTip("b3", nil, "b9") + g.ExpectBestHeader("b11") + g.ExpectBestInvalidHeader("b9c") + g.ExpectIsCurrent(false) + + // ------------------------------------------------------------------------- + // Invalidate a block that does not have its data available and then + // reconsider a descendant of it that itself has both ancestors and + // descendants without block data available. Then make the missing data + // available such that there is more cumulative work than the current best + // chain tip and ensure that everything is linked and the branch becomes the + // new best tip. + // + // This ensures that reconsidering blocks properly resurrects tracking of + // blocks that are not fully linked yet for both descendants and ancestors. + // + // Note that the @ below indicates data that is available before the + // invalidate and reconsider and ! indicates data that is made available + // afterwards. + // + // (orig tip) v + // -- + // ... -> b3 -> b4 -> b5 -> b6 -> b7 -> b8 -> b9 + // \-> b4f! -> b5f@ -> b6f! -> b7f@ -> b8f! -> b9f@ -> b10f! + // ---- --- --- ----- + // (added last) ^ (invalidate) ^ ^ (reconsider) ^ (new tip) + // ------------------------------------------------------------------------- + + // Notice that the data for b4f, b6f and, b8f, and b10f are intentionally + // not made available yet. + g.AcceptBlockData("b5f") + g.AcceptBlockData("b7f") + g.AcceptBlockData("b9f") + g.InvalidateBlockAndExpectTip("b6f", nil, "b9") + g.ReconsiderBlockAndExpectTip("b7f", nil, "b9") + g.ExpectIsCurrent(false) + + // Add the missing data to complete the side chain while only adding b4f + // last to ensure it triggers a reorg that consists of all of the blocks + // that were previously missing data, but should now be linked. + g.AcceptBlockData("b6f") + g.AcceptBlockData("b8f") + g.AcceptBlockData("b10f") + g.AcceptBlockData("b4f") + g.ExpectTip("b10f") + g.ExpectBestHeader("b11") + g.ExpectBestInvalidHeader("b9c") + g.ExpectIsCurrent(false) + + // ------------------------------------------------------------------------- + // Reconsider a block that is a multi-hop descendant of an invalidated chain + // and also itself has descendants such that the entire branch has more + // cumulative work than the current best chain. The entire branch, + // including the ancestors of the block, is expected to be reconsidered + // resulting in it becoming the new best chain. + // + // Note that the * indicates blocks that are marked as failed validation and + // the # indicates blocks that are marked as having an invalid ancestor. + // + // Before reconsider: + // + // b3 -> b4 -> b5 -> b6 -> b7 -> b8 -> b9* -> b10# -> b11# + // | -- + // | ^ (best chain tip) + // \-> b4f* -> b5f# -> b6f# -> b7f# -> b8f# -> b9f# -> b10f# + // ---- + // ^ (reconsider) + // + // After reconsider: + // + // b3 -> b4 -> b5 -> b6 -> b7 -> b8 -> b9* -> b10# -> b11# + // \-> b4f -> b5f -> b6f -> b7f -> b8f -> b9f -> b10f + // ---- + // ^ (best chain tip) + // ------------------------------------------------------------------------- + + g.InvalidateBlockAndExpectTip("b4f", nil, "b9") + g.InvalidateBlockAndExpectTip("b9", nil, "b8") + g.ExpectBestHeader("b8") + g.ExpectBestInvalidHeader("b11") + g.ExpectIsCurrent(true) + + g.ReconsiderBlockAndExpectTip("b7f", nil, "b10f") + g.ExpectBestHeader("b10f") + g.ExpectBestInvalidHeader("b11") + g.ExpectIsCurrent(true) + + // ------------------------------------------------------------------------- + // Invalidate and reconsider the current best chain tip. The chain should + // believe it is current both before and after since the best chain tip + // matches the best header in both cases. + // + // Note that the * indicates blocks that are marked as failed validation and + // the # indicates blocks that are marked as having an invalid ancestor. + // + // b3 -> b4 -> b5 -> b6 -> b7 -> b8 -> b9* -> b10# -> b11# + // \-> b4f -> b5f -> b6f -> b7f -> b8f -> b9f -> b10f + // ---- + // (invalidate, reconsider, best chain tip) ^ + // ------------------------------------------------------------------------- + + g.InvalidateBlockAndExpectTip("b10f", nil, "b9f") + g.ExpectBestHeader("b9f") + g.ExpectBestInvalidHeader("b11") + g.ExpectIsCurrent(true) + + g.ReconsiderBlockAndExpectTip("b10f", nil, "b10f") + g.ExpectBestHeader("b10f") + g.ExpectBestInvalidHeader("b11") + g.ExpectIsCurrent(true) + + // ------------------------------------------------------------------------- + // Reconsider a chain tip for a branch that has a previously invalidated + // ancestor and that has more work than the current best tip, but does not + // have all of the block data available. All of the ancestors must no + // longer be marked invalid and any descendants on other branches from any + // of those ancestors that were reconsidered must also have their invalid + // ancestor status removed. However, any such descendants that are marked + // as having failed validation instead must NOT have that status cleared. + // + // Note that the * indicates blocks that are marked as failed validation and + // the # indicates blocks that are marked as having an invalid ancestor. + // + // Before reconsider (b10 and b11 data not available): + // + // (reconsider) v + // --- + // b3 -> b4 -> b5 -> b6 -> b7 -> b8 -> b9* -> b10# -> b11# + // \-> b4b -> b5b* -> b6b# -> b7b# -> b8b# \-> b10a# + // | \-> b8c# -> b9c# + // \-> b4f -> b5f -> b6f -> b7f -> b8f -> b9f -> b10f + // ---- + // (best chain tip) ^ + // + // After reconsider (b10 and b11 data not available): + // + // b3 -> b4 -> b5 -> b6 -> b7 -> b8 -> b9 -> b10 -> b11 + // \-> b4b -> b5b* -> b6b# -> b7b# -> b8b# \-> b10a + // | \-> b8c# -> b9c# + // | ---- + // | ^ (best invalid header) + // \-> b4f -> b5f -> b6f -> b7f -> b8f -> b9f -> b10f + // ---- + // (best chain tip) ^ + // ------------------------------------------------------------------------- + + g.ReconsiderBlockAndExpectTip("b11", nil, "b10f") + g.ExpectBestHeader("b11") + g.ExpectBestInvalidHeader("b9c") + g.ExpectIsCurrent(false) + + // ------------------------------------------------------------------------- + // Accept the rest of the block data to reach the current best header so + // it becomes the best chain tip and ensure the chain latches to current. + // + // ... -> b3 -> b4 -> b5 -> b6 -> b7 -> b8 -> b9 -> b10 -> b11 + // ------------------------------------------------------------------------- + + g.AcceptBlockData("b10") + g.AcceptBlock("b11") + g.ExpectBestHeader("b11") + g.ExpectIsCurrent(true) + + // ------------------------------------------------------------------------- + // Invalidate and reconsider a block at the tip of the current best chain so + // that it has the same cumulative work as another branch that received the + // block data first. The second chain must become the best one despite them + // having the exact same work and the header for the initial branch being + // seen first because branches that receive their block data first take + // precedence. The initial branch must become the best chain again after + // it is reconsidered because it has more work. + // + // The chain should believe it is current both before and after since the + // best chain tip matches the best header in both cases. + // + // (invalidate, reconsider, header seen first) v + // --- + // ... -> b3 -> b4 -> b5 -> b6 -> b7 -> b8 -> b9 -> b10 -> b11 + // \-> b4f -> b5f -> b6f -> b7f -> b8f -> b9f -> b10f + // ---- + // (data seen first) ^ + // ------------------------------------------------------------------------- + + g.InvalidateBlockAndExpectTip("b11", nil, "b10f") + g.ExpectBestHeader("b10f") + g.ExpectIsCurrent(true) + + g.ReconsiderBlockAndExpectTip("b11", nil, "b11") + g.ExpectBestHeader("b11") + g.ExpectBestInvalidHeader("b9c") + g.ExpectIsCurrent(true) +} From 970a56e64412ebea2933a8d4888e720931508cae Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Tue, 29 Dec 2020 20:40:46 -0600 Subject: [PATCH 2/4] rpc/jsonrpc/types: Add invalidate/reconsiderblock. This adds the command types for the upcoming invalidateblock and reconsiderblock RPC commands. --- rpc/jsonrpc/types/chainsvrcmds.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/rpc/jsonrpc/types/chainsvrcmds.go b/rpc/jsonrpc/types/chainsvrcmds.go index 0491d60e8a..1633542ebf 100644 --- a/rpc/jsonrpc/types/chainsvrcmds.go +++ b/rpc/jsonrpc/types/chainsvrcmds.go @@ -859,6 +859,19 @@ func NewHelpCmd(command *string) *HelpCmd { } } +// InvalidateBlockCmd defines the invalidateblock JSON-RPC command. +type InvalidateBlockCmd struct { + BlockHash string +} + +// NewInvalidateBlockCmd returns a new instance which can be used to issue an +// invalidateblock JSON-RPC command. +func NewInvalidateBlockCmd(hash string) *InvalidateBlockCmd { + return &InvalidateBlockCmd{ + BlockHash: hash, + } +} + // LiveTicketsCmd is a type handling custom marshaling and // unmarshaling of livetickets JSON RPC commands. type LiveTicketsCmd struct{} @@ -908,6 +921,19 @@ func NewPingCmd() *PingCmd { return &PingCmd{} } +// ReconsiderBlockCmd defines the reconsiderblock JSON-RPC command. +type ReconsiderBlockCmd struct { + BlockHash string +} + +// NewReconsiderBlockCmd returns a new instance which can be used to issue a +// reconsiderblock JSON-RPC command. +func NewReconsiderBlockCmd(hash string) *ReconsiderBlockCmd { + return &ReconsiderBlockCmd{ + BlockHash: hash, + } +} + // SearchRawTransactionsCmd defines the searchrawtransactions JSON-RPC command. type SearchRawTransactionsCmd struct { Address string @@ -1179,10 +1205,12 @@ func init() { dcrjson.MustRegister(Method("getvoteinfo"), (*GetVoteInfoCmd)(nil), flags) dcrjson.MustRegister(Method("getwork"), (*GetWorkCmd)(nil), flags) dcrjson.MustRegister(Method("help"), (*HelpCmd)(nil), flags) + dcrjson.MustRegister(Method("invalidateblock"), (*InvalidateBlockCmd)(nil), flags) dcrjson.MustRegister(Method("livetickets"), (*LiveTicketsCmd)(nil), flags) dcrjson.MustRegister(Method("missedtickets"), (*MissedTicketsCmd)(nil), flags) dcrjson.MustRegister(Method("node"), (*NodeCmd)(nil), flags) dcrjson.MustRegister(Method("ping"), (*PingCmd)(nil), flags) + dcrjson.MustRegister(Method("reconsiderblock"), (*ReconsiderBlockCmd)(nil), flags) dcrjson.MustRegister(Method("regentemplate"), (*RegenTemplateCmd)(nil), flags) dcrjson.MustRegister(Method("searchrawtransactions"), (*SearchRawTransactionsCmd)(nil), flags) dcrjson.MustRegister(Method("sendrawtransaction"), (*SendRawTransactionCmd)(nil), flags) From 034e279c08e6df44b67fc9a241311e31daf14aad Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Tue, 29 Dec 2020 20:40:47 -0600 Subject: [PATCH 3/4] rpcserver: Add invalidate/reconsiderblock support. This implements the invalidateblock and reconsiderblock RPC commands including the required help strings. --- dcrjson/jsonrpcerr.go | 1 + internal/rpcserver/interface.go | 17 ++++ internal/rpcserver/rpcserver.go | 83 ++++++++++++++++++++ internal/rpcserver/rpcserverhandlers_test.go | 14 ++++ internal/rpcserver/rpcserverhelp.go | 18 ++++- 5 files changed, 130 insertions(+), 3 deletions(-) diff --git a/dcrjson/jsonrpcerr.go b/dcrjson/jsonrpcerr.go index 5877fdaceb..d1452849b2 100644 --- a/dcrjson/jsonrpcerr.go +++ b/dcrjson/jsonrpcerr.go @@ -77,6 +77,7 @@ const ( ErrRPCRawTxString RPCErrorCode = -32602 ErrRPCDecodeHexString RPCErrorCode = -22 ErrRPCDuplicateTx RPCErrorCode = -40 + ErrRPCReconsiderFailure RPCErrorCode = -50 ) // Errors that are specific to btcd. diff --git a/internal/rpcserver/interface.go b/internal/rpcserver/interface.go index b1ed46ed5a..25960f6aea 100644 --- a/internal/rpcserver/interface.go +++ b/internal/rpcserver/interface.go @@ -408,6 +408,23 @@ type Chain interface { // TSpendCountVotes returns the votes for the specified tspend up to // the specified block. TSpendCountVotes(*chainhash.Hash, *dcrutil.Tx) (int64, int64, error) + + // InvalidateBlock manually invalidates the provided block as if the block + // had violated a consensus rule and marks all of its descendants as having + // a known invalid ancestor. It then reorganizes the chain as necessary so + // the branch with the most cumulative proof of work that is still valid + // becomes the main chain. + InvalidateBlock(*chainhash.Hash) error + + // ReconsiderBlock removes the known invalid status of the provided block + // and all of its ancestors along with the known invalid ancestor status + // from all of its descendants that are neither themselves marked as having + // failed validation nor descendants of another such block. Therefore, it + // allows the affected blocks to be reconsidered under the current consensus + // rules. It then potentially reorganizes the chain as necessary so the + // block with the most cumulative proof of work that is valid becomes the + // tip of the main chain. + ReconsiderBlock(*chainhash.Hash) error } // Clock represents a clock for use with the RPC server. The purpose of this diff --git a/internal/rpcserver/rpcserver.go b/internal/rpcserver/rpcserver.go index 3a3cb47a68..28cfae899c 100644 --- a/internal/rpcserver/rpcserver.go +++ b/internal/rpcserver/rpcserver.go @@ -200,10 +200,12 @@ var rpcHandlersBeforeInit = map[types.Method]commandHandler{ "gettxoutsetinfo": handleGetTxOutSetInfo, "getwork": handleGetWork, "help": handleHelp, + "invalidateblock": handleInvalidateBlock, "livetickets": handleLiveTickets, "missedtickets": handleMissedTickets, "node": handleNode, "ping": handlePing, + "reconsiderblock": handleReconsiderBlock, "regentemplate": handleRegenTemplate, "searchrawtransactions": handleSearchRawTransactions, "sendrawtransaction": handleSendRawTransaction, @@ -3889,6 +3891,35 @@ func handleHelp(_ context.Context, s *Server, cmd interface{}) (interface{}, err return help, nil } +// handleInvalidateBlock implements the invalidateblock command. +func handleInvalidateBlock(_ context.Context, s *Server, cmd interface{}) (interface{}, error) { + c := cmd.(*types.InvalidateBlockCmd) + hash, err := chainhash.NewHashFromStr(c.BlockHash) + if err != nil { + return nil, rpcDecodeHexError(c.BlockHash) + } + + chain := s.cfg.Chain + err = chain.InvalidateBlock(hash) + if err != nil { + if errors.Is(err, blockchain.ErrUnknownBlock) { + return nil, &dcrjson.RPCError{ + Code: dcrjson.ErrRPCBlockNotFound, + Message: fmt.Sprintf("Block not found: %v", hash), + } + } + + if errors.Is(err, blockchain.ErrInvalidateGenesisBlock) { + return nil, rpcInvalidError("%v", err) + } + + context := fmt.Sprintf("Failed to invalidate block %s", hash) + return nil, rpcInternalError(err.Error(), context) + } + + return nil, nil +} + // handleLiveTickets implements the livetickets command. func handleLiveTickets(_ context.Context, s *Server, cmd interface{}) (interface{}, error) { lt, err := s.cfg.Chain.LiveTickets() @@ -3934,6 +3965,58 @@ func handlePing(_ context.Context, s *Server, cmd interface{}) (interface{}, err return nil, nil } +// handleReconsiderBlock implements the reconsiderblock command. +func handleReconsiderBlock(_ context.Context, s *Server, cmd interface{}) (interface{}, error) { + c := cmd.(*types.ReconsiderBlockCmd) + hash, err := chainhash.NewHashFromStr(c.BlockHash) + if err != nil { + return nil, rpcDecodeHexError(c.BlockHash) + } + + chain := s.cfg.Chain + err = chain.ReconsiderBlock(hash) + if err != nil { + if errors.Is(err, blockchain.ErrUnknownBlock) { + return nil, &dcrjson.RPCError{ + Code: dcrjson.ErrRPCBlockNotFound, + Message: fmt.Sprintf("Block not found: %v", hash), + } + } + + // Use separate error code for failed validation. + allRuleErrs := func(err error) bool { + var rErr blockchain.RuleError + if !errors.As(err, &rErr) { + return false + } + + var mErr blockchain.MultiError + if errors.As(err, &mErr) { + for _, e := range mErr { + if !errors.As(e, &rErr) { + return false + } + } + } + + return true + } + if allRuleErrs(err) { + return nil, &dcrjson.RPCError{ + Code: dcrjson.ErrRPCReconsiderFailure, + Message: fmt.Sprintf("Reconsidering block %s led to one or "+ + "more validation failures: %v", hash, err), + } + } + + // Fall back to an internal error. + context := fmt.Sprintf("Error while reconsidering block %s", hash) + return nil, rpcInternalError(err.Error(), context) + } + + return nil, nil +} + // handleRegenTemplate implements the regentemplate command. func handleRegenTemplate(_ context.Context, s *Server, cmd interface{}) (interface{}, error) { bt := s.cfg.BlockTemplater diff --git a/internal/rpcserver/rpcserverhandlers_test.go b/internal/rpcserver/rpcserverhandlers_test.go index 8247b9f832..0473da0918 100644 --- a/internal/rpcserver/rpcserverhandlers_test.go +++ b/internal/rpcserver/rpcserverhandlers_test.go @@ -163,6 +163,7 @@ type testRPCChain struct { headerByHeight wire.BlockHeader headerByHeightErr error heightRangeFn func(startHeight, endHeight int64) ([]chainhash.Hash, error) + invalidateBlockErr error isCurrent bool liveTickets []chainhash.Hash liveTicketsErr error @@ -176,6 +177,7 @@ type testRPCChain struct { missedTicketsErr error nextThresholdState blockchain.ThresholdStateTuple nextThresholdStateErr error + reconsiderBlockErr error stateLastChangedHeight int64 stateLastChangedHeightErr error ticketPoolValue dcrutil.Amount @@ -321,6 +323,12 @@ func (c *testRPCChain) HeightRange(startHeight, endHeight int64) ([]chainhash.Ha return c.heightRangeFn(startHeight, endHeight) } +// InvalidateBlock returns a mocked error from manually invalidating a given +// block. +func (c *testRPCChain) InvalidateBlock(hash *chainhash.Hash) error { + return c.invalidateBlockErr +} + // IsCurrent returns a mocked bool representing whether or not the chain // believes it is current. func (c *testRPCChain) IsCurrent() bool { @@ -368,6 +376,12 @@ func (c *testRPCChain) NextThresholdState(hash *chainhash.Hash, version uint32, return c.nextThresholdState, c.nextThresholdStateErr } +// ReconsiderBlock returns a mocked error from manually reconsidering a given +// block. +func (c *testRPCChain) ReconsiderBlock(hash *chainhash.Hash) error { + return c.reconsiderBlockErr +} + // StateLastChangedHeight returns a mocked height at which the provided // consensus deployment agenda last changed state. func (c *testRPCChain) StateLastChangedHeight(hash *chainhash.Hash, version uint32, deploymentID string) (int64, error) { diff --git a/internal/rpcserver/rpcserverhelp.go b/internal/rpcserver/rpcserverhelp.go index 2fdaa45c55..178d822558 100644 --- a/internal/rpcserver/rpcserverhelp.go +++ b/internal/rpcserver/rpcserverhelp.go @@ -724,15 +724,25 @@ var helpDescsEnUS = map[string]string{ "help--result0": "List of commands", "help--result1": "Help for specified command", + // InvalidateBlockCmd help. + "invalidateblock--synopsis": "Permanently invalidates a block as if it had violated consensus rules.\n" + + "Use reconsiderblock to remove the invalid status.", + "invalidateblock-blockhash": "The hash of the block to invalidate", + // PingCmd help. "ping--synopsis": "Queues a ping to be sent to each connected peer.\n" + "Ping times are provided by getpeerinfo via the pingtime and pingwait fields.", // RebroadcastMissed help. - "rebroadcastmissed--synopsis": "Asks the daemon to rebroadcast missed votes.\n", + "rebroadcastmissed--synopsis": "Asks the daemon to rebroadcast missed votes.", + + // RebroadcastWinnersCmd help. + "rebroadcastwinners--synopsis": "Asks the daemon to rebroadcast the winners of the voting lottery.", - // RebroadcastWinnerCmd help. - "rebroadcastwinners--synopsis": "Asks the daemon to rebroadcast the winners of the voting lottery.\n", + // ReconsiderBlockCmd help. + "reconsiderblock--synopsis": "Reconsiders a block for validation and best chain selection by removing any invalid status from it and its ancestors.\n" + + "Any descendants that are neither themselves marked as having failed validation, nor descendants of another such block, are also made eligibile for best chain selection.", + "reconsiderblock-blockhash": "The hash of the block to reconsider", // SearchRawTransactionsCmd help. "searchrawtransactions--synopsis": "Returns raw data for transactions involving the passed address.\n" + @@ -1033,10 +1043,12 @@ var rpcResultTypes = map[types.Method][]interface{}{ "getwork": {(*types.GetWorkResult)(nil), (*bool)(nil)}, "getcoinsupply": {(*int64)(nil)}, "help": {(*string)(nil), (*string)(nil)}, + "invalidateblock": nil, "livetickets": {(*types.LiveTicketsResult)(nil)}, "missedtickets": {(*types.MissedTicketsResult)(nil)}, "node": nil, "ping": nil, + "reconsiderblock": nil, "regentemplate": nil, "searchrawtransactions": {(*string)(nil), (*[]types.SearchRawTransactionsResult)(nil)}, "sendrawtransaction": {(*string)(nil)}, From 0e72a3ec11b6d12440e036f15554c117945b20e3 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Tue, 29 Dec 2020 20:40:47 -0600 Subject: [PATCH 4/4] docs: Add invalidate/reconsiderblock JSON-RPC API. This adds documentation for the new invalidateblock and reconsiderblock RPC commands to the JSON-RPC API docs. --- docs/json_rpc_api.mediawiki | 48 +++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/docs/json_rpc_api.mediawiki b/docs/json_rpc_api.mediawiki index b3a8abc15e..6eb92d4e05 100644 --- a/docs/json_rpc_api.mediawiki +++ b/docs/json_rpc_api.mediawiki @@ -342,6 +342,10 @@ the method name for further details such as parameter and return information. |Y |Returns a list of all commands or help for a specified command. |- +|[[#invalidateblock|invalidateblock]] +|N +|Permanently invalidates a block as if it had violated consensus rules. +|- |[[#livetickets|livetickets]] |Y |Returns live ticket hashes from the ticket database. @@ -358,6 +362,10 @@ the method name for further details such as parameter and return information. |N |Queues a ping to be sent to each connected peer. |- +|[[#reconsiderblock|reconsiderblock]] +|N +|Reconsiders a block for validation and best chain selection by removing any invalid status from it and its ancestors. Any descendants that are neither themselves marked as having failed validation, nor descendants of another such block, are also made eligibile for best chain selection. +|- |[[#regentemplate|regentemplate]] |Y |Asks the daemon to regenerate the mining block template. @@ -2161,6 +2169,26 @@ of the best block. ---- +====invalidateblock==== +{| +!Method +|invalidateblock +|- +!Parameters +| +# block hash: (string, required) the hash of the block to invalidate +|- +!Description +| +: Permanently invalidates a block as if it had violated consensus rules. +: Use [[#reconsiderblock|reconsiderblock]] to remove the invalid status. +|- +!Returns +|Nothing +|} + +---- + ====livetickets==== {| !Method @@ -2243,6 +2271,26 @@ of the best block. ---- +====reconsiderblock==== +{| +!Method +|reconsiderblock +|- +!Parameters +| +# block hash: (string, required) the hash of the block to reconsider +|- +!Description +| +: Reconsiders a block for validation and best chain selection by removing any invalid status from it and its ancestors. +: Any descendants that are neither themselves marked as having failed validation, nor descendants of another such block, are also made eligibile for best chain selection. +|- +!Returns +|Nothing +|} + +---- + ====regentemplate==== {| !Method