Skip to content

Commit

Permalink
[release-v1.7] blockchain: Enforce testnet difficulty throttling.
Browse files Browse the repository at this point in the history
Currently, version 3 of the test network implements a minimum difficulty
reduction rule that was inherited from btcsuite which intends to act as
mechanism to deal with major difficulty spikes due to ASICs which are
typically not running testnet and since it's not reasonable to require
high-powered hardware to keep the test network running smoothly.

Unfortunately, this existing rule is not a particularly good solution in
general as it is not very deterministic and introduces additional
complications around difficulty selection.  It is an even worse solution
in the the case of Decred due to its hybrid model.

Rather than the aforementioned reactive approach, this introduces more
deterministic proactive testnet rules in order limit the type of games
that ASICs can play on testnet.

In particular, two new rules are introduced that are only imposed
started with block height 962928:

- A maximum allowed difficulty is now imposed on testnet such that
  CPU mining will still be feasible without resorting to any type of
  reactive and more complicated difficulty dropping
- Once the maximum allowed difficulty is reached on testnet, blocks must
  be at least 1 minute apart

The combination of these rules will prevent the difficulty on testnet
from ever rising to levels that are out of reach for CPUs to continue
mining blocks and throttle production in the case of higher-powered
hardware such as GPUs and ASICs.

It should be noted that this solution is only suitable on a test network
where no real monetary value is in play and thus the typical game theory
mechanics do not apply.

Finally, code to invalidate the existing extremely high work testnet
chain which has stalled testnet after that point is added to allow the
test network to be recovered without needing to fire up a bunch of
ASICs.
  • Loading branch information
davecgh committed Aug 2, 2022
1 parent 0c0c921 commit 3387e4e
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 9 deletions.
48 changes: 48 additions & 0 deletions blockchain/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ type BlockChain struct {
latestCheckpoint *chaincfg.Checkpoint
deploymentVers map[string]uint32
db database.DB
minTestNetTarget *big.Int
dbInfo *databaseInfo
chainParams *chaincfg.Params
timeSource MedianTimeSource
Expand Down Expand Up @@ -2224,6 +2225,12 @@ func (b *BlockChain) RemoveSpendEntry(hash *chainhash.Hash) error {
return err
}

// isTestNet3 returns whether or not the chain instance is for version 3 of the
// test network.
func (b *BlockChain) isTestNet3() bool {
return b.chainParams.Net == wire.TestNet3
}

// ChainParams returns the network parameters of the chain.
//
// This is part of the indexers.ChainQueryer interface.
Expand Down Expand Up @@ -2377,6 +2384,16 @@ func New(ctx context.Context, config *Config) (*BlockChain, error) {
return nil, err
}

// Impose a maximum difficulty target on the test network to prevent runaway
// difficulty on testnet by ASICs and GPUs since it's not reasonable to
// require high-powered hardware to keep the test network running smoothly.
var minTestNetTarget *big.Int
if params.Net == wire.TestNet3 {
// This equates to a maximum difficulty of 2^6 = 64.
const maxTestDiffShift = 6
minTestNetTarget = new(big.Int).Rsh(params.PowLimit, maxTestDiffShift)
}

// Either use the subsidy cache provided by the caller or create a new
// one when one was not provided.
subsidyCache := config.SubsidyCache
Expand All @@ -2388,6 +2405,7 @@ func New(ctx context.Context, config *Config) (*BlockChain, error) {
assumeValid: config.AssumeValid,
latestCheckpoint: config.LatestCheckpoint,
deploymentVers: deploymentVers,
minTestNetTarget: minTestNetTarget,
db: config.DB,
chainParams: params,
timeSource: config.TimeSource,
Expand Down Expand Up @@ -2440,6 +2458,36 @@ func New(ctx context.Context, config *Config) (*BlockChain, error) {
log.Infof("UTXO database version info: version: %d, compression: %d, utxo "+
"set: %d", utxoDbInfo.version, utxoDbInfo.compVer, utxoDbInfo.utxoVer)

// Manually invalidate any chains on version 3 of the test network that were
// created prior to enforcement of the maximum difficulty rules.
if b.isTestNet3() {
// Discover any existing nodes at the max diff activation height that do
// not have the expected hash and have not already been invalidated.
invalidateNodes := make([]*blockNode, 0, 1)
b.index.Lock()
b.index.forEachChainTip(func(tip *blockNode) error {
node := tip.Ancestor(testNet3MaxDiffActivationHeight)
if node != nil && !node.status.KnownInvalid() {
invalidateNodes = append(invalidateNodes, node)
}
return nil
})
b.index.Unlock()

// Invalidate the blocks without notification callbacks as the calling
// code will not be fully initialized yet at this point and this is
// being done as a part of chain initialization.
curNtfnCallback := b.notifications
b.notifications = nil
for _, node := range invalidateNodes {
if err := b.InvalidateBlock(&node.hash); err != nil {
b.notifications = curNtfnCallback
return nil, err
}
}
b.notifications = curNtfnCallback
}

b.index.RLock()
bestHdr := b.index.bestHeader
b.index.RUnlock()
Expand Down
38 changes: 32 additions & 6 deletions blockchain/difficulty.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,17 @@ func (b *BlockChain) calcNextRequiredDifficulty(prevNode *blockNode, newBlockTim

// We're not at a retarget point, return the oldDiff.
params := b.chainParams
if (prevNode.height+1)%params.WorkDiffWindowSize != 0 {
// For networks that support it, allow special reduction of the
// required difficulty once too much time has elapsed without
// mining a block.
if params.ReduceMinDifficulty {
nextHeight := prevNode.height + 1
if nextHeight%params.WorkDiffWindowSize != 0 {
// For networks that support it, allow special reduction of the required
// difficulty once too much time has elapsed without mining a block.
//
// Note that this behavior is deprecated and thus is only supported on
// testnet v3 prior to the max diff activation height. It will be
// removed in future version of testnet.
if params.ReduceMinDifficulty && (!b.isTestNet3() || nextHeight <
testNet3MaxDiffActivationHeight) {

// Return minimum difficulty when more than the desired
// amount of time has elapsed without mining a block.
reductionTime := int64(params.MinDiffReductionTime / time.Second)
Expand Down Expand Up @@ -176,11 +182,31 @@ func (b *BlockChain) calcNextRequiredDifficulty(prevNode *blockNode, newBlockTim
nextDiffBig.Set(nextDiffBigMin)
}

// Limit new value to the proof of work limit.
// Prevent the difficulty from going lower than the minimum allowed
// difficulty.
//
// Larger numbers result in a lower difficulty, so imposing a minimum
// difficulty equates to limiting the maximum target value.
if nextDiffBig.Cmp(params.PowLimit) > 0 {
nextDiffBig.Set(params.PowLimit)
}

// Prevent the difficulty from going higher than a maximum allowed
// difficulty on the test network. This is to prevent runaway difficulty on
// testnet by ASICs and GPUs since it's not reasonable to require
// high-powered hardware to keep the test network running smoothly.
//
// Smaller numbers result in a higher difficulty, so imposing a maximum
// difficulty equates to limiting the minimum target value.
//
// This rule is only active on the version 3 test network once the max diff
// activation height has been reached.
if b.minTestNetTarget != nil && nextDiffBig.Cmp(b.minTestNetTarget) < 0 &&
(!b.isTestNet3() || nextHeight >= testNet3MaxDiffActivationHeight) {

nextDiffBig = b.minTestNetTarget
}

// Convert the difficulty to the compact representation and return it.
nextDiffBits := standalone.BigToCompact(nextDiffBig)
return nextDiffBits
Expand Down
11 changes: 8 additions & 3 deletions blockchain/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ const (
// timestamps to have a maximum precision of one second.
ErrInvalidTime = ErrorKind("ErrInvalidTime")

// ErrTimeTooOld indicates the time is either before the median time of
// the last several blocks per the chain consensus rules or prior to the
// most recent checkpoint.
// ErrTimeTooOld indicates the time is either before the median time of the
// last several blocks per the chain consensus rules or prior to the time
// that is required by max difficulty limitations on the test network.
ErrTimeTooOld = ErrorKind("ErrTimeTooOld")

// ErrTimeTooNew indicates the time is too far in the future as compared
Expand Down Expand Up @@ -110,6 +110,11 @@ const (
// most recent checkpoint.
ErrCheckpointTimeTooOld = ErrorKind("ErrCheckpointTimeTooOld")

// ErrBadMaxDiffCheckpoint indicates a block on the version 3 test network
// at the height used to activate maximum difficulty semantics does not
// match the expected one.
ErrBadMaxDiffCheckpoint = ErrorKind("ErrBadMaxDiffCheckpoint")

// ErrNoTransactions indicates the block does not have a least one
// transaction. A valid block must have at least the coinbase
// transaction.
Expand Down
1 change: 1 addition & 0 deletions blockchain/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func TestErrorKindStringer(t *testing.T) {
{ErrBadCheckpoint, "ErrBadCheckpoint"},
{ErrForkTooOld, "ErrForkTooOld"},
{ErrCheckpointTimeTooOld, "ErrCheckpointTimeTooOld"},
{ErrBadMaxDiffCheckpoint, "ErrBadMaxDiffCheckpoint"},
{ErrNoTransactions, "ErrNoTransactions"},
{ErrTooManyTransactions, "ErrTooManyTransactions"},
{ErrNoTxInputs, "ErrNoTxInputs"},
Expand Down
39 changes: 39 additions & 0 deletions blockchain/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ const (
// another. However, because there is a 2^128 chance of a collision, the
// paranoid user may wish to turn this feature on.
checkForDuplicateHashes = false

// testNet3MaxDiffActivationHeight is the height that enforcement of the
// maximum difficulty rules starts.
testNet3MaxDiffActivationHeight = 962928
)

// mustParseHash converts the passed big-endian hex string into a
Expand Down Expand Up @@ -133,6 +137,10 @@ var (
block424011Hash = mustParseHash("0000000000000000317fc6c7a8a6578be7dfa9c96eb81d620050a3732b02d572")
block428809Hash = mustParseHash("00000000000000003147798ccffcecaa420fb1c7934d8f4e33809a871ee34aaa")
block430191Hash = mustParseHash("00000000000000002127ad6d4cb30cc16f6344589b417e42650388bb0690a88e")

// block962928Hash is the hash of the checkpoint used to activate maximum
// difficulty semantics on the version 3 test network.
block962928Hash = mustParseHash("0000004fd1b267fd39111d456ff557137824538e6f6776168600e56002e23b93")
)

// voteBitsApproveParent returns whether or not the passed vote bits indicate
Expand Down Expand Up @@ -1051,6 +1059,27 @@ func (b *BlockChain) checkBlockHeaderPositional(header *wire.BlockHeader, prevNo
str = fmt.Sprintf(str, header.Timestamp, medianTime)
return ruleError(ErrTimeTooOld, str)
}

// A block on the test network must have a timestamp that is at least
// one minute after the previous one once the maximum allowed difficulty
// has been reached. This helps throttle ASICs and GPUs since it's not
// reasonable to require high-powered hardware to keep the test network
// running smoothly.
//
// This rule is only active on the version 3 test network once the max
// diff activation height has been reached.
blockHeight := prevNode.height + 1
if b.minTestNetTarget != nil &&
expDiff <= standalone.BigToCompact(b.minTestNetTarget) &&
(!b.isTestNet3() || blockHeight >= testNet3MaxDiffActivationHeight) {

minTime := time.Unix(prevNode.timestamp, 0).Add(time.Minute)
if !header.Timestamp.After(minTime) {
str := "testnet block timestamp of %v is not after required %v"
str = fmt.Sprintf(str, header.Timestamp, minTime)
return ruleError(ErrTimeTooOld, str)
}
}
}

// The height of this block is one more than the referenced previous
Expand Down Expand Up @@ -1089,6 +1118,16 @@ func (b *BlockChain) checkBlockHeaderPositional(header *wire.BlockHeader, prevNo
return ruleError(ErrBadCheckpoint, str)
}

// Reject version 3 test network chains that are not specifically the chain
// used to activate maximum difficulty semantics.
if b.isTestNet3() && blockHeight == testNet3MaxDiffActivationHeight &&
blockHash != *block962928Hash {

str := fmt.Sprintf("block at height %d does not match checkpoint hash",
blockHeight)
return ruleError(ErrBadMaxDiffCheckpoint, str)
}

if !fastAdd {
// Reject old version blocks once a majority of the network has
// upgraded.
Expand Down

0 comments on commit 3387e4e

Please sign in to comment.