From 14baadc3cbc37c9932214366b763989ed8128220 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Mon, 22 May 2023 17:01:01 -0500 Subject: [PATCH] multi: Implement DCP0011 PoW hash consensus vote. This implements the agenda for voting on changing the hash function to BLAKE3, resetting the target difficulty, and changing the difficulty algorithm to ASERT (Absolutely Scheduled Exponentially weighted Rising Targets) as defined in DCP0011 along with consensus tests. In terms of the overall effects, this includes updates to: - The validation logic for the proof of work hash and required block difficulty - Enforcement of block submission via the getwork and submitblock RPCs - Mining template generation - The output of the getwork and notifywork RPCs - The internal CPU miner Also note that this does not implement the block version bump that will ultimately be needed by the mining code since there are multiple consensus votes gated behind it and will therefore be done separately. The following is an overview of the changes: - Introduce convenience function for determining if the vote passed and is now active - Introduce convenience function for determining whether or not the agenda is forced active on networks other than the main network - Add per-network parameters to chaincfg for the new difficulty algorithm including the initial starting difficulty for BLAKE3 and the half life - Modify block validation to enforce BLAKE3 as the proof of work hash algorithm in accordance with the state of the vote - Modify block validation to enforce target difficulties per the ASERT algorithm based on a reset initial starting difficulty in accordance with the state of the vote - Update internal CPU miner to solve blocks with either BLAKE256 or BLAKE3 in accordance with the state of the vote - Update the getwork, notifywork, and submitblock RPCs to support BLAKE3 in accordance with the state of the vote - Add tests for determining if the agenda is active for both mainnet and testnet - Add tests for the getwork RPC including submission of a block solved with BLAKE3 - Add tests to ensure proper behavior for the hash function and difficulty semantics before and after the vote agenda is active --- chaincfg/mainnetparams.go | 26 +- chaincfg/params.go | 29 ++ chaincfg/regnetparams.go | 26 +- chaincfg/simnetparams.go | 28 +- chaincfg/testnetparams.go | 26 +- go.mod | 2 +- internal/blockchain/agendas_test.go | 170 ++++++++++++ internal/blockchain/chain.go | 23 ++ internal/blockchain/chainio.go | 9 + internal/blockchain/difficulty.go | 149 +++++++++- internal/blockchain/difficulty_test.go | 7 +- internal/blockchain/process.go | 4 +- internal/blockchain/thresholdstate.go | 61 +++++ internal/blockchain/validate.go | 273 +++++++++++++++++-- internal/blockchain/validate_test.go | 192 +++++++++++++ internal/mining/cpuminer/cpuminer.go | 71 ++++- internal/mining/mining.go | 4 +- internal/mining/mining_test.go | 12 +- internal/rpcserver/interface.go | 5 + internal/rpcserver/rpcserver.go | 116 ++++++-- internal/rpcserver/rpcserverhandlers_test.go | 112 +++++++- internal/rpcserver/rpcwebsocket.go | 7 +- server.go | 1 + 23 files changed, 1228 insertions(+), 125 deletions(-) diff --git a/chaincfg/mainnetparams.go b/chaincfg/mainnetparams.go index db2f233623..6415814670 100644 --- a/chaincfg/mainnetparams.go +++ b/chaincfg/mainnetparams.go @@ -93,22 +93,28 @@ func MainNetParams() *Params { }, // Chain parameters - GenesisBlock: &genesisBlock, - GenesisHash: genesisBlock.BlockHash(), - PowLimit: mainPowLimit, - PowLimitBits: mainPowLimitBits, - ReduceMinDifficulty: false, - MinDiffReductionTime: 0, // Does not apply since ReduceMinDifficulty false - GenerateSupported: false, - MaximumBlockSizes: []int{393216}, - MaxTxSize: 393216, - TargetTimePerBlock: time.Minute * 5, + GenesisBlock: &genesisBlock, + GenesisHash: genesisBlock.BlockHash(), + PowLimit: mainPowLimit, + PowLimitBits: mainPowLimitBits, + ReduceMinDifficulty: false, + MinDiffReductionTime: 0, // Does not apply since ReduceMinDifficulty false + GenerateSupported: false, + MaximumBlockSizes: []int{393216}, + MaxTxSize: 393216, + TargetTimePerBlock: time.Minute * 5, + + // Version 1 difficulty algorithm (EMA + BLAKE256) parameters. WorkDiffAlpha: 1, WorkDiffWindowSize: 144, WorkDiffWindows: 20, TargetTimespan: time.Minute * 5 * 144, // TimePerBlock * WindowSize RetargetAdjustmentFactor: 4, + // Version 2 difficulty algorithm (ASERT + BLAKE3) parameters. + WorkDiffV2Blake3StartBits: 0x1b00a5a6, + WorkDiffV2HalfLifeSecs: 43200, // 144 * TimePerBlock (12 hours) + // Subsidy parameters. BaseSubsidy: 3119582664, // 21m MulSubsidy: 100, diff --git a/chaincfg/params.go b/chaincfg/params.go index 9d2069d7e2..8a2726f77d 100644 --- a/chaincfg/params.go +++ b/chaincfg/params.go @@ -297,16 +297,26 @@ type Params struct { // block. TargetTimePerBlock time.Duration + // ------------------------------------------------------------------------- + // Version 1 difficulty algorithm (EMA + BLAKE256) parameters. + // ------------------------------------------------------------------------- + // WorkDiffAlpha is the stake difficulty EMA calculation alpha (smoothing) // value. It is different from a normal EMA alpha. Closer to 1 --> smoother. + // + // This only applies to the version 1 difficulty retarget algorithm. WorkDiffAlpha int64 // WorkDiffWindowSize is the number of windows (intervals) used for calculation // of the exponentially weighted average. + // + // This only applies to the version 1 difficulty retarget algorithm. WorkDiffWindowSize int64 // WorkDiffWindows is the number of windows (intervals) used for calculation // of the exponentially weighted average. + // + // This only applies to the version 1 difficulty retarget algorithm. WorkDiffWindows int64 // TargetTimespan is the desired amount of time that should elapse @@ -314,13 +324,32 @@ type Params struct { // it should be changed in order to maintain the desired block // generation rate. This value should correspond to the product of // WorkDiffWindowSize and TimePerBlock above. + // + // This only applies to the version 1 difficulty retarget algorithm. TargetTimespan time.Duration // RetargetAdjustmentFactor is the adjustment factor used to limit // the minimum and maximum amount of adjustment that can occur between // difficulty retargets. + // + // This only applies to the version 1 difficulty retarget algorithm. RetargetAdjustmentFactor int64 + // ------------------------------------------------------------------------- + // Version 2 difficulty algorithm (ASERT + BLAKE3) parameters. + // ------------------------------------------------------------------------- + + // WorkDiffV2Blake3StartBits is the starting difficulty bits to use for + // proof of work under BLAKE3. + WorkDiffV2Blake3StartBits uint32 + + // WorkDiffV2HalfLife is the number of seconds to use for the relaxation + // time when calculating how difficult it is to solve a block. The + // algorithm sets the difficulty exponentially such that it is halved or + // doubled for every multiple of this value the most recent block is behind + // or ahead of the ideal schedule. + WorkDiffV2HalfLifeSecs int64 + // Subsidy parameters. // // Subsidy calculation for exponential reductions: diff --git a/chaincfg/regnetparams.go b/chaincfg/regnetparams.go index 6eb0188080..a6c1701a49 100644 --- a/chaincfg/regnetparams.go +++ b/chaincfg/regnetparams.go @@ -93,22 +93,28 @@ func RegNetParams() *Params { DNSSeeds: nil, // NOTE: There must NOT be any seeds. // Chain parameters - GenesisBlock: &genesisBlock, - GenesisHash: genesisBlock.BlockHash(), - PowLimit: regNetPowLimit, - PowLimitBits: regNetPowLimitBits, - ReduceMinDifficulty: false, - MinDiffReductionTime: 0, // Does not apply since ReduceMinDifficulty false - GenerateSupported: true, - MaximumBlockSizes: []int{1000000, 1310720}, - MaxTxSize: 1000000, - TargetTimePerBlock: time.Second, + GenesisBlock: &genesisBlock, + GenesisHash: genesisBlock.BlockHash(), + PowLimit: regNetPowLimit, + PowLimitBits: regNetPowLimitBits, + ReduceMinDifficulty: false, + MinDiffReductionTime: 0, // Does not apply since ReduceMinDifficulty false + GenerateSupported: true, + MaximumBlockSizes: []int{1000000, 1310720}, + MaxTxSize: 1000000, + TargetTimePerBlock: time.Second, + + // Version 1 difficulty algorithm (EMA + BLAKE256) parameters. WorkDiffAlpha: 1, WorkDiffWindowSize: 8, WorkDiffWindows: 4, TargetTimespan: time.Second * 8, // TimePerBlock * WindowSize RetargetAdjustmentFactor: 4, + // Version 2 difficulty algorithm (ASERT + BLAKE3) parameters. + WorkDiffV2Blake3StartBits: regNetPowLimitBits, + WorkDiffV2HalfLifeSecs: 6, // 6 * TimePerBlock + // Subsidy parameters. BaseSubsidy: 50000000000, MulSubsidy: 100, diff --git a/chaincfg/simnetparams.go b/chaincfg/simnetparams.go index def2707e9e..cbd785d123 100644 --- a/chaincfg/simnetparams.go +++ b/chaincfg/simnetparams.go @@ -52,7 +52,7 @@ func SimNetParams() *Params { Voters: 0, FreshStake: 0, Revocations: 0, - Timestamp: time.Unix(1401292357, 0), // 2009-01-08 20:54:25 -0600 CST + Timestamp: time.Unix(1401292357, 0), // 2014-05-28 15:52:37 +0000 UTC PoolSize: 0, Bits: simNetPowLimitBits, SBits: 0, @@ -93,22 +93,28 @@ func SimNetParams() *Params { DNSSeeds: nil, // NOTE: There must NOT be any seeds. // Chain parameters - GenesisBlock: &genesisBlock, - GenesisHash: genesisBlock.BlockHash(), - PowLimit: simNetPowLimit, - PowLimitBits: simNetPowLimitBits, - ReduceMinDifficulty: false, - MinDiffReductionTime: 0, // Does not apply since ReduceMinDifficulty false - GenerateSupported: true, - MaximumBlockSizes: []int{1000000, 1310720}, - MaxTxSize: 1000000, - TargetTimePerBlock: time.Second, + GenesisBlock: &genesisBlock, + GenesisHash: genesisBlock.BlockHash(), + PowLimit: simNetPowLimit, + PowLimitBits: simNetPowLimitBits, + ReduceMinDifficulty: false, + MinDiffReductionTime: 0, // Does not apply since ReduceMinDifficulty false + GenerateSupported: true, + MaximumBlockSizes: []int{1000000, 1310720}, + MaxTxSize: 1000000, + TargetTimePerBlock: time.Second, + + // Version 1 difficulty algorithm (EMA + BLAKE256) parameters. WorkDiffAlpha: 1, WorkDiffWindowSize: 8, WorkDiffWindows: 4, TargetTimespan: time.Second * 8, // TimePerBlock * WindowSize RetargetAdjustmentFactor: 4, + // Version 2 difficulty algorithm (ASERT + BLAKE3) parameters. + WorkDiffV2Blake3StartBits: simNetPowLimitBits, + WorkDiffV2HalfLifeSecs: 6, // 6 * TimePerBlock + // Subsidy parameters. BaseSubsidy: 50000000000, MulSubsidy: 100, diff --git a/chaincfg/testnetparams.go b/chaincfg/testnetparams.go index 08ad64f69a..ecdb32204d 100644 --- a/chaincfg/testnetparams.go +++ b/chaincfg/testnetparams.go @@ -91,22 +91,28 @@ func TestNet3Params() *Params { // // Note that the minimum difficulty reduction parameter only applies up // to and including block height 962927. - GenesisBlock: &genesisBlock, - GenesisHash: genesisBlock.BlockHash(), - PowLimit: testNetPowLimit, - PowLimitBits: testNetPowLimitBits, - ReduceMinDifficulty: true, - MinDiffReductionTime: time.Minute * 10, // ~99.3% chance to be mined before reduction - GenerateSupported: true, - MaximumBlockSizes: []int{1310720}, - MaxTxSize: 1000000, - TargetTimePerBlock: time.Minute * 2, + GenesisBlock: &genesisBlock, + GenesisHash: genesisBlock.BlockHash(), + PowLimit: testNetPowLimit, + PowLimitBits: testNetPowLimitBits, + ReduceMinDifficulty: true, + MinDiffReductionTime: time.Minute * 10, // ~99.3% chance to be mined before reduction + GenerateSupported: true, + MaximumBlockSizes: []int{1310720}, + MaxTxSize: 1000000, + TargetTimePerBlock: time.Minute * 2, + + // Version 1 difficulty algorithm (EMA + BLAKE256) parameters. WorkDiffAlpha: 1, WorkDiffWindowSize: 144, WorkDiffWindows: 20, TargetTimespan: time.Minute * 2 * 144, // TimePerBlock * WindowSize RetargetAdjustmentFactor: 4, + // Version 2 difficulty algorithm (ASERT + BLAKE3) parameters. + WorkDiffV2Blake3StartBits: testNetPowLimitBits, + WorkDiffV2HalfLifeSecs: 720, // 6 * TimePerBlock (12 minutes) + // Subsidy parameters. BaseSubsidy: 2500000000, // 25 Coin MulSubsidy: 100, diff --git a/go.mod b/go.mod index 58177ae9a6..d8fa07c986 100644 --- a/go.mod +++ b/go.mod @@ -40,6 +40,7 @@ require ( github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 golang.org/x/sys v0.5.0 golang.org/x/term v0.5.0 + lukechampine.com/blake3 v1.2.1 ) require ( @@ -49,7 +50,6 @@ require ( github.com/decred/dcrd/hdkeychain/v3 v3.1.0 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect - lukechampine.com/blake3 v1.2.1 // indirect ) replace ( diff --git a/internal/blockchain/agendas_test.go b/internal/blockchain/agendas_test.go index 094c13cfea..8c604bddf9 100644 --- a/internal/blockchain/agendas_test.go +++ b/internal/blockchain/agendas_test.go @@ -1017,6 +1017,176 @@ func TestSubsidySplitDeployment(t *testing.T) { testSubsidySplitDeployment(t, chaincfg.RegNetParams()) } +// testBlake3PowDeployment ensures the deployment of the blake3 proof of work +// agenda activates for the provided network parameters. +func testBlake3PowDeployment(t *testing.T, params *chaincfg.Params) { + // Clone the parameters so they can be mutated, find the correct deployment + // for the modified subsidy split agenda as well as the yes vote choice + // within it, and, finally, ensure it is always available to vote by + // removing the time constraints to prevent test failures when the real + // expiration time passes. + const voteID = chaincfg.VoteIDBlake3Pow + params = cloneParams(params) + deploymentVer, deployment := findDeployment(t, params, voteID) + yesChoice := findDeploymentChoice(t, deployment, "yes") + removeDeploymentTimeConstraints(deployment) + + // Shorter versions of params for convenience. + stakeValidationHeight := uint32(params.StakeValidationHeight) + ruleChangeActivationInterval := params.RuleChangeActivationInterval + + // blake3AnchorHeight is the height of the expected blake3 anchor block + // given the test conditions below. + blake3AnchorHeight := int64(stakeValidationHeight + + ruleChangeActivationInterval*3 - 1) + + tests := []struct { + name string + numNodes uint32 // num fake nodes to create + curActive bool // whether agenda active for current block + nextActive bool // whether agenda active for NEXT block + resetAnchor bool // whether or not to reset cached blake 3 anchors + }{{ + name: "stake validation height", + numNodes: stakeValidationHeight, + curActive: false, + nextActive: false, + }, { + name: "started", + numNodes: ruleChangeActivationInterval, + curActive: false, + nextActive: false, + }, { + name: "lockedin", + numNodes: ruleChangeActivationInterval, + curActive: false, + nextActive: false, + }, { + name: "one before active", + numNodes: ruleChangeActivationInterval - 1, + curActive: false, + nextActive: true, + }, { + name: "exactly active", + numNodes: 1, + curActive: true, + nextActive: true, + }, { + name: "one after active", + numNodes: 1, + curActive: true, + nextActive: true, + }, { + name: "one before next rcai after active", + numNodes: ruleChangeActivationInterval - 2, + curActive: true, + nextActive: true, + }, { + name: "exactly next rcai after active", + numNodes: 1, + curActive: true, + nextActive: true, + }, { + name: "one after next rcai after active", + numNodes: 1, + curActive: true, + nextActive: true, + }, { + name: "one before 2nd rcai after active with anchor reset", + numNodes: ruleChangeActivationInterval - 2, + curActive: true, + nextActive: true, + resetAnchor: true, + }, { + name: "exactly 2nd rcai after active with anchor reset", + numNodes: 1, + curActive: true, + nextActive: true, + resetAnchor: true, + }, { + name: "one after 2nd rcai after active with anchor reset", + numNodes: 1, + curActive: true, + nextActive: true, + resetAnchor: true, + }} + + curTimestamp := time.Now() + bc := newFakeChain(params) + node := bc.bestChain.Tip() + for _, test := range tests { + for i := uint32(0); i < test.numNodes; i++ { + node = newFakeNode(node, int32(deploymentVer), deploymentVer, 0, + curTimestamp) + + // Create fake votes that vote yes on the agenda to ensure it is + // activated. + for j := uint16(0); j < params.TicketsPerBlock; j++ { + node.votes = append(node.votes, stake.VoteVersionTuple{ + Version: deploymentVer, + Bits: yesChoice.Bits | 0x01, + }) + } + bc.index.AddNode(node) + bc.bestChain.SetTip(node) + curTimestamp = curTimestamp.Add(time.Second) + } + + // Ensure the agenda reports the expected activation status for the + // current block. + gotActive, err := bc.isBlake3PowAgendaActive(node.parent) + if err != nil { + t.Errorf("%s: unexpected err: %v", test.name, err) + continue + } + if gotActive != test.curActive { + t.Errorf("%s: mismatched current active status - got: %v, want: %v", + test.name, gotActive, test.curActive) + continue + } + + // Ensure the agenda reports the expected activation status for the NEXT + // block + gotActive, err = bc.IsBlake3PowAgendaActive(&node.hash) + if err != nil { + t.Errorf("%s: unexpected err: %v", test.name, err) + continue + } + if gotActive != test.nextActive { + t.Errorf("%s: mismatched next active status - got: %v, want: %v", + test.name, gotActive, test.nextActive) + continue + } + + // Reset the cached blake3 anchor block when requested by the test flag. + // This helps ensure the logic that walks backwards to find the anchor + // works as intended. + if test.resetAnchor { + bc.cachedBlake3WorkDiffCandidateAnchor.Store(nil) + bc.cachedBlake3WorkDiffAnchor.Store(nil) + } + + // Ensure the blake3 anchor block is the expected value once the agenda + // is active. + if test.nextActive { + wantAnchor := bc.bestChain.nodeByHeight(blake3AnchorHeight) + gotAnchor := bc.blake3WorkDiffAnchor(node) + if gotAnchor != wantAnchor { + t.Errorf("%s: mistmatched blake3 anchor - got: %s, want %s", + test.name, gotAnchor, wantAnchor) + continue + } + } + } +} + +// TestBlake3PowDeployment ensures the deployment of the blake3 proof of work +// agenda activates as expected. +func TestBlake3PowDeployment(t *testing.T) { + testBlake3PowDeployment(t, chaincfg.MainNetParams()) + testBlake3PowDeployment(t, chaincfg.RegNetParams()) +} + // testSubsidySplitR2Deployment ensures the deployment of the 1/89/10 subsidy // split agenda activates for the provided network parameters. func testSubsidySplitR2Deployment(t *testing.T, params *chaincfg.Params) { diff --git a/internal/blockchain/chain.go b/internal/blockchain/chain.go index 4d6d193435..6ac3635503 100644 --- a/internal/blockchain/chain.go +++ b/internal/blockchain/chain.go @@ -15,6 +15,7 @@ import ( "math/bits" "strings" "sync" + "sync/atomic" "time" "github.com/decred/dcrd/blockchain/stake/v5" @@ -167,6 +168,7 @@ type BlockChain struct { deploymentData map[string]deploymentInfo minKnownWork *uint256.Uint256 minTestNetTarget *big.Int + minTestNetDiffBits uint32 db database.DB dbInfo *databaseInfo chainParams *chaincfg.Params @@ -271,6 +273,24 @@ type BlockChain struct { calcVoterVersionIntervalCache map[[chainhash.HashSize]byte]uint32 calcStakeVersionCache map[[chainhash.HashSize]byte]uint32 + // cachedBlake3WorkDiffCandidateAnchor houses a cached candidate anchor + // point to use for the difficulty algorithm defined in DCP0011 before the + // state of the blake3 proof of work agenda can be determined definitively. + // + // This is set based on heuristics that make it very likely to be the + // correct anchor point and exists to avoid additional work that would + // otherwise be required during the initial header sync. + cachedBlake3WorkDiffCandidateAnchor atomic.Pointer[blockNode] + + // cachedBlake3WorkDiffAnchor houses a cached anchor point to use for the + // difficulty algorithm defined in DCP0011. + // + // It is only set when the blake3 proof of work agenda has been determined + // to be active and will either be the block just prior to the activation of + // the agenda or the genesis block for networks where the agenda is always + // active such as the simulation network. + cachedBlake3WorkDiffAnchor atomic.Pointer[blockNode] + // bulkImportMode provides a mechanism to indicate that several validation // checks can be avoided when bulk importing blocks already known to be valid. // It is protected by the chain lock. @@ -2372,10 +2392,12 @@ func New(ctx context.Context, config *Config) (*BlockChain, error) { // 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 + var minTestNetDiffBits uint32 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) + minTestNetDiffBits = standalone.BigToCompact(minTestNetTarget) } // Either use the subsidy cache provided by the caller or create a new @@ -2405,6 +2427,7 @@ func New(ctx context.Context, config *Config) (*BlockChain, error) { deploymentData: deploymentData, minKnownWork: minKnownWork, minTestNetTarget: minTestNetTarget, + minTestNetDiffBits: minTestNetDiffBits, db: config.DB, chainParams: params, timeSource: config.TimeSource, diff --git a/internal/blockchain/chainio.go b/internal/blockchain/chainio.go index ebf1eea9eb..ebff2e39fc 100644 --- a/internal/blockchain/chainio.go +++ b/internal/blockchain/chainio.go @@ -1735,6 +1735,15 @@ func (b *BlockChain) initChainState(ctx context.Context, } } + // Set the block to treat as the anchor block for the purposes of + // determining how far ahead or behind the ideal schedule the provided + // block is when calculating the blake3 target difficulty to the genesis + // block on networks where the blake3 proof of work agenda is always + // active. + if b.isBlake3PowAgendaForcedActive() { + b.cachedBlake3WorkDiffAnchor.Store(b.bestChain.genesis()) + } + b.stateSnapshot = newBestState(tip, blockSize, numTxns, state.totalTxns, tip.CalcPastMedianTime(), state.totalSubsidy, uint32(tip.stakeNode.PoolSize()), diff --git a/internal/blockchain/difficulty.go b/internal/blockchain/difficulty.go index 75f47fd832..601471b60c 100644 --- a/internal/blockchain/difficulty.go +++ b/internal/blockchain/difficulty.go @@ -24,8 +24,6 @@ var ( // findPrevTestNetDifficulty returns the difficulty of the previous block which // did not have the special testnet minimum difficulty rule applied. -// -// This function MUST be called with the chain state lock held (for writes). func (b *BlockChain) findPrevTestNetDifficulty(startNode *blockNode) uint32 { // Search backwards through the chain for the last block without // the special rule applied. @@ -47,9 +45,10 @@ func (b *BlockChain) findPrevTestNetDifficulty(startNode *blockNode) uint32 { return lastBits } -// calcNextRequiredDifficulty calculates the required difficulty for the block -// after the passed previous block node based on the difficulty retarget rules. -func (b *BlockChain) calcNextRequiredDifficulty(prevNode *blockNode, newBlockTime time.Time) uint32 { +// calcNextBlake256Diff calculates the required difficulty for the block AFTER +// the passed previous block node based on the difficulty retarget rules for the +// blake256 hash algorithm used at Decred launch. +func (b *BlockChain) calcNextBlake256Diff(prevNode *blockNode, newBlockTime time.Time) uint32 { // Get the old difficulty; if we aren't at a block height where it changes, // just return this. oldDiff := prevNode.bits @@ -212,20 +211,152 @@ func (b *BlockChain) calcNextRequiredDifficulty(prevNode *blockNode, newBlockTim return nextDiffBits } +// calcNextBlake3Diff calculates the required difficulty for the block +// AFTER the passed previous block node based on the difficulty retarget rules +// defined in DCP0011. +func (b *BlockChain) calcNextBlake3Diff(prevNode *blockNode, blake3Anchor *blockNode) uint32 { + // Calculate the time and height deltas as the difference between the + // provided block and the blake3 anchor block. + // + // Notice that if the difficulty prior to the activation point were being + // maintained, this would need to be the timestamp and height of the parent + // of the blake3 anchor block (except when the anchor is the genesis block) + // in order for the absolute calculations to exactly match the behavior of + // relative calculations. + // + // However, since the initial difficulty is reset with the agenda, no + // additional offsets are needed. + timeDelta := prevNode.timestamp - blake3Anchor.timestamp + heightDelta := prevNode.height - blake3Anchor.height + + // Calculate the next target difficulty using the ASERT algorithm. + // + // Note that the difficulty of the anchor block is NOT used for the initial + // difficulty because the difficulty must be reset due to the change to + // blake3 for proof of work. The initial difficulty comes from the chain + // parameters instead. + params := b.chainParams + nextDiff := standalone.CalcASERTDiff(params.WorkDiffV2Blake3StartBits, + params.PowLimit, int64(params.TargetTimePerBlock.Seconds()), timeDelta, + heightDelta, params.WorkDiffV2HalfLifeSecs) + + // 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. + if b.minTestNetTarget != nil && nextDiff < b.minTestNetDiffBits { + nextDiff = b.minTestNetDiffBits + } + + return nextDiff +} + +// blake3WorkDiffAnchor returns the block to treat as the anchor block for the +// purposes of determining how far ahead or behind the ideal schedule the +// provided block is when calculating the blake3 target difficulty for the block +// AFTER the passed previous block node. +// +// This function MUST only be called with the blake3 proof of work agenda active +// and with the chain state lock held (for writes). +func (b *BlockChain) blake3WorkDiffAnchor(prevNode *blockNode) *blockNode { + // Use the previously cached anchor when it exists and is actually an + // ancestor of the passed node. + // + // Note that it will always be set to the genesis block on networks where + // the agenda is always active. + anchor := b.cachedBlake3WorkDiffAnchor.Load() + if anchor != nil && anchor.IsAncestorOf(prevNode) { + return anchor + } + + // Find the block just prior to the activation of the blake3 proof of work + // agenda from the perspective of the block AFTER the passed block. + // + // The state of an agenda can only change at a rule change activation + // boundary, so determine the final block of the rule change activation + // interval just prior to the interval the block AFTER the provided one + // falls in and then loop backwards one interval at a time until the agenda + // is no longer active. + rcai := int64(b.chainParams.RuleChangeActivationInterval) + svh := b.chainParams.StakeValidationHeight + finalNodeHeight := calcWantHeight(svh, rcai, prevNode.height+1) + candidate := prevNode.Ancestor(finalNodeHeight) + for candidate != nil && candidate.parent != nil { + // Since isBlake3PowAgendaActive returns the state for the block AFTER + // the provided one and the goal here is to determine the state of the + // agenda for the candidate anchor (which is the final block of the rule + // change activation interval under test), use the parent to get the + // state of the candidate itself. + isActive, err := b.isBlake3PowAgendaActive(candidate.parent) + if err != nil { + panicf("known good agenda state lookup failed for block node "+ + "hash %v (height %v) -- %v", candidate.parent.hash, + candidate.parent.height, err) + } + if !isActive { + anchor = candidate + break + } + + // Skip back to the ancestor that is the final block of the previous + // rule change interval. + candidate = candidate.RelativeAncestor(rcai) + } + + // Update the cached anchor to the discovered one since it is highly likely + // the next call will involve a descendant of this anchor as opposed to some + // other anchor on an entirely unrelated side chain. + if anchor != nil { + b.cachedBlake3WorkDiffAnchor.Store(anchor) + } + + return anchor +} + +// calcNextRequiredDifficulty calculates the required difficulty for the block +// AFTER the passed previous block node based on the active difficulty retarget +// rules. +// +// This function MUST be called with the chain state lock held (for writes). +func (b *BlockChain) calcNextRequiredDifficulty(prevNode *blockNode, newBlockTime time.Time) (uint32, error) { + // Choose the difficulty algorithm based on the result of the vote for the + // blake3 proof of work agenda. + isActive, err := b.isBlake3PowAgendaActive(prevNode) + if err != nil { + return 0, err + } + if isActive { + // Determine the block to treat as the anchor block for the purposes of + // determining how far ahead or behind the ideal schedule the provided + // block is when calculating the blake3 target difficulty. + // + // This will either be the block just prior to the activation of the + // blake3 proof of work agenda or the genesis block for networks where + // the agenda is always active such as the simulation network. + anchor := b.blake3WorkDiffAnchor(prevNode) + return b.calcNextBlake3Diff(prevNode, anchor), nil + } + + return b.calcNextBlake256Diff(prevNode, newBlockTime), nil +} + // CalcNextRequiredDifficulty calculates the required difficulty for the block -// after the given block based on the difficulty retarget rules. +// AFTER the given block based on the active difficulty retarget rules. // // This function is safe for concurrent access. func (b *BlockChain) CalcNextRequiredDifficulty(hash *chainhash.Hash, timestamp time.Time) (uint32, error) { node := b.index.LookupNode(hash) - if node == nil { + if node == nil || !b.index.CanValidate(node) { return 0, unknownBlockError(hash) } b.chainLock.Lock() - difficulty := b.calcNextRequiredDifficulty(node, timestamp) + difficulty, err := b.calcNextRequiredDifficulty(node, timestamp) b.chainLock.Unlock() - return difficulty, nil + return difficulty, err } // mergeDifficulty takes an original stake difficulty and two new, scaled diff --git a/internal/blockchain/difficulty_test.go b/internal/blockchain/difficulty_test.go index 8db0d76cdd..1b1261cf00 100644 --- a/internal/blockchain/difficulty_test.go +++ b/internal/blockchain/difficulty_test.go @@ -1,5 +1,5 @@ // Copyright (c) 2014 The btcsuite developers -// Copyright (c) 2015-2020 The Decred developers +// Copyright (c) 2015-2023 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -1116,7 +1116,10 @@ func TestMinDifficultyReduction(t *testing.T) { // Update the block time according to the test data and calculate // the difficulty for the next block. blockTime = blockTime.Add(test.timeAdjustment(i)) - diff := bc.calcNextRequiredDifficulty(node, blockTime) + diff, err := bc.calcNextRequiredDifficulty(node, blockTime) + if err != nil { + t.Fatalf("%s: unexpected err: %v", test.name, err) + } // Ensure the calculated difficulty matches the expected value. expectedDiff := test.expectedDiff(i) diff --git a/internal/blockchain/process.go b/internal/blockchain/process.go index a0da54ae0b..92a3170cdd 100644 --- a/internal/blockchain/process.go +++ b/internal/blockchain/process.go @@ -1,5 +1,5 @@ // Copyright (c) 2013-2016 The btcsuite developers -// Copyright (c) 2015-2022 The Decred developers +// Copyright (c) 2015-2023 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -290,7 +290,7 @@ func (b *BlockChain) ProcessBlockHeader(header *wire.BlockHeader) error { // the code is changed to violate that assumption. // // The flags do not modify the behavior of this function directly, however they -// are needed to pass along to checkBlockPositional. +// are needed to pass along to checkBlockDataPositional. // // This function MUST be called with the chain lock held (for writes). func (b *BlockChain) maybeAcceptBlockData(node *blockNode, block *dcrutil.Block, flags BehaviorFlags) ([]*blockNode, error) { diff --git a/internal/blockchain/thresholdstate.go b/internal/blockchain/thresholdstate.go index d7a94954dd..bcba26b389 100644 --- a/internal/blockchain/thresholdstate.go +++ b/internal/blockchain/thresholdstate.go @@ -844,6 +844,67 @@ func (b *BlockChain) IsSubsidySplitAgendaActive(prevHash *chainhash.Hash) (bool, return isActive, err } +// isBlake3PowAgendaForcedActive returns whether or not the agenda to change the +// proof of work hash function to blake3, as defined in DCP0011, is forced +// active by the chain parameters. +func (b *BlockChain) isBlake3PowAgendaForcedActive() bool { + const deploymentID = chaincfg.VoteIDBlake3Pow + deployment, ok := b.deploymentData[deploymentID] + if !ok { + return false + } + + state := deployment.forcedState + return state != nil && state.State == ThresholdActive +} + +// isBlake3PowAgendaActive returns whether or not the agenda to change the proof +// of work hash function to blake3, as defined in DCP0011, has passed and is now +// active from the point of view of the passed block node. +// +// It is important to note that, as the variable name indicates, this function +// expects the block node prior to the block for which the deployment state is +// desired. In other words, the returned deployment state is for the block +// AFTER the passed node. +// +// This function MUST be called with the chain state lock held (for writes). +func (b *BlockChain) isBlake3PowAgendaActive(prevNode *blockNode) (bool, error) { + const deploymentID = chaincfg.VoteIDBlake3Pow + deployment, ok := b.deploymentData[deploymentID] + if !ok { + str := fmt.Sprintf("deployment ID %s does not exist", deploymentID) + return false, contextError(ErrUnknownDeploymentID, str) + } + + // NOTE: The choice field of the return threshold state is not examined + // here because there is only one possible choice that can be active for + // the agenda, which is yes, so there is no need to check it. + state := b.deploymentState(prevNode, &deployment) + return state.State == ThresholdActive, nil +} + +// IsBlake3PowAgendaActive returns whether or not the agenda to change the proof +// of work hash function to blake3, as defined in DCP0011, has passed and is now +// active for the block AFTER the given block. +// +// This function is safe for concurrent access. +func (b *BlockChain) IsBlake3PowAgendaActive(prevHash *chainhash.Hash) (bool, error) { + // The agenda is never active for the genesis block. + if *prevHash == *zeroHash { + return false, nil + } + + prevNode := b.index.LookupNode(prevHash) + if prevNode == nil || !b.index.CanValidate(prevNode) { + return false, unknownBlockError(prevHash) + } + + b.chainLock.Lock() + isActive, err := b.isBlake3PowAgendaActive(prevNode) + b.chainLock.Unlock() + return isActive, err +} + // isSubsidySplitR2AgendaActive returns whether or not the agenda to change the // block reward subsidy split to 1/89/10, as defined in DCP0012, has passed and // is now active from the point of view of the passed block node. diff --git a/internal/blockchain/validate.go b/internal/blockchain/validate.go index 60714a1604..1d82805d57 100644 --- a/internal/blockchain/validate.go +++ b/internal/blockchain/validate.go @@ -689,14 +689,19 @@ func standaloneToChainRuleError(err error) error { return err } -// checkProofOfWork ensures the block header bits which indicate the target -// difficulty is in min/max range and that the block hash is less than the -// target difficulty as claimed. +// checkProofOfWorkSanity ensures the block header bits which indicate the +// target difficulty is in min/max range and that the proof of work hash is less +// than the target difficulty as claimed. This check is context free. +// +// NOTE: This version differs from checkProofOfWorkContext in that it is run in +// the context free sanity checks where the result of the vote to change the +// proof of work hash function is not available. See the comments in the +// function itself for more details. // // The flags modify the behavior of this function as follows: // - BFNoPoWCheck: The check to ensure the block hash is less than the target // difficulty is not performed. -func checkProofOfWork(header *wire.BlockHeader, powLimit *big.Int, flags BehaviorFlags) error { +func checkProofOfWorkSanity(header *wire.BlockHeader, powLimit *big.Int, flags BehaviorFlags) error { // Only ensure the target difficulty bits are in the valid range when the // the flag to avoid proof of work checks is set. if flags&BFNoPoWCheck == BFNoPoWCheck { @@ -709,8 +714,24 @@ func checkProofOfWork(header *wire.BlockHeader, powLimit *big.Int, flags Behavio // - The target difficulty must be larger than zero. // - The target difficulty must be less than the maximum allowed. // - The proof of work hash must be less than the claimed target. - powHash := header.PowHashV1() - err := standalone.CheckProofOfWork(&powHash, header.Bits, powLimit) + // + // The ability to determine whether or not the blake3 proof of work agenda + // is active is not possible here because that relies on additional context + // that is not available in the context free checks. However, it is + // important to check proof of work here in the context free checks to + // protect against various forms of malicious behavior. + // + // Thus, allow valid proof of work under both algorithms while rejecting + // blocks that satisify neither here in the context free checks and allow + // the contextual checks that happen later to ensure the proof of work is + // valid specifically for the correct hash algorithm as determined by the + // state of the blake3 proof of work agenda. + powHashV1 := header.PowHashV1() + err := standalone.CheckProofOfWork(&powHashV1, header.Bits, powLimit) + if err != nil { + powHashV2 := header.PowHashV2() + err = standalone.CheckProofOfWork(&powHashV2, header.Bits, powLimit) + } return standaloneToChainRuleError(err) } @@ -719,7 +740,7 @@ func checkProofOfWork(header *wire.BlockHeader, powLimit *big.Int, flags Behavio // context free. // // The flags do not modify the behavior of this function directly, however they -// are needed to pass along to checkProofOfWork. +// are needed to pass along to checkProofOfWorkSanity. func checkBlockHeaderSanity(header *wire.BlockHeader, timeSource MedianTimeSource, flags BehaviorFlags, chainParams *chaincfg.Params) error { // The stake validation height should always be at least stake enabled // height, so assert it because the code below relies on that assumption. @@ -731,10 +752,10 @@ func checkBlockHeaderSanity(header *wire.BlockHeader, timeSource MedianTimeSourc "height %d", stakeEnabledHeight, stakeValidationHeight)) } - // Ensure the proof of work bits in the block header is in min/max - // range and the block hash is less than the target value described by - // the bits. - err := checkProofOfWork(header, chainParams.PowLimit, flags) + // Ensure the proof of work bits in the block header is in min/max range and + // the proof of work hash is less than the target value described by the + // bits. + err := checkProofOfWorkSanity(header, chainParams.PowLimit, flags) if err != nil { return err } @@ -1014,6 +1035,145 @@ func (b *BlockChain) isOldBlockVersionByMajority(header *wire.BlockHeader, block return b.isMajorityVersion(nextVersion, prevNode, rejectNumRequired) } +// checkDifficultyPositional ensures the difficulty specified in the block +// header matches the calculated difficulty based on the difficulty retarget +// rules. These checks do not, and must not, rely on having the full block data +// of all ancestors available. +func (b *BlockChain) checkDifficultyPositional(header *wire.BlockHeader, prevNode *blockNode) error { + // ------------------------------------------------------------------------- + // The ability to determine whether or not the blake3 proof of work agenda + // is active is not possible here because that relies on additional context + // that is not available in the positional checks. However, it is important + // to check for valid bits in the positional checks to protect against + // various forms of malicious behavior. + // + // Thus, allow valid difficulty bits under both difficulty algorithms while + // rejecting blocks that satisify neither here in the positional checks and + // allow the contextual checks that happen later to ensure the difficulty + // bits are valid specifically for the correct difficulty algorithm as + // determined by the state of the blake3 proof of work agenda. + // ------------------------------------------------------------------------- + + // A cached anchor point for the difficulty algorithm defined in DCP0011 is + // set when the contextual checks have determined that the agenda is + // actually active. + // + // Since contextual checks for previous block(s) may or may not have been + // done yet, this can't be relied on entirely, however, it is a nice + // optimization when it can be used. + // + // In practice, once the chain is synced, and the agenda is active, the + // anchor will be set and thus this can run immediately. + // + // It will always be set to the genesis block on networks where the agenda + // is always active. + cachedAnchor := b.cachedBlake3WorkDiffAnchor.Load() + if cachedAnchor != nil && cachedAnchor.IsAncestorOf(prevNode) { + blake3Diff := b.calcNextBlake3Diff(prevNode, cachedAnchor) + if header.Bits != blake3Diff { + str := fmt.Sprintf("block difficulty of %d is not the expected "+ + "value of %d", header.Bits, blake3Diff) + return ruleError(ErrUnexpectedDifficulty, str) + } + + return nil + } + + // Only the original difficulty algorithm needs to be checked when it is + // impossible for the blake 3 proof of work agenda to be active or the block + // is not solved for blake 3. + isSolvedBlake3 := func(header *wire.BlockHeader) bool { + powHash := header.PowHashV2() + err := standalone.CheckProofOfWorkHash(&powHash, header.Bits) + return err == nil + } + minBlake3BlockVersion := uint32(10) + if !isMainNet(b.chainParams) { + minBlake3BlockVersion++ + } + isBlake3PossiblyActive := uint32(header.Version) >= minBlake3BlockVersion + if !isBlake3PossiblyActive || !isSolvedBlake3(header) { + // Ensure the difficulty specified in the block header matches the + // calculated difficulty based on the previous block and difficulty + // retarget rules for the blake256 hash algorithm used at Decred launch. + blake256Diff := b.calcNextBlake256Diff(prevNode, header.Timestamp) + if header.Bits != blake256Diff { + str := fmt.Sprintf("block difficulty of %d is not the expected "+ + "value of %d", header.Bits, blake256Diff) + return ruleError(ErrUnexpectedDifficulty, str) + } + + return nil + } + + // At this point, the blake3 proof of work agenda might possibly be active + // and the block is solved using blake3, so the agenda is very likely + // active. + // + // Calculating the required difficulty once the agenda activates for the + // algorithm defined in DCP0011 requires the block prior to the activation + // of the agenda as an anchor. However, as previously discussed, the + // additional context needed to definitively determine when the agenda + // activated is not available here in the positional checks. + // + // In light of that, the following logic uses the fact that the agenda could + // have only possibly activated at a rule change activation interval to + // iterate backwards one interval at a time through all possible candidate + // anchors until one of them results in a required difficulty that matches. + // + // In the case there is a match, the header is assumed to be valid enough to + // make it through the positional checks. + // + // As an additional optimization to avoid a bunch of extra work during the + // initial header sync, a candidate anchor that results in a matching + // required difficulty is cached and tried first on subsequent descendant + // headers since it is very likely to be the correct one. + cachedCandidate := b.cachedBlake3WorkDiffCandidateAnchor.Load() + if cachedCandidate != nil && cachedCandidate.IsAncestorOf(prevNode) { + blake3Diff := b.calcNextBlake3Diff(prevNode, cachedCandidate) + if header.Bits == blake3Diff { + return nil + } + } + rcai := int64(b.chainParams.RuleChangeActivationInterval) + svh := b.chainParams.StakeValidationHeight + if int64(header.Height) >= svh+rcai { + // Iterate backwards through all possible anchor candidates which + // consist of the final blocks of previous rule change activation + // intervals so long as the block also has a version that is at least + // the minimum version that is enforced before voting on the agenda + // could have even started. + finalNodeHeight := calcWantHeight(svh, rcai, int64(header.Height)) + candidate := prevNode.Ancestor(finalNodeHeight) + for candidate != nil && + uint32(candidate.blockVersion) >= minBlake3BlockVersion { + + blake3Diff := b.calcNextBlake3Diff(prevNode, candidate) + if header.Bits == blake3Diff { + b.cachedBlake3WorkDiffCandidateAnchor.Store(candidate) + return nil + } + candidate = candidate.RelativeAncestor(rcai) + } + } + + // At this point, none of the possible difficulties for blake3 matched, so + // the agenda is very likely not actually active and therefore the only + // remaining valid option is the original difficulty algorithm. + // + // Ensure the difficulty specified in the block header matches the + // calculated difficulty based on the previous block and difficulty retarget + // rules for the blake256 hash algorithm used at Decred launch. + blake256Diff := b.calcNextBlake256Diff(prevNode, header.Timestamp) + if header.Bits != blake256Diff { + str := fmt.Sprintf("block difficulty of %d is not the expected value "+ + "of %d", header.Bits, blake256Diff) + return ruleError(ErrUnexpectedDifficulty, str) + } + + return nil +} + // checkBlockHeaderPositional performs several validation checks on the block // header which depend on its position within the block chain and having the // headers of all ancestors available. These checks do not, and must not, rely @@ -1032,17 +1192,6 @@ func (b *BlockChain) checkBlockHeaderPositional(header *wire.BlockHeader, prevNo fastAdd := flags&BFFastAdd == BFFastAdd if !fastAdd { - // Ensure the difficulty specified in the block header matches - // the calculated difficulty based on the previous block and - // difficulty retarget rules. - expDiff := b.calcNextRequiredDifficulty(prevNode, header.Timestamp) - blockDifficulty := header.Bits - if blockDifficulty != expDiff { - str := fmt.Sprintf("block difficulty of %d is not the "+ - "expected value of %d", blockDifficulty, expDiff) - return ruleError(ErrUnexpectedDifficulty, str) - } - // Ensure the timestamp for the block header is after the // median time of the last several blocks (medianTimeBlocks). medianTime := prevNode.CalcPastMedianTime() @@ -1062,7 +1211,7 @@ func (b *BlockChain) checkBlockHeaderPositional(header *wire.BlockHeader, prevNo // diff activation height has been reached. blockHeight := prevNode.height + 1 if b.minTestNetTarget != nil && - expDiff <= standalone.BigToCompact(b.minTestNetTarget) && + header.Bits <= standalone.BigToCompact(b.minTestNetTarget) && (!b.isTestNet3() || blockHeight >= testNet3MaxDiffActivationHeight) { minTime := time.Unix(prevNode.timestamp, 0).Add(time.Minute) @@ -1072,6 +1221,13 @@ func (b *BlockChain) checkBlockHeaderPositional(header *wire.BlockHeader, prevNo return ruleError(ErrTimeTooOld, str) } } + + // Ensure the difficulty specified in the block header matches the + // calculated difficulty based on the difficulty retarget rules. + err := b.checkDifficultyPositional(header, prevNode) + if err != nil { + return err + } } // The height of this block is one more than the referenced previous @@ -1201,6 +1357,39 @@ func (b *BlockChain) checkBlockPositional(block *dcrutil.Block, prevNode *blockN return b.checkBlockDataPositional(block, prevNode, flags) } +// checkProofOfWorkContext ensures the proof of work hash is less than the +// target difficulty indicated by the block header difficulty bits. +// +// NOTE: This does not ensure the target difficulty is in the valid min/max +// range since that is already handled in checkProofOfWorkSanity which is +// required to have been called prior. +// +// The flags modify the behavior of this function as follows: +// - BFNoPoWCheck: The check to ensure the block hash is less than the target +// difficulty is not performed. +func (b *BlockChain) checkProofOfWorkContext(header *wire.BlockHeader, prevNode *blockNode, flags BehaviorFlags) error { + // Nothing to do when the flag to avoid proof of work checks is set. + if flags&BFNoPoWCheck == BFNoPoWCheck { + return nil + } + + // Choose the proof of work mining algorithm based on the result of the vote + // for the blake3 proof of work agenda. + isBlake3PowActive, err := b.isBlake3PowAgendaActive(prevNode) + if err != nil { + return err + } + powHashFn := header.PowHashV1 + if isBlake3PowActive { + powHashFn = header.PowHashV2 + } + + // Ensure the proof of work hash is less than the claimed target. + powHash := powHashFn() + err = standalone.CheckProofOfWorkHash(&powHash, header.Bits) + return standaloneToChainRuleError(err) +} + // checkBlockHeaderContext performs several validation checks on the block // header which depend on having the full block data for all of its ancestors // available. This includes checks which depend on tallying the results of @@ -1215,7 +1404,19 @@ func (b *BlockChain) checkBlockPositional(block *dcrutil.Block, prevNode *blockN // vote will necessarily need to be transitioned to this function. // // The flags modify the behavior of this function as follows: -// - BFFastAdd: No check are performed. +// +// BFFastAdd: +// - The difficulty bits value is not checked to ensure it matches the next +// required calculated value +// - The stake difficulty is not checked to ensure it matches the next +// required calculated value +// - The stake version is not check to ensure it matches the expected version +// - The pool size commitment is not checked +// - The ticket lottery final state is not checked +// +// BFNoPoWCheck: +// - The check to ensure the block hash is less than the target difficulty is +// not performed // // This function MUST be called with the chain state lock held (for writes). func (b *BlockChain) checkBlockHeaderContext(header *wire.BlockHeader, prevNode *blockNode, flags BehaviorFlags) error { @@ -1224,8 +1425,32 @@ func (b *BlockChain) checkBlockHeaderContext(header *wire.BlockHeader, prevNode return nil } + // Ensure the proof of work bits in the block header is in min/max range and + // the proof of work hash is less than the target value described by the + // bits. + // + // Note that the bits have already been validated to ensure they are in the + // min/max range in the header sanity checks. + err := b.checkProofOfWorkContext(header, prevNode, flags) + if err != nil { + return err + } + fastAdd := flags&BFFastAdd == BFFastAdd if !fastAdd { + // Ensure the difficulty specified in the block header matches the + // calculated difficulty based on the previous block and difficulty + // retarget rules. + expDiff, err := b.calcNextRequiredDifficulty(prevNode, header.Timestamp) + if err != nil { + return err + } + if header.Bits != expDiff { + str := fmt.Sprintf("block difficulty of %d is not the expected "+ + "value of %d", header.Bits, expDiff) + return ruleError(ErrUnexpectedDifficulty, str) + } + // Ensure the stake difficulty specified in the block header // matches the calculated difficulty based on the previous block // and difficulty retarget rules. diff --git a/internal/blockchain/validate_test.go b/internal/blockchain/validate_test.go index ecdac30364..f0f453b693 100644 --- a/internal/blockchain/validate_test.go +++ b/internal/blockchain/validate_test.go @@ -2091,6 +2091,198 @@ func TestModifiedSubsidySplitSemantics(t *testing.T) { g.AcceptTipBlock() } +// TestBlake3PowSemantics ensures that the various semantics enforced by the +// blake3 proof of work agenda behave as intended. +func TestBlake3PowSemantics(t *testing.T) { + t.Parallel() + + // Use a set of test chain parameters which allow for quicker vote + // activation as compared to various existing network params. + params := quickVoteActivationParams() + + // Clone the parameters so they can be mutated, find the correct deployment + // for the blake3 proof of work agenda and ensure it is always available to + // vote by removing the time constraints to prevent test failures when the + // real expiration time passes. + const voteID = chaincfg.VoteIDBlake3Pow + params = cloneParams(params) + deploymentVer, deployment := findDeployment(t, params, voteID) + removeDeploymentTimeConstraints(deployment) + + // Create a test harness initialized with the genesis block as the tip. + g := newChaingenHarness(t, params) + + // Convenience funcs to determine if a block is solved for the original and + // updated hash algorithms. + powLimit := params.PowLimit + isSolvedV1 := func(header *wire.BlockHeader) bool { + powHash := header.PowHashV1() + err := standalone.CheckProofOfWork(&powHash, header.Bits, powLimit) + return err == nil + } + isSolvedV2 := func(header *wire.BlockHeader) bool { + powHash := header.PowHashV2() + err := standalone.CheckProofOfWork(&powHash, header.Bits, powLimit) + return err == nil + } + + // ------------------------------------------------------------------------- + // Generate and accept enough blocks to reach stake validation height. + // + // Note that this also ensures the proof of work requirements prior to the + // activation of the agenda remains unaffected. + // ------------------------------------------------------------------------- + + g.AdvanceToStakeValidationHeight() + + // ------------------------------------------------------------------------- + // Create a block that is solved with the new hash algorithm, and unsolved + // with the original hash algorithm. + // + // The block should be rejected because the agenda is NOT active. + // + // ... + // \-> bsvhbad + // ------------------------------------------------------------------------- + + tipName := g.TipName() + outs := g.OldestCoinbaseOuts() + g.UsePowHashAlgo(chaingen.PHABlake3) + g.UsePowDiffAlgo(chaingen.PDAAsert, "genesis") + bsvhbad := g.NextBlock("bsvhbad", &outs[0], outs[1:]) + // This test requires the block to be solved for the new hash algorithm and + // unsolved for the original one, but since the difficulty is so low in the + // tests, the block might still end up being inadvertently solved. It can't + // be checked inside a munger because the block is finalized after the + // function returns and those changes could also inadvertently solve the + // block. Thus, just increment the nonce until both conditions are + // satisfied and then replace it in the generator's state. + { + origHash := bsvhbad.BlockHash() + for !isSolvedV2(&bsvhbad.Header) || isSolvedV1(&bsvhbad.Header) { + bsvhbad.Header.Nonce++ + } + g.UpdateBlockState("bsvhbad", origHash, "bsvhbad", bsvhbad) + } + g.RejectTipBlock(ErrHighHash) + + // ------------------------------------------------------------------------- + // Generate and accept enough blocks with the appropriate vote bits set to + // reach one block prior to the blake3 proof of work agenda becoming + // active. + // + // Note that this also ensures the proof of work hash prior to the + // activation of the agenda remains unaffected. + // ------------------------------------------------------------------------- + + g.SetTip(tipName) + g.UsePowHashAlgo(chaingen.PHABlake256r14) + g.UsePowDiffAlgo(chaingen.PDAEma) + g.AdvanceFromSVHToActiveAgendas(voteID) + + // replaceVers is a munge function which modifies the provided block by + // replacing the block, stake, and vote versions with the blake3 proof of + // work deployment version. + replaceVers := func(b *wire.MsgBlock) { + chaingen.ReplaceBlockVersion(int32(deploymentVer))(b) + chaingen.ReplaceStakeVersion(deploymentVer)(b) + chaingen.ReplaceVoteVersions(deploymentVer)(b) + } + + // ------------------------------------------------------------------------- + // Create a block that is solved with the original hash algorithm and + // unsolved with the new one. + // + // The block should be rejected because the agenda is active. + // + // ... + // \-> b1bad + // ------------------------------------------------------------------------- + + tipName = g.TipName() + blake3AnchorName := tipName + outs = g.OldestCoinbaseOuts() + b1bad := g.NextBlock("b1bad", &outs[0], outs[1:], replaceVers) + // This test requires the block to be solved for the original hash + // algorithm and unsolved for the updated one, but since the difficulty is + // so low in the tests, the block might still end up being inadvertently + // solved. It can't be checked inside a munger because the block is + // finalized after the function returns and those changes could also + // inadvertently solve the block. Thus, just increment the nonce until both + // conditions are satisfied and then replace it in the generator's state. + { + origHash := b1bad.BlockHash() + for !isSolvedV1(&b1bad.Header) || isSolvedV2(&b1bad.Header) { + b1bad.Header.Nonce++ + } + g.UpdateBlockState("b1bad", origHash, "b1bad", b1bad) + } + g.RejectTipBlock(ErrHighHash) + + // ------------------------------------------------------------------------- + // Create a block that is mined with the new hash and difficulty algorithms. + // + // The block should be accepted because the agenda is active. + // + // ... -> b1 + // ------------------------------------------------------------------------- + + g.SetTip(tipName) + g.UsePowHashAlgo(chaingen.PHABlake3) + g.UsePowDiffAlgo(chaingen.PDAAsert, blake3AnchorName) + g.NextBlock("b1", &outs[0], outs[1:], replaceVers) + g.SaveTipCoinbaseOuts() + g.AcceptTipBlock() + + // ------------------------------------------------------------------------- + // Create a block that is mined with the new hash and difficulty algorithms + // and has a timestamp that is behind schedule in order to cause the + // required difficulty of the next block to rise according to the new + // difficulty algorithm. + // + // The block should be accepted because the agenda is active. + // + // ... -> b1 -> b2 + // ------------------------------------------------------------------------- + + outs = g.OldestCoinbaseOuts() + g.NextBlock("b2", &outs[0], outs[1:], replaceVers, func(b *wire.MsgBlock) { + b.Header.Timestamp = b.Header.Timestamp.Add(-time.Second) + }) + g.SaveTipCoinbaseOuts() + g.AcceptTipBlock() + + // ------------------------------------------------------------------------- + // Create a block that is mined with the new hash algorithm and uses the + // version 1 difficulty algorithm. + // + // The block should be rejected because the agenda is active and the block + // claims the wrong required difficulty. + // + // ... -> b1 -> b2 + // \-> b3bad + // ------------------------------------------------------------------------- + + tipName = g.TipName() + g.UsePowDiffAlgo(chaingen.PDAEma) + outs = g.OldestCoinbaseOuts() + g.NextBlock("b3bad", &outs[0], outs[1:], replaceVers) + g.RejectTipBlock(ErrUnexpectedDifficulty) + + // ------------------------------------------------------------------------- + // Create a block that is mined with the new hash and difficulty algorithms. + // + // The block should be accepted because the agenda is active. + // + // ... -> b1 -> b2 -> b3 + // ------------------------------------------------------------------------- + + g.SetTip(tipName) + g.UsePowDiffAlgo(chaingen.PDAAsert, blake3AnchorName) + g.NextBlock("b3", &outs[0], outs[1:], replaceVers) + g.AcceptTipBlock() +} + // TestModifiedSubsidySplitR2Semantics ensures that the various semantics // enforced by the modified subsidy split round 2 agenda behave as intended. func TestModifiedSubsidySplitR2Semantics(t *testing.T) { diff --git a/internal/mining/cpuminer/cpuminer.go b/internal/mining/cpuminer/cpuminer.go index a42cbfd7c7..ba0b4e6477 100644 --- a/internal/mining/cpuminer/cpuminer.go +++ b/internal/mining/cpuminer/cpuminer.go @@ -23,6 +23,7 @@ import ( "github.com/decred/dcrd/internal/mining" "github.com/decred/dcrd/internal/staging/primitives" "github.com/decred/dcrd/wire" + "lukechampine.com/blake3" ) const ( @@ -98,6 +99,11 @@ type Config struct { // not either the provided block is itself known to be invalid or is // known to have an invalid ancestor. IsKnownInvalidBlock func(*chainhash.Hash) bool + + // IsBlake3PowAgendaActive returns whether or not the agenda to change the + // proof of work hash function to blake3, as defined in DCP0011, has passed + // and is now active for the block AFTER the given block. + IsBlake3PowAgendaActive func(prevHash *chainhash.Hash) (bool, error) } // CPUMiner provides facilities for solving blocks (mining) using the CPU in a @@ -194,7 +200,7 @@ out: // submitBlock submits the passed block to network after ensuring it passes all // of the consensus validation rules. -func (m *CPUMiner) submitBlock(block *dcrutil.Block) bool { +func (m *CPUMiner) submitBlock(block *dcrutil.Block, isBlake3PowActive bool) bool { m.submitBlockLock.Lock() defer m.submitBlockLock.Unlock() @@ -225,7 +231,11 @@ func (m *CPUMiner) submitBlock(block *dcrutil.Block) bool { // The block was accepted. blockHash := block.Hash() var powHashStr string - powHash := block.MsgBlock().PowHashV1() + powHashFn := block.MsgBlock().PowHashV1 + if isBlake3PowActive { + powHashFn = block.MsgBlock().PowHashV2 + } + powHash := powHashFn() if powHash != *blockHash { powHashStr = ", pow hash " + powHash.String() } @@ -242,7 +252,9 @@ func (m *CPUMiner) submitBlock(block *dcrutil.Block) bool { // // This function will return early with false when the provided context is // cancelled or an unexpected error happens. -func (m *CPUMiner) solveBlock(ctx context.Context, header *wire.BlockHeader, stats *speedStats) bool { +func (m *CPUMiner) solveBlock(ctx context.Context, header *wire.BlockHeader, + stats *speedStats, isBlake3PowActive bool) bool { + // Choose a random extra nonce offset for this block template and // worker. enOffset, err := wire.RandomUint64() @@ -260,6 +272,12 @@ func (m *CPUMiner) solveBlock(ctx context.Context, header *wire.BlockHeader, sta return false } + // Choose the hash function depending on the active agendas. + powHashFn := blake256.Sum256 + if isBlake3PowActive { + powHashFn = blake3.Sum256 + } + // Serialize the header once so only the specific bytes that need to be // updated can be done in the main loops below. hdrBytes, err := header.Bytes() @@ -328,7 +346,7 @@ func (m *CPUMiner) solveBlock(ctx context.Context, header *wire.BlockHeader, sta // compute the block header hash. const nonceSerOffset = 140 littleEndian.PutUint32(hdrBytes[nonceSerOffset:], nonce) - hash := chainhash.Hash(blake256.Sum256(hdrBytes)) + hash := chainhash.Hash(powHashFn(hdrBytes)) hashesCompleted++ // The block is solved when the new block hash is less than the @@ -359,7 +377,9 @@ func (m *CPUMiner) solveBlock(ctx context.Context, header *wire.BlockHeader, sta // are mined when on the simulation network. // // It must be run as a goroutine. -func (m *CPUMiner) solver(ctx context.Context, template *mining.BlockTemplate, speedStats *speedStats) { +func (m *CPUMiner) solver(ctx context.Context, template *mining.BlockTemplate, + speedStats *speedStats, isBlake3PowActive bool) { + defer m.workerWg.Done() for { @@ -409,7 +429,8 @@ func (m *CPUMiner) solver(ctx context.Context, template *mining.BlockTemplate, s // The block in the template is shallow copied to avoid mutating the // data of the shared template. shallowBlockCopy := *template.Block - if m.solveBlock(ctx, &shallowBlockCopy.Header, speedStats) { + shallowBlockHdr := &shallowBlockCopy.Header + if m.solveBlock(ctx, shallowBlockHdr, speedStats, isBlake3PowActive) { // Avoid submitting any solutions that might have been found in // between the time a worker was signalled to stop and it actually // stopping. @@ -418,7 +439,7 @@ func (m *CPUMiner) solver(ctx context.Context, template *mining.BlockTemplate, s } block := dcrutil.NewBlock(&shallowBlockCopy) - if !m.submitBlock(block) { + if !m.submitBlock(block, isBlake3PowActive) { m.Lock() m.minedOnParents[prevBlock]++ m.Unlock() @@ -467,9 +488,9 @@ func (m *CPUMiner) generateBlocks(ctx context.Context, workerID uint64) { case templateNtfn := <-templateSub.C(): // Clean up the map that tracks the number of blocks mined on a // given parent whenever a template is received due to a new parent. + prevHash := templateNtfn.Template.Block.Header.PrevBlock if m.cfg.PermitConnectionlessMining { if templateNtfn.Reason == mining.TURNewParent { - prevHash := templateNtfn.Template.Block.Header.PrevBlock m.Lock() for k := range m.minedOnParents { if k != prevHash { @@ -480,14 +501,24 @@ func (m *CPUMiner) generateBlocks(ctx context.Context, workerID uint64) { } } - // Ensure the previous solver goroutine (if any) is stopped and - // start another one for the new template. + // Ensure the previous solver goroutine (if any) is stopped. if solverCancel != nil { solverCancel() } + + // Determine the state of the blake3 proof of work agenda. An error + // should never really happen here in practice, but just loop around + // and wait for another template if it does. + isBlake3PowActive, err := m.cfg.IsBlake3PowAgendaActive(&prevHash) + if err != nil { + continue + } + + // Start another goroutine for the new template. solverCtx, solverCancel = context.WithCancel(ctx) m.workerWg.Add(1) - go m.solver(solverCtx, templateNtfn.Template, &speedStats) + go m.solver(solverCtx, templateNtfn.Template, &speedStats, + isBlake3PowActive) case <-ctx.Done(): // Ensure resources associated with the solver goroutine context are @@ -752,12 +783,21 @@ out: discretePrev := m.discretePrevHash discreteBlockHash := m.discreteBlockHash m.Unlock() - if templateNtfn.Template.Block.Header.PrevBlock == discretePrev && + prevHash := templateNtfn.Template.Block.Header.PrevBlock + if prevHash == discretePrev && !m.cfg.IsKnownInvalidBlock(&discreteBlockHash) { continue } + // Determine the state of the blake3 proof of work agenda. An error + // should never really happen here in practice, but just loop around and + // wait for another template if it does. + isBlake3PowActive, err := m.cfg.IsBlake3PowAgendaActive(&prevHash) + if err != nil { + continue + } + // Attempt to solve the block. // // The function will exit with false if the block was not solved for any @@ -774,11 +814,12 @@ out: // The block in the template is shallow copied to avoid mutating the // data of the shared template. shallowBlockCopy := *templateNtfn.Template.Block - if m.solveBlock(ctx, &shallowBlockCopy.Header, &stats) { + shallowBlockHdr := &shallowBlockCopy.Header + if m.solveBlock(ctx, shallowBlockHdr, &stats, isBlake3PowActive) { block := dcrutil.NewBlock(&shallowBlockCopy) - if m.submitBlock(block) { + if m.submitBlock(block, isBlake3PowActive) { m.Lock() - m.discretePrevHash = shallowBlockCopy.Header.PrevBlock + m.discretePrevHash = shallowBlockHdr.PrevBlock m.discreteBlockHash = *block.Hash() m.Unlock() blockHashes = append(blockHashes, block.Hash()) diff --git a/internal/mining/mining.go b/internal/mining/mining.go index 73496a516a..8e313938ea 100644 --- a/internal/mining/mining.go +++ b/internal/mining/mining.go @@ -1,5 +1,5 @@ // Copyright (c) 2014-2016 The btcsuite developers -// Copyright (c) 2015-2022 The Decred developers +// Copyright (c) 2015-2023 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -1474,7 +1474,7 @@ mempoolLoop: bestHeaderBytes, err := bestHeader.Bytes() if err != nil { str := fmt.Sprintf("failed to serialize header for block %v: %v", - bestHeader.BlockHash(), err) + best.Hash, err) return nil, makeError(ErrSerializeHeader, str) } diff --git a/internal/mining/mining_test.go b/internal/mining/mining_test.go index acf96605a2..e4e0ee051f 100644 --- a/internal/mining/mining_test.go +++ b/internal/mining/mining_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2022 The Decred developers +// Copyright (c) 2020-2023 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -315,15 +315,15 @@ func TestNewBlockTemplateAutoRevocations(t *testing.T) { vote.MsgTx().TxIn[0].ValueIn = 0 // Get the best header bytes. - bestHeader, err := harness.chain.HeaderByHash(&harness.chain.bestState.Hash) + bestStateHash := harness.chain.bestState.Hash + bestHeader, err := harness.chain.HeaderByHash(&bestStateHash) if err != nil { - t.Fatalf("unable to get tip block header %v: %v", - harness.chain.bestState.Hash, err) + t.Fatalf("unable to get tip block header %v: %v", bestStateHash, err) } bestHeaderBytes, err := bestHeader.Bytes() if err != nil { - t.Fatalf("failed to serialize header for block %v: %v", - bestHeader.BlockHash(), err) + t.Fatalf("failed to serialize header for block %v: %v", bestStateHash, + err) } // Add revocations for tickets that were previously missed. diff --git a/internal/rpcserver/interface.go b/internal/rpcserver/interface.go index 109b706458..17bafa419b 100644 --- a/internal/rpcserver/interface.go +++ b/internal/rpcserver/interface.go @@ -420,6 +420,11 @@ type Chain interface { // block with the most cumulative proof of work that is valid becomes the // tip of the main chain. ReconsiderBlock(*chainhash.Hash) error + + // IsBlake3PowAgendaActive returns whether or not the agenda to change the + // proof of work hash function to blake3, as defined in DCP0011, has passed + // and is now active for the block AFTER the given block. + IsBlake3PowAgendaActive(*chainhash.Hash) (bool, 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 d126ea5989..9fa34fd4dc 100644 --- a/internal/rpcserver/rpcserver.go +++ b/internal/rpcserver/rpcserver.go @@ -93,6 +93,17 @@ const ( getworkDataLenBlake256 = (1 + ((wire.MaxBlockHeaderPayload*8 + 65) / (blake256.BlockSize * 8))) * blake256.BlockSize + // blake3BlkSize is the internal block size of the blake3 hashing algorithm. + blake3BlkSize = 64 + + // getworkDataLenBlake3 is the length of the data field of the getwork RPC + // when providing work for blake3. It consists of the serialized block + // header plus the internal blake3 padding. The internal blake3 padding + // consists of enough zeros to pad the message out to a multiple of the + // blake3 block size (64 bytes). + getworkDataLenBlake3 = ((wire.MaxBlockHeaderPayload + (blake3BlkSize - 1)) / + blake3BlkSize) * blake3BlkSize + // getworkExpirationDiff is the number of blocks below the current // best block in height to begin pruning out old block work from // the template pool. @@ -123,6 +134,10 @@ var ( // calculation. blake256Pad []byte + // blake3Pad is the extra blake3 internal padding needed for the data of the + // getwork RPC. It consists of all zeros. + blake3Pad = make([]byte, getworkDataLenBlake3-wire.MaxBlockHeaderPayload) + // JSON 2.0 batched request prefix batchedRequestPrefix = []byte("[") @@ -1756,7 +1771,7 @@ func handleGenerate(ctx context.Context, s *Server, cmd interface{}) (interface{ c := cmd.(*types.GenerateCmd) - // Respond with an error if the client is requesting 0 blocks to be generated. + // Respond with an error when no blocks are requested. if c.NumBlocks == 0 { return nil, rpcInternalError("Invalid number of blocks", "Configuration") @@ -3696,8 +3711,15 @@ func getWorkTemplateKey(header *wire.BlockHeader) [merkleRootPairSize]byte { // It consists of the serialized block header along with any internal padding // that makes the data ready for callers to make use of only the final chunk // along with the midstate for the rest when solving the block. -func serializeGetWorkData(header *wire.BlockHeader) ([]byte, error) { - const getworkDataLen = getworkDataLenBlake256 +func serializeGetWorkData(header *wire.BlockHeader, isBlake3PowActive bool) ([]byte, error) { + // Choose the full buffer data length as well as internal padding to apply + // based on the result of the vote for the blake3 proof of work agenda. + getworkDataLen := getworkDataLenBlake256 + internalHashFuncPad := blake256Pad + if isBlake3PowActive { + getworkDataLen = getworkDataLenBlake3 + internalHashFuncPad = blake3Pad + } // Serialize the block header into a buffer large enough to hold the block // header and any internal padding that is added and returned as part of the @@ -3720,7 +3742,7 @@ func serializeGetWorkData(header *wire.BlockHeader) ([]byte, error) { // padding for the hash function. This makes the data ready for callers to // make use of only the final chunk along with the midstate for the rest. data = data[:getworkDataLen] - copy(data[wire.MaxBlockHeaderPayload:], blake256Pad) + copy(data[wire.MaxBlockHeaderPayload:], internalHashFuncPad) return data, nil } @@ -3824,7 +3846,11 @@ func handleGetWorkRequest(ctx context.Context, s *Server) (interface{}, error) { // internal padding that makes the data ready for callers to make use of // only the final chunk along with the midstate for the rest when solving // the block. - data, err := serializeGetWorkData(&headerCopy) + isBlake3PowActive, err := s.isBlake3PowAgendaActive(&headerCopy.PrevBlock) + if err != nil { + return nil, err + } + data, err := serializeGetWorkData(&headerCopy, isBlake3PowActive) if err != nil { return nil, err } @@ -3849,10 +3875,43 @@ func handleGetWorkRequest(ctx context.Context, s *Server) (interface{}, error) { return reply, nil } +// minInt is a helper function to return the minimum of two ints. This avoids +// the need to cast to floats. +func minInt(a, b int) int { + if a < b { + return a + } + return b +} + +// maxInt is a helper function to return the maximum of two ints. This avoids +// the need to cast to floats. +func maxInt(a, b int) int { + if a > b { + return a + } + return b +} + // handleGetWorkSubmission is a helper for handleGetWork which deals with // the calling submitting work to be verified and processed. func handleGetWorkSubmission(_ context.Context, s *Server, hexData string) (interface{}, error) { // Ensure the provided data is sane. + minDataLen := minInt(getworkDataLenBlake256, getworkDataLenBlake3) + maxDataLen := maxInt(getworkDataLenBlake256, getworkDataLenBlake3) + paddedHexDataLen := len(hexData) + len(hexData)%2 + if paddedHexDataLen < minDataLen*2 || paddedHexDataLen > maxDataLen*2 { + if minDataLen == maxDataLen { + return nil, rpcInvalidError("Argument must be a hexadecimal string "+ + "with length %d (not %d)", minDataLen*2, len(hexData)) + } + return nil, rpcInvalidError("Argument must be a hexadecimal string "+ + "with a length between %d and %d (not %d)", minDataLen*2, + maxDataLen*2, len(hexData)) + } + + // Decode the provided hex data while padding the front with a 0 for + // odd-length strings since the decoder requires even-length strings. if len(hexData)%2 != 0 { hexData = "0" + hexData } @@ -3860,10 +3919,6 @@ func handleGetWorkSubmission(_ context.Context, s *Server, hexData string) (inte if err != nil { return false, rpcDecodeHexError(hexData) } - if len(data) != getworkDataLenBlake256 { - return nil, rpcInvalidError("Argument must be %d bytes (not %d)", - getworkDataLenBlake256, len(data)) - } // Deserialize the block header from the data. var submittedHeader wire.BlockHeader @@ -3873,9 +3928,30 @@ func handleGetWorkSubmission(_ context.Context, s *Server, hexData string) (inte return false, rpcInvalidError("Invalid block header: %v", err) } + // Reject orphan blocks. This is done here to provide nicer feedback about + // why the block was rejected since attempting to determine the state of a + // voting agenda requires all previous blocks to be known. + prevBlkHash := &submittedHeader.PrevBlock + if _, err := s.cfg.Chain.HeaderByHash(prevBlkHash); err != nil { + log.Infof("Block submitted via getwork rejected: orphan building on "+ + "parent %v", prevBlkHash) + return false, nil + } + + // Choose the proof of work mining algorithm based on the result of the vote + // for the blake3 proof of work agenda. + isBlake3PowActive, err := s.isBlake3PowAgendaActive(prevBlkHash) + if err != nil { + return false, err + } + powHashFn := submittedHeader.PowHashV1 + if isBlake3PowActive { + powHashFn = submittedHeader.PowHashV2 + } + // Ensure the submitted proof of work hash is less than the target // difficulty. - powHash := submittedHeader.PowHashV1() + powHash := powHashFn() err = standalone.CheckProofOfWork(&powHash, submittedHeader.Bits, s.cfg.ChainParams.PowLimit) if err != nil { @@ -3917,12 +3993,6 @@ func handleGetWorkSubmission(_ context.Context, s *Server, hexData string) (inte // nodes. This will in turn relay it to the network like normal. err = s.cfg.SyncMgr.SubmitBlock(block) if err != nil { - if errors.Is(err, blockchain.ErrMissingParent) { - log.Infof("Block submitted via getwork rejected: orphan building "+ - "on parent %v", block.MsgBlock().Header.PrevBlock) - return false, nil - } - // Anything other than a rule violation is an unexpected error, // so return that error as an internal error. var rErr blockchain.RuleError @@ -5001,6 +5071,20 @@ func (s *Server) isSubsidySplitAgendaActive(prevBlkHash *chainhash.Hash) (bool, return isSubsidySplitEnabled, nil } +// isBlake3PowAgendaActive returns whether or not the agenda to change the proof +// of work hash function to blake3 is active or not for the block AFTER the +// provided block hash. +func (s *Server) isBlake3PowAgendaActive(prevBlkHash *chainhash.Hash) (bool, error) { + chain := s.cfg.Chain + isActive, err := chain.IsBlake3PowAgendaActive(prevBlkHash) + if err != nil { + context := fmt.Sprintf("Could not obtain blake3 proof of work "+ + "agenda status for block %s", prevBlkHash) + return false, rpcInternalError(err.Error(), context) + } + return isActive, nil +} + // isSubsidySplitR2AgendaActive returns if the modified subsidy split round 2 // agenda is active or not for the block AFTER the provided block hash. func (s *Server) isSubsidySplitR2AgendaActive(prevBlkHash *chainhash.Hash) (bool, error) { diff --git a/internal/rpcserver/rpcserverhandlers_test.go b/internal/rpcserver/rpcserverhandlers_test.go index aeadb14afb..7e7ac83068 100644 --- a/internal/rpcserver/rpcserverhandlers_test.go +++ b/internal/rpcserver/rpcserverhandlers_test.go @@ -197,6 +197,8 @@ type testRPCChain struct { treasuryActiveErr error subsidySplitActive bool subsidySplitActiveErr error + blake3PowActive bool + blake3PowActiveErr error subsidySplitR2Active bool subsidySplitR2ActiveErr error } @@ -438,6 +440,12 @@ func (c *testRPCChain) IsSubsidySplitAgendaActive(*chainhash.Hash) (bool, error) return c.subsidySplitActive, c.subsidySplitActiveErr } +// IsBlake3PowAgendaActive returns a mocked bool representing whether or not the +// blake3 proof of work agenda is active. +func (c *testRPCChain) IsBlake3PowAgendaActive(*chainhash.Hash) (bool, error) { + return c.blake3PowActive, c.blake3PowActiveErr +} + // IsSubsidySplitR2AgendaActive returns a mocked bool representing whether or // not the modified subsidy split round 2 agenda is active. func (c *testRPCChain) IsSubsidySplitR2AgendaActive(*chainhash.Hash) (bool, error) { @@ -5661,7 +5669,8 @@ func TestHandleGetWork(t *testing.T) { mockPowLimitBits := standalone.BigToCompact(mockPowLimitBig) serializeGetWorkDataBlake256 := func(header *wire.BlockHeader) []byte { - data, err := serializeGetWorkData(header) + const isBlake3PowActive = false + data, err := serializeGetWorkData(header, isBlake3PowActive) if err != nil { t.Fatalf("unexpected serialize error: %v", err) } @@ -5678,7 +5687,7 @@ func TestHandleGetWork(t *testing.T) { var buf bytes.Buffer buf.Write(submissionB[:10]) buf.WriteRune('g') - buf.Write(submissionB[10:]) + buf.Write(submissionB[10 : len(submissionB)-2]) invalidHexSub := buf.String() buf.Reset() @@ -5687,6 +5696,23 @@ func TestHandleGetWork(t *testing.T) { buf.Write(submissionB[240:]) invalidPOWSub := buf.String() + // Create a mock block solved by blake3 based on the existing test block. + solvedBlake3Block := func() *wire.MsgBlock { + isSolved := func(header *wire.BlockHeader) bool { + powHash := header.PowHashV2() + err := standalone.CheckProofOfWork(&powHash, header.Bits, + mockPowLimitBig) + return err == nil + } + blk := block432100 + header := &blk.Header + header.Bits = mockPowLimitBits + for !isSolved(header) { + header.Nonce++ + } + return &blk + }() + testRPCServerHandler(t, []rpcTest{{ name: "handleGetWork: CPU IsMining enabled", handler: handleGetWork, @@ -5851,10 +5877,10 @@ func TestHandleGetWork(t *testing.T) { return params }(), mockMiningState: defaultMockMiningState(), - mockSyncManager: func() *testSyncManager { - syncManager := defaultMockSyncManager() - syncManager.submitBlockErr = blockchain.ErrMissingParent - return syncManager + mockChain: func() *testRPCChain { + chain := defaultMockRPCChain() + chain.headerByHashErr = blockchain.ErrUnknownBlock + return chain }(), result: false, }, { @@ -5879,6 +5905,66 @@ func TestHandleGetWork(t *testing.T) { }, mockMiningState: defaultMockMiningState(), result: true, + }, { + name: "handleGetWork: blake3 submission ok", + handler: handleGetWork, + cmd: &types.GetWorkCmd{ + Data: func() *string { + const isBlake3PowActive = true + data, err := serializeGetWorkData(&solvedBlake3Block.Header, + isBlake3PowActive) + if err != nil { + t.Fatalf("unexpected serialize error: %v", err) + } + encoded := hex.EncodeToString(data) + return &encoded + }(), + }, + mockChainParams: func() *chaincfg.Params { + params := cloneParams(defaultChainParams) + params.PowLimit = mockPowLimitBig + params.PowLimitBits = mockPowLimitBits + return params + }(), + mockMiningState: func() *testMiningState { + mockMiningState := defaultMockMiningState() + tmplKey := getWorkTemplateKey(&solvedBlake3Block.Header) + workState := newWorkState() + workState.templatePool[tmplKey] = solvedBlake3Block + workState.prevBestHash = &solvedBlake3Block.Header.PrevBlock + mockMiningState.workState = workState + return mockMiningState + }(), + mockChain: func() *testRPCChain { + chain := defaultMockRPCChain() + chain.blake3PowActive = true + return chain + }(), + result: true, + }, { + name: "handleGetWork: submission hex too short", + handler: handleGetWork, + cmd: &types.GetWorkCmd{ + Data: func() *string { + submissionHex := string(submissionB[:getworkDataLenBlake256-1]) + return &submissionHex + }(), + }, + mockMiningState: defaultMockMiningState(), + wantErr: true, + errCode: dcrjson.ErrRPCInvalidParameter, + }, { + name: "handleGetWork: submission hex too long", + handler: handleGetWork, + cmd: &types.GetWorkCmd{ + Data: func() *string { + submissionHex := string(submissionB) + "a" + return &submissionHex + }(), + }, + mockMiningState: defaultMockMiningState(), + wantErr: true, + errCode: dcrjson.ErrRPCInvalidParameter, }, { name: "handleGetWork: invalid submission hex", handler: handleGetWork, @@ -5888,6 +5974,20 @@ func TestHandleGetWork(t *testing.T) { mockMiningState: defaultMockMiningState(), wantErr: true, errCode: dcrjson.ErrRPCDecodeHexString, + }, { + name: "handleGetWork: unable to obtain blake3 pow agenda status", + handler: handleGetWork, + cmd: &types.GetWorkCmd{ + Data: &submission, + }, + mockMiningState: defaultMockMiningState(), + mockChain: func() *testRPCChain { + chain := defaultMockRPCChain() + chain.blake3PowActiveErr = blockchain.ErrUnknownBlock + return chain + }(), + wantErr: true, + errCode: dcrjson.ErrRPCInternal.Code, }, { name: "handleGetWork: invalid proof of work", handler: handleGetWork, diff --git a/internal/rpcserver/rpcwebsocket.go b/internal/rpcserver/rpcwebsocket.go index f3aa05b057..229dc81a97 100644 --- a/internal/rpcserver/rpcwebsocket.go +++ b/internal/rpcserver/rpcwebsocket.go @@ -839,7 +839,12 @@ func (m *wsNotificationManager) notifyWork(clients map[chan struct{}]*wsClient, // only the final chunk along with the midstate for the rest when solving // the block. header := &templateNtfn.Template.Block.Header - data, err := serializeGetWorkData(header) + isBlake3PowActive, err := m.server.isBlake3PowAgendaActive(&header.PrevBlock) + if err != nil { + log.Errorf("Could not obtain blake3 agenda status: %v", err) + return + } + data, err := serializeGetWorkData(header, isBlake3PowActive) if err != nil { log.Errorf("Failed to serialize data: %v", err) return diff --git a/server.go b/server.go index a3a8ea6743..c475ab682e 100644 --- a/server.go +++ b/server.go @@ -3679,6 +3679,7 @@ func newServer(ctx context.Context, listenAddrs []string, db database.DB, ConnectedCount: s.ConnectedCount, IsCurrent: s.syncManager.IsCurrent, IsKnownInvalidBlock: s.chain.IsKnownInvalidBlock, + IsBlake3PowAgendaActive: s.chain.IsBlake3PowAgendaActive, }) }