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..d292633c26 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 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 e5ea6e08df..abfd813975 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, }) }