Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,058 changes: 1,058 additions & 0 deletions internal/blockchain/agendas.go

Large diffs are not rendered by default.

553 changes: 552 additions & 1 deletion internal/blockchain/agendas_test.go

Large diffs are not rendered by default.

322 changes: 34 additions & 288 deletions internal/blockchain/chain.go

Large diffs are not rendered by default.

234 changes: 1 addition & 233 deletions internal/blockchain/chain_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright (c) 2013-2016 The btcsuite developers
// Copyright (c) 2015-2025 The Decred developers
// Copyright (c) 2015-2026 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

Expand All @@ -11,7 +11,6 @@ import (
"encoding/gob"
"errors"
"fmt"
"math"
"os"
"path/filepath"
"reflect"
Expand Down Expand Up @@ -48,237 +47,6 @@ func cloneParams(params *chaincfg.Params) *chaincfg.Params {
return &paramsCopy
}

// TestDeploymentParamsValidation ensures that the code related to validating
// the deployments in the provided chain parameters works as intended.
func TestDeploymentParamsValidation(t *testing.T) {
// Create parameters with the deployments replaced with known mocked
// values for use as a base to mutate in the tests below.
mockParams := cloneParams(chaincfg.RegNetParams())
mockParams.Deployments = map[uint32][]chaincfg.ConsensusDeployment{
0: {{
Vote: chaincfg.Vote{
Id: "testvote",
Mask: 0x6,
Choices: []chaincfg.Choice{{
Id: "abstain",
Description: "abstain voting for change",
Bits: 0x0000,
IsAbstain: true,
IsNo: false,
}, {
Id: "no",
Description: "keep the existing rules",
Bits: 0x0002, // Bit 1
IsAbstain: false,
IsNo: true,
}, {
Id: "yes",
Description: "change to the new rules",
Bits: 0x0004, // Bit 2
IsAbstain: false,
IsNo: false,
}},
},
StartTime: 0, // Always available for vote
ExpireTime: math.MaxInt64, // Never expires
}},
}

tests := []struct {
name string // test description
munger func(*chaincfg.Params) // func to mutate params
err error // expected error
}{{
name: "reject duplicate deployment id in the same version",
munger: func(params *chaincfg.Params) {
// Duplicate the vote in deployment version 0 so there is a
// duplicate id in the same version.
votes := params.Deployments[0]
params.Deployments[0] = append(votes, votes[0])
},
err: ErrDuplicateDeployment,
}, {
name: "reject duplicate deployment id in different versions",
munger: func(params *chaincfg.Params) {
// Duplicate the votes in deployment version 0 to deployment version
// 1 so there is a duplicate id in different versions.
params.Deployments[1] = params.Deployments[0]
},
err: ErrDuplicateDeployment,
}, {
name: "require optional forced choice to exist when specified",
munger: func(params *chaincfg.Params) {
deployment := &params.Deployments[0][0]
deployment.ForcedChoiceID = "bogus"
},
err: ErrUnknownDeploymentChoice,
}, {
name: "require optional forced choice to be non abstain when specified",
munger: func(params *chaincfg.Params) {
deployment := &params.Deployments[0][0]
deployment.ForcedChoiceID = "abstain"
},
err: ErrDeploymentChoiceAbstain,
}, {
name: "require distinct masks for all votes in same deployment version",
munger: func(params *chaincfg.Params) {
// Create another deployment with a mask that uses a bit that is
// already used in the existing one.
deployment := chaincfg.ConsensusDeployment{
Vote: chaincfg.Vote{
Id: "testvote2",
Mask: 0xc,
Choices: []chaincfg.Choice{{
Id: "abstain",
Description: "abstain voting for change",
Bits: 0x0000,
IsAbstain: true,
IsNo: false,
}, {
Id: "no",
Description: "keep the existing rules",
Bits: 0x0004, // Bit 2
IsAbstain: false,
IsNo: true,
}, {
Id: "yes",
Description: "change to the new rules",
Bits: 0x0008, // Bit 3
IsAbstain: false,
IsNo: false,
}},
},
StartTime: 0, // Always available for vote
ExpireTime: math.MaxInt64, // Never expires
}
params.Deployments[0] = append(params.Deployments[0], deployment)
},
err: ErrDeploymentBadMask,
}, {
name: "require non-zero mask",
munger: func(params *chaincfg.Params) {
vote := &params.Deployments[0][0].Vote
vote.Mask = 0
},
err: ErrDeploymentBadMask,
}, {
name: "reject mask with reserved parent regular tx tree approval bit",
munger: func(params *chaincfg.Params) {
vote := &params.Deployments[0][0].Vote
vote.Mask |= dcrutil.BlockValid
},
err: ErrDeploymentBadMask,
}, {
name: "require consecutive mask",
munger: func(params *chaincfg.Params) {
vote := &params.Deployments[0][0].Vote
vote.Mask = 0xa // 0b1010 -- non-consecutive mask
},
err: ErrDeploymentBadMask,
}, {
name: "reject too many choices",
munger: func(params *chaincfg.Params) {
vote := &params.Deployments[0][0].Vote
vote.Choices = append(vote.Choices, chaincfg.Choice{
Id: "maybe",
Bits: 0x0006,
})
vote.Choices = append(vote.Choices, chaincfg.Choice{
Id: "another",
Bits: 0x0006, // invalid bits too
})
},
err: ErrDeploymentTooManyChoices,
}, {
name: "reject choices without an id",
munger: func(params *chaincfg.Params) {
vote := &params.Deployments[0][0].Vote
vote.Choices[1].Id = ""
},
err: ErrDeploymentMissingChoiceID,
}, {
name: "reject choice with abstain bits but no abstain flag",
munger: func(params *chaincfg.Params) {
// Mark the abstain choice as not being abstain.
vote := &params.Deployments[0][0].Vote
vote.Choices[0].IsAbstain = false
},
err: ErrDeploymentBadChoiceBits,
}, {
name: "reject choice with bits that do not conform to the mask",
munger: func(params *chaincfg.Params) {
// Assign invalid bits per the mask to a choice.
vote := &params.Deployments[0][0].Vote
vote.Choices[2].Bits = 0xc // 0b1100
},
err: ErrDeploymentBadChoiceBits,
}, {
name: "require exclusive abstain and no flags",
munger: func(params *chaincfg.Params) {
// Mark a choice with both flags.
vote := &params.Deployments[0][0].Vote
vote.Choices[0].IsNo = true
},
err: ErrDeploymentNonExclusiveFlags,
}, {
name: "require unique choice ids",
munger: func(params *chaincfg.Params) {
// Assign duplicate id.
vote := &params.Deployments[0][0].Vote
vote.Choices[2].Id = vote.Choices[1].Id
},
err: ErrDeploymentDuplicateChoice,
}, {
name: "require the abstain choice",
munger: func(params *chaincfg.Params) {
// Remove the abstain choice.
vote := &params.Deployments[0][0].Vote
vote.Choices = vote.Choices[1:]
},
err: ErrDeploymentMissingAbstain,
}, {
name: "reject more than one abstain choice",
munger: func(params *chaincfg.Params) {
// Mark a second choice as abstain.
vote := &params.Deployments[0][0].Vote
vote.Choices[1].IsAbstain = true
vote.Choices[1].IsNo = false
},
err: ErrDeploymentTooManyAbstain,
}, {
name: "require a no choice",
munger: func(params *chaincfg.Params) {
// Remove the no choice.
vote := &params.Deployments[0][0].Vote
vote.Choices = append(vote.Choices[:1], vote.Choices[2:]...)
},
err: ErrDeploymentMissingNo,
}, {
name: "reject more than one no choice",
munger: func(params *chaincfg.Params) {
// Mark a second choice as no.
vote := &params.Deployments[0][0].Vote
vote.Choices[2].IsNo = true
},
err: ErrDeploymentTooManyNo,
}}

for _, test := range tests {
// Clone the mocked parameters so they can be mutated by each test
// without affecting the others and then mutate them per the test.
params := cloneParams(mockParams)
if test.munger != nil {
test.munger(params)
}

_, err := extractDeployments(params)
if !errors.Is(err, test.err) {
t.Fatalf("%q: unexpected err -- got %v, want %v", test.name, err,
test.err)
}
}
}

// TestBlockchainFunction tests the various blockchain API to ensure proper
// functionality.
func TestBlockchainFunctions(t *testing.T) {
Expand Down
5 changes: 4 additions & 1 deletion internal/blockchain/chainio.go
Original file line number Diff line number Diff line change
Expand Up @@ -1746,7 +1746,10 @@ func (b *BlockChain) initChainState(ctx context.Context,
numTxns := uint64(len(block.Transactions))

// Calculate the next stake difficulty.
nextStakeDiff := b.calcNextRequiredStakeDifficulty(tip)
nextStakeDiff, err := b.calcNextRequiredStakeDifficulty(tip)
if err != nil {
return err
}

// Attempt to discover and set the old fork rejection checkpoint node.
b.maybeSetForkRejectionCheckpoint()
Expand Down
61 changes: 47 additions & 14 deletions internal/blockchain/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"math"
"math/bits"
"reflect"
"slices"
"testing"
"time"

Expand Down Expand Up @@ -176,14 +177,15 @@ func newFakeChain(params *chaincfg.Params) *BlockChain {
index.bestHeader = node
index.AddNode(node)

// Generate a deployment ID map from the provided params.
deploymentData, err := extractDeployments(params)
// Make agendas from the provided params while validating the deployments
// conform to the required semantics.
agendas, err := makeAgendas(params, makeHistoricalAgendas())
if err != nil {
panic(err)
}

return &BlockChain{
deploymentData: deploymentData,
agendas: agendas,
chainParams: params,
index: index,
bestChain: newChainView(node),
Expand Down Expand Up @@ -508,6 +510,27 @@ func findDeploymentAllYesChoices(t *testing.T, params *chaincfg.Params, voteIDs
panic("unreachable")
}

// removeDeployment removes the deployment associated with the provided vote ID
// from the deployments of the provided parameters or fails with a fatal test
// error when not found.
func removeDeployment(t *testing.T, params *chaincfg.Params, voteID string) {
t.Helper()

// Find the correct deployment for the passed vote ID.
for version, deployments := range params.Deployments {
for i, deployment := range deployments {
if deployment.Vote.Id == voteID {
s := slices.Delete(deployments, i, i+1)
params.Deployments[version] = s
return
}
}
}

t.Fatalf("unable to find deployment for id %q", voteID)
panic("unreachable")
}

// reassignVoteMaskAndChoiceBits calculates and updates the masks and choice
// bits for all votes in the provided agendas so that they are non-overlapping.
func reassignVoteMaskAndChoiceBits(agendas []chaincfg.ConsensusDeployment) {
Expand Down Expand Up @@ -544,6 +567,23 @@ func forceDeploymentResult(t *testing.T, params *chaincfg.Params, voteID, choice
deployment.ForcedChoiceID = choiceID
}

// removeHistoricalConsensusChange modifies the passed chain to remove any
// historical consensus change information for the given vote ID when it exists.
func removeHistoricalConsensusChange(t *testing.T, chain *BlockChain, agendaID string) {
t.Helper()

historicalAgendas := makeHistoricalAgendas()
if ha, ok := historicalAgendas[chain.chainParams.Net]; ok {
delete(ha, agendaID)
}
agendas, err := makeAgendas(chain.chainParams, historicalAgendas)
if err != nil {
t.Fatalf("failed to make agendas without historical consensus change "+
"for agenda %q: %v", agendaID, err)
}
chain.agendas = agendas
}

// chaingenHarness provides a test harness which encapsulates a test instance, a
// chaingen generator instance, and a block chain instance to provide all of the
// functionality of the aforementioned types as well as several convenience
Expand Down Expand Up @@ -988,7 +1028,7 @@ func (g *chaingenHarness) TestThresholdState(id string, state ThresholdState) {
// TestThresholdStateChoice queries the threshold state from the current tip
// block associated with the harness generator and expects the returned state
// and choice to match the provided value.
func (g *chaingenHarness) TestThresholdStateChoice(id string, state ThresholdState, choice *chaincfg.Choice) {
func (g *chaingenHarness) TestThresholdStateChoice(id string, state ThresholdState, choiceID string) {
g.t.Helper()

tipHash := g.Tip().BlockHash()
Expand All @@ -1005,17 +1045,10 @@ func (g *chaingenHarness) TestThresholdStateChoice(id string, state ThresholdSta
"state for %s -- got %v, want %v", g.TipName(), tipHash, tipHeight,
id, s.State, state)
}
if !reflect.DeepEqual(s.Choice, choice) {
gotChoiceID, wantChoiceID := "<nil>", "<nil>"
if s.Choice != nil {
gotChoiceID = s.Choice.Id
}
if choice != nil {
wantChoiceID = choice.Id
}
if s.ChoiceID != choiceID {
g.t.Fatalf("block %q (hash %s, height %d) unexpected choice for %s -- "+
"got %v, want %v", g.TipName(), tipHash, tipHeight, id, gotChoiceID,
wantChoiceID)
"got %v, want %v", g.TipName(), tipHash, tipHeight, id, s.ChoiceID,
choiceID)
}
}

Expand Down
Loading