multi: Implement DCP0010 subsidy consensus vote.

This implements the agenda for voting on the modified subsidy split
defined in DCP0010 along with consensus tests.

In particular, once the vote has passed and is active, the PoW subsidy
will be 10% of the block reward and the PoS subsidy will be 80%.  The
Treasury subsidy will remain at 10%.

In terms of the overall effects, this includes updates to:
- The validation logic for votes, coinbases, and overall block subsidy
- Enforcement when considering candidate votes for acceptance to the
  mempool, relaying, and inclusion into block templates
- Mining template generation
- The output of the getblocksubsidy RPC

Also note that this does not implement the block version bump that will
ultimately be needed by the mining code since there are multiple
consensus votes gated behind it and will therefore be done separately.

The following is an overview of the changes:

- Introduce a convenience function for determining if the vote passed
  and is now active
- Add new methods to blockchain/standalone for calculating the work and
  stake vote subsidies according to either the original or modified
  values per a provided flag
- Modify vote validation to enforce the new stake vote subsidy in
  accordance with the state of the vote
- Modify coinbase validation to enforce the new work subsidy in
  accordance with the state of the vote
- Modify block validation logic to enforce the total overall subsidy in
  accordance with the state of the vote
- Add tests for determining if the agenda is active for both mainnet and
  testnet
- Add tests for getblocksubsidy RPC
- Add tests to ensure proper behavior for the modified subsidy splits as
  follows:
  - Ensure new blockchain validation semantics are enforced once the
    agenda is active
  - Ensure mempool correctly accepts and rejects votes in accordance
    with the state of the vote
This commit is contained in:
Dave Collins 2021-12-13 14:05:23 -06:00
parent c4d1094a0e
commit e972ac4e84
No known key found for this signature in database
GPG Key ID: B8904D9D9C93D1F2
16 changed files with 662 additions and 62 deletions

View File

@ -936,3 +936,120 @@ func TestAutoRevocationsDeployment(t *testing.T) {
testAutoRevocationsDeployment(t, chaincfg.MainNetParams())
testAutoRevocationsDeployment(t, chaincfg.RegNetParams())
}
// testSubsidySplitDeployment ensures the deployment of the 10/80/10 subsidy
// split agenda activates for the provided network parameters.
func testSubsidySplitDeployment(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.
params = cloneParams(params)
deploymentVer, deployment, err := findDeployment(params,
chaincfg.VoteIDChangeSubsidySplit)
if err != nil {
t.Fatal(err)
}
yesChoice, err := findDeploymentChoice(deployment, "yes")
if err != nil {
t.Fatal(err)
}
removeDeploymentTimeConstraints(deployment)
// Shorter versions of params for convenience.
stakeValidationHeight := uint32(params.StakeValidationHeight)
ruleChangeActivationInterval := params.RuleChangeActivationInterval
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
}{{
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,
}}
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.isSubsidySplitAgendaActive(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.IsSubsidySplitAgendaActive(&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
}
}
}
// TestSubsidySplitDeployment ensures the deployment of the 10/80/10 subsidy
// split agenda activates as expected.
func TestSubsidySplitDeployment(t *testing.T) {
testSubsidySplitDeployment(t, chaincfg.MainNetParams())
testSubsidySplitDeployment(t, chaincfg.RegNetParams())
}

View File

@ -6,7 +6,7 @@ require (
github.com/decred/dcrd/blockchain/stake/v4 v4.0.0-20211110133211-e53d26e01d1f
github.com/decred/dcrd/blockchain/standalone/v2 v2.0.0
github.com/decred/dcrd/chaincfg/chainhash v1.0.3
github.com/decred/dcrd/chaincfg/v3 v3.1.0
github.com/decred/dcrd/chaincfg/v3 v3.1.1
github.com/decred/dcrd/database/v3 v3.0.0-20211012235250-77033596a107
github.com/decred/dcrd/dcrec v1.0.0
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1
@ -19,6 +19,4 @@ require (
github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca
)
replace (
github.com/decred/dcrd/chaincfg/v3 => ../chaincfg
)
replace github.com/decred/dcrd/blockchain/standalone/v2 => ./standalone

View File

@ -9,14 +9,13 @@ github.com/decred/base58 v1.0.3/go.mod h1:pXP9cXCfM2sFLb2viz2FNIdeMWmZDBKG3ZBYbi
github.com/decred/dcrd/blockchain/stake/v4 v4.0.0-20210906140327-598bf66f24a6/go.mod h1:CStg0VQxxpVWphul8V3BtBOlhkkHfGE3CgwZK00xYwE=
github.com/decred/dcrd/blockchain/stake/v4 v4.0.0-20211110133211-e53d26e01d1f h1:zMUvaP/V+JQEsQ0fT342NKSvSt6zhz5svdEJbIR8nF4=
github.com/decred/dcrd/blockchain/stake/v4 v4.0.0-20211110133211-e53d26e01d1f/go.mod h1:CStg0VQxxpVWphul8V3BtBOlhkkHfGE3CgwZK00xYwE=
github.com/decred/dcrd/blockchain/standalone/v2 v2.0.0 h1:9gUuH0u/IZNPWBK9K3CxgAWPG7nTqVSsZefpGY4Okns=
github.com/decred/dcrd/blockchain/standalone/v2 v2.0.0/go.mod h1:t2qaZ3hNnxHZ5kzVJDgW5sp47/8T5hYJt7SR+/JtRhI=
github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60=
github.com/decred/dcrd/chaincfg/chainhash v1.0.3 h1:PF2czcYZGW3dz4i/35AUfVAgnqHl9TMNQt1ADTYGOoE=
github.com/decred/dcrd/chaincfg/chainhash v1.0.3/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60=
github.com/decred/dcrd/chaincfg/v3 v3.0.0/go.mod h1:EspyubQ7D2w6tjP7rBGDIE7OTbuMgBjR2F2kZFnh31A=
github.com/decred/dcrd/chaincfg/v3 v3.1.0 h1:u8l+E6ryv8E0WY69pM/lUI36UeAVcLKBwD/Q3xPiuog=
github.com/decred/dcrd/chaincfg/v3 v3.1.0/go.mod h1:4XF9nlx2NeGD4xzw1+L0DGICZMl0a5rKV8nnuHLgk8o=
github.com/decred/dcrd/chaincfg/v3 v3.1.1 h1:Ki8kq5IXGmjriiQyPCrCTF1aZSBiORb91/Sr5xW4otw=
github.com/decred/dcrd/chaincfg/v3 v3.1.1/go.mod h1:4XF9nlx2NeGD4xzw1+L0DGICZMl0a5rKV8nnuHLgk8o=
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/crypto/ripemd160 v1.0.1 h1:TjRL4LfftzTjXzaufov96iDAkbY2R3aTvH2YMYa1IOc=

View File

@ -878,6 +878,56 @@ func (b *BlockChain) IsAutoRevocationsAgendaActive(prevHash *chainhash.Hash) (bo
return isActive, err
}
// isSubsidySplitAgendaActive returns whether or not the agenda to change the
// block reward subsidy split to 10/80/10, as defined in DCP0010, 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) isSubsidySplitAgendaActive(prevNode *blockNode) (bool, error) {
const deploymentID = chaincfg.VoteIDChangeSubsidySplit
deploymentVer, ok := b.deploymentVers[deploymentID]
if !ok {
return true, nil
}
state, err := b.deploymentState(prevNode, deploymentVer, deploymentID)
if err != nil {
return false, err
}
// 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.
return state.State == ThresholdActive, nil
}
// IsSubsidySplitAgendaActive returns whether or not the agenda to change the
// block reward subsidy split to 10/80/10, as defined in DCP0010, has passed and
// is now active for the block AFTER the given block.
//
// This function is safe for concurrent access.
func (b *BlockChain) IsSubsidySplitAgendaActive(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.isSubsidySplitAgendaActive(prevNode)
b.chainLock.Unlock()
return isActive, err
}
// VoteCounts is a compacted struct that is used to message vote counts.
type VoteCounts struct {
Total uint32

View File

@ -336,6 +336,12 @@ const (
// being active are applied.
AFAutoRevocationsEnabled
// AFSubsidySplitEnabled may be set to indicate that the modified subsidy
// split agenda defined in DCP00010 should be considered as active when
// checking a transaction so that any additional checks which depend on the
// agenda being active are applied.
AFSubsidySplitEnabled
// AFNone is a convenience value to specifically indicate no flags.
AFNone AgendaFlags = 0
)
@ -358,6 +364,12 @@ func (flags AgendaFlags) IsAutoRevocationsEnabled() bool {
return flags&AFAutoRevocationsEnabled == AFAutoRevocationsEnabled
}
// IsSubsidySplitEnabled returns whether the flags indicate that the modified
// subsidy split agenda defined in DCP0010 is enabled.
func (flags AgendaFlags) IsSubsidySplitEnabled() bool {
return flags&AFSubsidySplitEnabled == AFSubsidySplitEnabled
}
// determineCheckTxFlags returns the flags to use when checking transactions
// based on the agendas that are active as of the block AFTER the given node.
func (b *BlockChain) determineCheckTxFlags(prevNode *blockNode) (AgendaFlags, error) {
@ -381,6 +393,13 @@ func (b *BlockChain) determineCheckTxFlags(prevNode *blockNode) (AgendaFlags, er
return 0, err
}
// Determine if the modified subsidy split agenda is active as of the block
// being checked.
isSubsidySplitEnabled, err := b.isSubsidySplitAgendaActive(prevNode)
if err != nil {
return 0, err
}
// Create and return agenda flags for checking transactions based on which
// ones are active as of the block being checked.
checkTxFlags := AFNone
@ -393,6 +412,9 @@ func (b *BlockChain) determineCheckTxFlags(prevNode *blockNode) (AgendaFlags, er
if isAutoRevocationsEnabled {
checkTxFlags |= AFAutoRevocationsEnabled
}
if isSubsidySplitEnabled {
checkTxFlags |= AFSubsidySplitEnabled
}
return checkTxFlags, nil
}
@ -2586,7 +2608,7 @@ func checkTicketRedeemerCommitments(ticketHash *chainhash.Hash,
func checkVoteInputs(subsidyCache *standalone.SubsidyCache, tx *dcrutil.Tx,
txHeight int64, view *UtxoViewpoint, params *chaincfg.Params,
prevHeader *wire.BlockHeader, isTreasuryEnabled,
isAutoRevocationsEnabled bool) error {
isAutoRevocationsEnabled, isSubsidySplitEnabled bool) error {
ticketMaturity := int64(params.TicketMaturity)
voteHash := tx.Hash()
@ -2600,7 +2622,8 @@ func checkVoteInputs(subsidyCache *standalone.SubsidyCache, tx *dcrutil.Tx,
// is voting on. Unfortunately, this is now part of consensus, so changing
// it requires a hard fork vote.
_, heightVotingOn := stake.SSGenBlockVotedOn(msgTx)
voteSubsidy := subsidyCache.CalcStakeVoteSubsidy(int64(heightVotingOn))
voteSubsidy := subsidyCache.CalcStakeVoteSubsidyV2(int64(heightVotingOn),
isSubsidySplitEnabled)
// The input amount specified by the stakebase must commit to the subsidy
// generated by the vote.
@ -2818,7 +2841,7 @@ func checkRevocationInputs(tx *dcrutil.Tx, txHeight int64, view *UtxoViewpoint,
func CheckTransactionInputs(subsidyCache *standalone.SubsidyCache,
tx *dcrutil.Tx, txHeight int64, view *UtxoViewpoint, checkFraudProof bool,
chainParams *chaincfg.Params, prevHeader *wire.BlockHeader,
isTreasuryEnabled, isAutoRevocationsEnabled bool) (int64, error) {
isTreasuryEnabled, isAutoRevocationsEnabled, isSubsidySplitEnabled bool) (int64, error) {
// Coinbase transactions have no inputs.
msgTx := tx.MsgTx()
@ -2856,7 +2879,7 @@ func CheckTransactionInputs(subsidyCache *standalone.SubsidyCache,
if isVote {
err := checkVoteInputs(subsidyCache, tx, txHeight, view,
chainParams, prevHeader, isTreasuryEnabled,
isAutoRevocationsEnabled)
isAutoRevocationsEnabled, isSubsidySplitEnabled)
if err != nil {
return 0, err
}
@ -2919,8 +2942,8 @@ func CheckTransactionInputs(subsidyCache *standalone.SubsidyCache,
if isVote && idx == 0 {
// However, do add the reward amount.
_, heightVotingOn := stake.SSGenBlockVotedOn(msgTx)
stakeVoteSubsidy := subsidyCache.CalcStakeVoteSubsidy(
int64(heightVotingOn))
stakeVoteSubsidy := subsidyCache.CalcStakeVoteSubsidyV2(
int64(heightVotingOn), isSubsidySplitEnabled)
totalAtomIn += stakeVoteSubsidy
continue
}
@ -3290,7 +3313,10 @@ func checkNumSigOps(tx *dcrutil.Tx, view *UtxoViewpoint, index int, txTree bool,
// checkStakeBaseAmounts calculates the total amount given as subsidy from
// single stakebase transactions (votes) within a block. This function skips a
// ton of checks already performed by CheckTransactionInputs.
func checkStakeBaseAmounts(subsidyCache *standalone.SubsidyCache, height int64, txs []*dcrutil.Tx, view *UtxoViewpoint, isTreasuryEnabled bool) error {
func checkStakeBaseAmounts(subsidyCache *standalone.SubsidyCache, height int64,
txs []*dcrutil.Tx, view *UtxoViewpoint, isTreasuryEnabled,
isSubsidySplitEnabled bool) error {
for _, tx := range txs {
msgTx := tx.MsgTx()
if stake.IsSSGen(msgTx, isTreasuryEnabled) {
@ -3314,9 +3340,10 @@ func checkStakeBaseAmounts(subsidyCache *standalone.SubsidyCache, height int64,
difference := totalOutputs - originTxAtom
// Subsidy aligns with the height we're voting on, not
// with the height of the current block.
calcSubsidy := subsidyCache.CalcStakeVoteSubsidy(height - 1)
// Subsidy aligns with the height being voting on, not with the
// height of the current block.
calcSubsidy := subsidyCache.CalcStakeVoteSubsidyV2(height-1,
isSubsidySplitEnabled)
if difference > calcSubsidy {
str := fmt.Sprintf("ssgen tx %v spent more "+
@ -3365,7 +3392,10 @@ func getStakeBaseAmounts(txs []*dcrutil.Tx, view *UtxoViewpoint, isTreasuryEnabl
// getStakeTreeFees determines the amount of fees for in the stake tx tree of
// some node given a transaction store.
func getStakeTreeFees(subsidyCache *standalone.SubsidyCache, height int64, txs []*dcrutil.Tx, view *UtxoViewpoint, isTreasuryEnabled bool) (dcrutil.Amount, error) {
func getStakeTreeFees(subsidyCache *standalone.SubsidyCache, height int64,
txs []*dcrutil.Tx, view *UtxoViewpoint, isTreasuryEnabled,
isSubsidySplitEnabled bool) (dcrutil.Amount, error) {
totalInputs := int64(0)
totalOutputs := int64(0)
for _, tx := range txs {
@ -3414,9 +3444,10 @@ func getStakeTreeFees(subsidyCache *standalone.SubsidyCache, height int64, txs [
// For votes, subtract the subsidy to determine actual fees.
if isSSGen {
// Subsidy aligns with the height we're voting on, not
// with the height of the current block.
totalOutputs -= subsidyCache.CalcStakeVoteSubsidy(height - 1)
// Subsidy aligns with the height we're voting on, not with the
// height of the current block.
totalOutputs -= subsidyCache.CalcStakeVoteSubsidyV2(height-1,
isSubsidySplitEnabled)
}
if isTSpend {
@ -3454,6 +3485,13 @@ func (b *BlockChain) checkTransactionsAndConnect(inputFees dcrutil.Amount, node
return err
}
// Determine if the subsidy split change agenda is active as of the block
// being checked.
isSubsidySplitEnabled, err := b.isSubsidySplitAgendaActive(node.parent)
if err != nil {
return err
}
// Perform several checks on the inputs for each transaction. Also
// accumulate the total fees. This could technically be combined with
// the loop above instead of running another loop over the
@ -3479,7 +3517,7 @@ func (b *BlockChain) checkTransactionsAndConnect(inputFees dcrutil.Amount, node
txFee, err := CheckTransactionInputs(b.subsidyCache, tx,
node.height, view, true, /* check fraud proofs */
b.chainParams, &prevHeader, isTreasuryEnabled,
isAutoRevocationsEnabled)
isAutoRevocationsEnabled, isSubsidySplitEnabled)
if err != nil {
log.Tracef("CheckTransactionInputs failed; error "+
"returned: %v", err)
@ -3526,16 +3564,16 @@ func (b *BlockChain) checkTransactionsAndConnect(inputFees dcrutil.Amount, node
if node.height == 1 {
expAtomOut = b.subsidyCache.CalcBlockSubsidy(node.height)
} else {
subsidyWork := b.subsidyCache.CalcWorkSubsidy(node.height,
node.voters)
subsidyTax := b.subsidyCache.CalcTreasurySubsidy(node.height,
subsidyWork := b.subsidyCache.CalcWorkSubsidyV2(node.height,
node.voters, isSubsidySplitEnabled)
subsidyTreasury := b.subsidyCache.CalcTreasurySubsidy(node.height,
node.voters, isTreasuryEnabled)
if isTreasuryEnabled {
// When TreasuryBase is enabled the subsidyTax
// lives in STransactions.
// The treasury payout is done via a treasurybase in the stake
// tree when the treasury agenda is active.
expAtomOut = subsidyWork + totalFees
} else {
expAtomOut = subsidyWork + subsidyTax + totalFees
expAtomOut = subsidyWork + subsidyTreasury + totalFees
}
}
@ -3592,8 +3630,8 @@ func (b *BlockChain) checkTransactionsAndConnect(inputFees dcrutil.Amount, node
return ruleError(ErrNoStakeTx, str)
}
err := checkStakeBaseAmounts(b.subsidyCache, node.height,
txs, view, isTreasuryEnabled)
err := checkStakeBaseAmounts(b.subsidyCache, node.height, txs, view,
isTreasuryEnabled, isSubsidySplitEnabled)
if err != nil {
return err
}
@ -3606,10 +3644,11 @@ func (b *BlockChain) checkTransactionsAndConnect(inputFees dcrutil.Amount, node
var expAtomOut int64
if node.height >= b.chainParams.StakeValidationHeight {
// Subsidy aligns with the height we're voting on, not
// with the height of the current block.
expAtomOut = b.subsidyCache.CalcStakeVoteSubsidy(node.height-1) *
int64(node.voters)
// Subsidy aligns with the height being voting on, not with the
// height of the current block.
voteSubsidy := b.subsidyCache.CalcStakeVoteSubsidyV2(node.height-1,
isSubsidySplitEnabled)
expAtomOut = voteSubsidy * int64(node.voters)
} else {
expAtomOut = totalFees
}
@ -3876,8 +3915,12 @@ func (b *BlockChain) checkConnectBlock(node *blockNode, block, parent *dcrutil.B
return err
}
isSubsidySplitEnabled, err := b.isSubsidySplitAgendaActive(node.parent)
if err != nil {
return err
}
stakeTreeFees, err := getStakeTreeFees(b.subsidyCache, node.height,
block.STransactions(), view, isTreasuryEnabled)
block.STransactions(), view, isTreasuryEnabled, isSubsidySplitEnabled)
if err != nil {
log.Tracef("getStakeTreeFees failed for TxTreeStake: %v", err)
return err

View File

@ -1960,3 +1960,171 @@ func TestAutoRevocations(t *testing.T) {
}
}
}
// TestModifiedSubsidySplitSemantics ensures that the various semantics enforced
// by the modified subsidy split agenda behave as intended.
func TestModifiedSubsidySplitSemantics(t *testing.T) {
// 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 explicit version upgrade 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.VoteIDChangeSubsidySplit
params = cloneParams(params)
deploymentVer, deployment, err := findDeployment(params, voteID)
if err != nil {
t.Fatal(err)
}
removeDeploymentTimeConstraints(deployment)
// Create a test harness initialized with the genesis block as the tip.
g, teardownFunc := newChaingenHarness(t, params, "modifiedsubsidysplittest")
defer teardownFunc()
// replaceCoinbaseSubsidy is a munge function which modifies the provided
// block by replacing the coinbase subsidy with the proportion required for
// the modified subsidy split agenda.
replaceCoinbaseSubsidy := func(b *wire.MsgBlock) {
cache := g.chain.subsidyCache
// Calculate the modified pow subsidy along with the treasury subsidy.
const numVotes = 5
const withDCP0010 = true
height := int64(b.Header.Height)
trsySubsidy := cache.CalcTreasurySubsidy(height, numVotes, noTreasury)
powSubsidy := cache.CalcWorkSubsidyV2(height, numVotes, withDCP0010)
// Update the input value to the the new expected subsidy sum.
coinbaseTx := b.Transactions[0]
coinbaseTx.TxIn[0].ValueIn = trsySubsidy + powSubsidy
// Evenly split the modified pow subsidy over the relevant outputs.
powOutputs := coinbaseTx.TxOut[2:]
numPoWOutputs := int64(len(powOutputs))
amount := powSubsidy / numPoWOutputs
for i := int64(0); i < numPoWOutputs; i++ {
if i == numPoWOutputs-1 {
amount = powSubsidy - amount*(numPoWOutputs-1)
}
powOutputs[i].Value = amount
}
}
// replaceVoteSubsidies is a munge function which modifies the provided
// block by replacing all vote subsidies with the proportion required for
// the modified subsidy split agenda.
replaceVoteSubsidies := func(b *wire.MsgBlock) {
cache := g.chain.subsidyCache
// Calculate the modified vote subsidy and update all of the votes
// accordingly.
const withDCP0010 = true
height := int64(b.Header.Height)
voteSubsidy := cache.CalcStakeVoteSubsidyV2(height, withDCP0010)
chaingen.ReplaceVoteSubsidies(dcrutil.Amount(voteSubsidy))(b)
}
// -------------------------------------------------------------------------
// Generate and accept enough blocks with the appropriate vote bits set to
// reach one block prior to the modified subsidy split agenda becoming
// active.
//
// Note that this also ensures the subsidy split prior to the activation of
// the agenda remains unaffected.
// -------------------------------------------------------------------------
g.AdvanceToStakeValidationHeight()
// -------------------------------------------------------------------------
// Create a block that pays the modified work subsidy prior to activation
// of the modified subsidy split agenda.
//
// The block should be rejected because the agenda is NOT active.
//
// ...
// \-> bsvhbad
// -------------------------------------------------------------------------
tipName := g.TipName()
outs := g.OldestCoinbaseOuts()
g.NextBlock("bsvhbad", &outs[0], outs[1:], replaceCoinbaseSubsidy)
g.RejectTipBlock(ErrBadCoinbaseAmountIn)
// -------------------------------------------------------------------------
// Create a block that pays the modified vote subsidy prior to activation
// of the modified subsidy split agenda.
//
// The block should be rejected because the agenda is NOT active.
//
// ...
// \-> bsvhbad2
// -------------------------------------------------------------------------
g.SetTip(tipName)
g.NextBlock("bsvhbad2", &outs[0], outs[1:], replaceVoteSubsidies)
g.RejectTipBlock(ErrBadStakebaseAmountIn)
// -------------------------------------------------------------------------
// Generate and accept enough blocks with the appropriate vote bits set to
// reach one block prior to the modified subsidy split agenda becoming
// active.
// -------------------------------------------------------------------------
g.SetTip(tipName)
g.AdvanceFromSVHToActiveAgendas(voteID)
// replaceVers is a munge function which modifies the provided block by
// replacing the block, stake, and vote versions with the modified subsidy
// split deployment version.
replaceVers := func(b *wire.MsgBlock) {
chaingen.ReplaceBlockVersion(int32(deploymentVer))(b)
chaingen.ReplaceStakeVersion(deploymentVer)(b)
chaingen.ReplaceVoteVersions(deploymentVer)(b)
}
// -------------------------------------------------------------------------
// Create a block that pays the original vote subsidy that was in effect
// prior to activation of the modified subsidy split agenda.
//
// The block should be rejected because the agenda is active.
//
// ...
// \-> b1bad
// -------------------------------------------------------------------------
tipName = g.TipName()
outs = g.OldestCoinbaseOuts()
g.NextBlock("b1bad", &outs[0], outs[1:], replaceVers, replaceCoinbaseSubsidy)
g.RejectTipBlock(ErrBadStakebaseAmountIn)
// -------------------------------------------------------------------------
// Create a block that pays the original work subsidy that was in effect
// prior to activation of the modified subsidy split agenda.
//
// The block should be rejected because the agenda is active.
//
// ...
// \-> b1bad2
// -------------------------------------------------------------------------
g.SetTip(tipName)
g.NextBlock("b1bad2", &outs[0], outs[1:], replaceVers, replaceVoteSubsidies)
g.RejectTipBlock(ErrBadCoinbaseAmountIn)
// -------------------------------------------------------------------------
// Create a block that pays the modified work and vote subsidies.
//
// The block should be accepted because the agenda is active.
//
// ... -> b1
// -------------------------------------------------------------------------
g.SetTip(tipName)
g.NextBlock("b1", &outs[0], outs[1:], replaceVers, replaceCoinbaseSubsidy,
replaceVoteSubsidies)
g.SaveTipCoinbaseOuts()
g.AcceptTipBlock()
}

2
go.mod
View File

@ -12,7 +12,7 @@ require (
github.com/decred/dcrd/blockchain/v4 v4.0.0-20210129200153-14fd1a785bf2
github.com/decred/dcrd/certgen v1.1.1
github.com/decred/dcrd/chaincfg/chainhash v1.0.3
github.com/decred/dcrd/chaincfg/v3 v3.1.0
github.com/decred/dcrd/chaincfg/v3 v3.1.1
github.com/decred/dcrd/connmgr/v3 v3.0.0
github.com/decred/dcrd/container/apbf v1.0.0
github.com/decred/dcrd/crypto/ripemd160 v1.0.1

View File

@ -151,14 +151,17 @@ type Config struct {
// vote in the mempool.
OnVoteReceived func(voteTx *dcrutil.Tx)
// IsTreasuryAgendaActive returns if the treasury agenda is active or
// not.
// IsTreasuryAgendaActive returns if the treasury agenda is active or not.
IsTreasuryAgendaActive func() (bool, error)
// IsAutoRevocationsAgendaActive returns if the automatic ticket revocations
// agenda is active or not.
IsAutoRevocationsAgendaActive func() (bool, error)
// IsSubsidySplitAgendaActive returns if the modified subsidy split agenda
// is active or not.
IsSubsidySplitAgendaActive func() (bool, error)
// OnTSpendReceived defines the function used to signal receiving a new
// tspend in the mempool.
OnTSpendReceived func(voteTx *dcrutil.Tx)
@ -1280,6 +1283,7 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit,
// Determine active agendas based on flags.
isTreasuryEnabled := checkTxFlags.IsTreasuryEnabled()
isAutoRevocationsEnabled := checkTxFlags.IsAutoRevocationsEnabled()
isSubsidyEnabled := checkTxFlags.IsSubsidySplitEnabled()
// A standalone transaction must not be a coinbase transaction.
if standalone.IsCoinBaseTx(msgTx, isTreasuryEnabled) {
@ -1579,9 +1583,9 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit,
if err != nil {
return nil, err
}
txFee, err := blockchain.CheckTransactionInputs(mp.cfg.SubsidyCache,
tx, nextBlockHeight, utxoView, true, mp.cfg.ChainParams,
&bestHeader, isTreasuryEnabled, isAutoRevocationsEnabled)
txFee, err := blockchain.CheckTransactionInputs(mp.cfg.SubsidyCache, tx,
nextBlockHeight, utxoView, true, mp.cfg.ChainParams, &bestHeader,
isTreasuryEnabled, isAutoRevocationsEnabled, isSubsidyEnabled)
if err != nil {
var cerr blockchain.RuleError
if errors.As(err, &cerr) {
@ -1887,6 +1891,11 @@ func (mp *TxPool) determineCheckTxFlags() (blockchain.AgendaFlags, error) {
return 0, err
}
isSubsidySplitEnabled, err := mp.cfg.IsSubsidySplitAgendaActive()
if err != nil {
return 0, err
}
// Create agenda flags for checking transactions based on which ones are
// active or should otherwise always be enforced.
//
@ -1898,6 +1907,9 @@ func (mp *TxPool) determineCheckTxFlags() (blockchain.AgendaFlags, error) {
if isAutoRevocationsEnabled {
checkTxFlags |= blockchain.AFAutoRevocationsEnabled
}
if isSubsidySplitEnabled {
checkTxFlags |= blockchain.AFSubsidySplitEnabled
}
return checkTxFlags, nil
}

View File

@ -366,6 +366,7 @@ type poolHarness struct {
chainParams *chaincfg.Params
treasuryActive bool
autoRevocationsActive bool
subsidySplitActive bool
chain *fakeChain
txPool *TxPool
@ -624,7 +625,8 @@ func newTxOut(amount int64, pkScriptVer uint16, pkScript []byte) *wire.TxOut {
func (p *poolHarness) CreateVote(ticket *dcrutil.Tx, mungers ...func(*wire.MsgTx)) (*dcrutil.Tx, error) {
// Calculate the vote subsidy.
subsidyCache := p.txPool.cfg.SubsidyCache
subsidy := subsidyCache.CalcStakeVoteSubsidy(p.chain.BestHeight())
subsidy := subsidyCache.CalcStakeVoteSubsidyV2(p.chain.BestHeight(),
p.subsidySplitActive)
// Parse the ticket purchase transaction and generate the vote reward.
ticketPayKinds, ticketHash160s, ticketValues, _, _, _ :=
stake.TxSStxStakeOutputInfo(ticket.MsgTx())
@ -847,6 +849,9 @@ func newPoolHarness(chainParams *chaincfg.Params) (*poolHarness, []spendableOutp
IsAutoRevocationsAgendaActive: func() (bool, error) {
return harness.autoRevocationsActive, nil
},
IsSubsidySplitAgendaActive: func() (bool, error) {
return harness.subsidySplitActive, nil
},
}),
}
@ -3146,3 +3151,108 @@ func TestFraudProofHandling(t *testing.T) {
}
}
}
// TestSubsidySplitSemantics ensures the mempool has the following semantics
// in regards to the modified subsidy split agenda:
//
// - Accepts votes with the original subsidy when the agenda is NOT active
// - Rejects votes with the original subsidy when the agenda is active
// - Accepts votes with the modified subsidy when the agenda is active
// - Rejects votes with the modified subsidy when the agenda is NOT active
func TestSubsidySplitSemantics(t *testing.T) {
t.Parallel()
harness, outputs, err := newPoolHarness(chaincfg.MainNetParams())
if err != nil {
t.Fatalf("unable to create test pool: %v", err)
}
txPool := harness.txPool
tc := &testContext{t, harness}
// Create a regular transaction from the first spendable output provided by
// the harness.
tx, err := harness.CreateTx(outputs[0])
if err != nil {
t.Fatalf("unable to create transaction: %v", err)
}
// Create a ticket purchase transaction spending the outputs of the prior
// regular transaction.
ticket, err := harness.CreateTicketPurchaseFromTx(tx, 40000)
if err != nil {
t.Fatalf("unable to create ticket purchase transaction: %v", err)
}
// Add the ticket outputs as utxos to fake their existence. Use one after
// the stake enabled height for the height of the fake utxos to ensure they
// are matured for the votes cast at stake validation height below.
harness.chain.SetHeight(harness.chainParams.StakeEnabledHeight + 1)
harness.chain.utxos.AddTxOuts(ticket, harness.chain.BestHeight(), 0,
noTreasury, noAutoRevocations)
// Create a vote that votes on a block at stake validation height using the
// proportions required when the modified subsidy split agenda is NOT active.
harness.subsidySplitActive = false
hash := chainhash.Hash{0x5c, 0xa1, 0xab, 0x1e}
mockBlock := dcrutil.NewBlock(&wire.MsgBlock{})
harness.chain.SetBestHash(&hash)
harness.chain.SetHeight(harness.chainParams.StakeValidationHeight)
harness.chain.blocks[hash] = mockBlock
preDCP0010Vote, err := harness.CreateVote(ticket)
if err != nil {
t.Fatalf("unable to create vote: %v", err)
}
// Create another vote that votes on a block at stake validation height
// using the proportions required when the modified subsidy split agenda is
// active.
harness.subsidySplitActive = true
postDCP0010Vote, err := harness.CreateVote(ticket)
if err != nil {
t.Fatalf("unable to create vote: %v", err)
}
// Attempt to add the vote with the modified subsidy when the agenda is NOT
// active and ensure it is rejected. Also, ensure it is not in the orphan
// pool, not in the transaction pool, and not reported as available.
harness.subsidySplitActive = false
_, err = txPool.ProcessTransaction(postDCP0010Vote, false, false, true, 0)
if !errors.Is(err, blockchain.ErrBadStakebaseAmountIn) {
t.Fatal("did not get expected ErrBadStakebaseAmountIn error")
}
testPoolMembership(tc, postDCP0010Vote, false, false)
// Attempt to add the vote with the original subsidy when the agenda is NOT
// active and ensure it is accepted. Also, ensure it is not in the orphan
// pool, is in the transaction pool, and is reported as available.
_, err = txPool.ProcessTransaction(preDCP0010Vote, false, false, true, 0)
if err != nil {
t.Fatalf("failed to accept valid vote %v", err)
}
testPoolMembership(tc, preDCP0010Vote, false, true)
// Remove the vote from the pool and ensure it is not in the orphan pool,
// not in the transaction pool, and not reported as available.
harness.txPool.RemoveTransaction(preDCP0010Vote, true, noTreasury,
noAutoRevocations)
testPoolMembership(tc, preDCP0010Vote, false, false)
// Attempt to add the vote with the original subsidy when the agenda is
// active and ensure it is rejected. Also, ensure it is not in the orphan
// pool, not in the transaction pool, and not reported as available.
harness.subsidySplitActive = true
_, err = txPool.ProcessTransaction(preDCP0010Vote, false, false, true, 0)
if !errors.Is(err, blockchain.ErrBadStakebaseAmountIn) {
t.Fatal("did not get expected ErrBadStakebaseAmountIn error")
}
testPoolMembership(tc, preDCP0010Vote, false, false)
// Attempt to add the vote with the modified subsidy when the agenda is
// active and ensure it is accepted. Also, ensure it is not in the orphan
// pool, is in the transaction pool, and is reported as available.
_, err = txPool.ProcessTransaction(postDCP0010Vote, false, false, true, 0)
if err != nil {
t.Fatalf("failed to accept valid vote %v", err)
}
testPoolMembership(tc, postDCP0010Vote, false, true)
}

View File

@ -108,7 +108,7 @@ type Config struct {
CheckTransactionInputs func(tx *dcrutil.Tx, txHeight int64,
view *blockchain.UtxoViewpoint, checkFraudProof bool,
prevHeader *wire.BlockHeader, isTreasuryEnabled,
isAutoRevocationsEnabled bool) (int64, error)
isAutoRevocationsEnabled, isSubsidyEnabled bool) (int64, error)
// CheckTSpendHasVotes defines the function to use to check whether the given
// tspend has enough votes to be included in a block AFTER the specified block.
@ -177,6 +177,11 @@ type Config struct {
// AFTER the given block.
IsAutoRevocationsAgendaActive func(prevHash *chainhash.Hash) (bool, error)
// IsSubsidySplitAgendaActive defines the function to use to determine if
// the modified subsidy split agenda is active or not for the block AFTER
// the given block.
IsSubsidySplitAgendaActive func(prevHash *chainhash.Hash) (bool, error)
// MaxTreasuryExpenditure defines the function to use to get the maximum amount
// of funds that can be spent from the treasury by a set of TSpends for a block
// that extends the given block hash. The function should return 0 if it is
@ -535,7 +540,11 @@ func calcBlockCommitmentRootV1(block *wire.MsgBlock, prevScripts blockcf2.PrevSc
//
// See the comment for NewBlockTemplate for more information about why the nil
// address handling is useful.
func createCoinbaseTx(subsidyCache *standalone.SubsidyCache, coinbaseScript []byte, opReturnPkScript []byte, nextBlockHeight int64, addr stdaddr.Address, voters uint16, params *chaincfg.Params, isTreasuryEnabled bool) (*dcrutil.Tx, error) {
func createCoinbaseTx(subsidyCache *standalone.SubsidyCache,
coinbaseScript []byte, opReturnPkScript []byte, nextBlockHeight int64,
addr stdaddr.Address, voters uint16, params *chaincfg.Params,
isTreasuryEnabled, isSubsidyEnabled bool) (*dcrutil.Tx, error) {
// Coinbase transactions have no inputs, so previous outpoint is zero hash
// and max index.
coinbaseInput := &wire.TxIn{
@ -618,7 +627,8 @@ func createCoinbaseTx(subsidyCache *standalone.SubsidyCache, coinbaseScript []by
// - Output that includes the block height and potential extra nonce used
// to ensure a unique hash
// - Output that pays the work subsidy to the miner
workSubsidy := subsidyCache.CalcWorkSubsidy(nextBlockHeight, voters)
workSubsidy := subsidyCache.CalcWorkSubsidyV2(nextBlockHeight, voters,
isSubsidyEnabled)
tx := wire.NewMsgTx()
tx.Version = txVersion
tx.AddTxIn(coinbaseInput)
@ -799,7 +809,10 @@ func (g *BlkTmplGenerator) maybeInsertStakeTx(stx *dcrutil.Tx, treeValid bool, i
// work off of is present, it will return a copy of that template to pass to the
// miner.
// Safe for concurrent access.
func (g *BlkTmplGenerator) handleTooFewVoters(nextHeight int64, miningAddress stdaddr.Address, isTreasuryEnabled bool) (*BlockTemplate, error) {
func (g *BlkTmplGenerator) handleTooFewVoters(nextHeight int64,
miningAddress stdaddr.Address, isTreasuryEnabled,
isSubsidyEnabled bool) (*BlockTemplate, error) {
stakeValidationHeight := g.cfg.ChainParams.StakeValidationHeight
// Handle not enough voters being present if we're set to mine aggressively
@ -830,7 +843,8 @@ func (g *BlkTmplGenerator) handleTooFewVoters(nextHeight int64, miningAddress st
}
coinbaseTx, err := createCoinbaseTx(g.cfg.SubsidyCache, coinbaseScript,
opReturnPkScript, topBlock.Height(), miningAddress,
tipHeader.Voters, g.cfg.ChainParams, isTreasuryEnabled)
tipHeader.Voters, g.cfg.ChainParams, isTreasuryEnabled,
isSubsidyEnabled)
if err != nil {
return nil, err
}
@ -1215,6 +1229,11 @@ func (g *BlkTmplGenerator) NewBlockTemplate(payToAddress stdaddr.Address) (*Bloc
return nil, err
}
isSubsidyEnabled, err := g.cfg.IsSubsidySplitAgendaActive(&prevHash)
if err != nil {
return nil, err
}
var (
isTVI bool
maxTreasurySpend int64
@ -1240,7 +1259,7 @@ func (g *BlkTmplGenerator) NewBlockTemplate(payToAddress stdaddr.Address) (*Bloc
log.Debugf("Too few voters found on any HEAD block, " +
"recycling a parent block to mine on")
return g.handleTooFewVoters(nextBlockHeight, payToAddress,
isTreasuryEnabled)
isTreasuryEnabled, isSubsidyEnabled)
}
log.Debugf("Found eligible parent %v with enough votes to build "+
@ -1777,7 +1796,7 @@ nextPriorityQueueItem:
// by the miner.
_, err = g.cfg.CheckTransactionInputs(bundledTx.Tx, nextBlockHeight,
blockUtxos, false, &bestHeader, isTreasuryEnabled,
isAutoRevocationsEnabled)
isAutoRevocationsEnabled, isSubsidyEnabled)
if err != nil {
log.Tracef("Skipping tx %s due to error in "+
"CheckTransactionInputs: %v", bundledTx.Tx.Hash(), err)
@ -2091,7 +2110,7 @@ nextPriorityQueueItem:
coinbaseTx, err := createCoinbaseTx(g.cfg.SubsidyCache, coinbaseScript,
opReturnPkScript, nextBlockHeight, payToAddress, uint16(voters),
g.cfg.ChainParams, isTreasuryEnabled)
g.cfg.ChainParams, isTreasuryEnabled, isSubsidyEnabled)
if err != nil {
return nil, err
}
@ -2204,7 +2223,7 @@ nextPriorityQueueItem:
log.Warnf("incongruent number of voters in mempool " +
"vs mempool.voters; not enough voters found")
return g.handleTooFewVoters(nextBlockHeight, payToAddress,
isTreasuryEnabled)
isTreasuryEnabled, isSubsidyEnabled)
}
// Correct transaction index fraud proofs for any transactions that

View File

@ -72,6 +72,8 @@ type fakeChain struct {
isTreasuryAgendaActiveErr error
isAutoRevocationsAgendaActive bool
isAutoRevocationsAgendaActiveErr error
isSubsidySplitAgendaActive bool
isSubsidySplitAgendaActiveErr error
maxTreasuryExpenditure int64
maxTreasuryExpenditureErr error
parentUtxos *blockchain.UtxoViewpoint
@ -216,6 +218,13 @@ func (c *fakeChain) IsAutoRevocationsAgendaActive(prevHash *chainhash.Hash) (boo
return c.isAutoRevocationsAgendaActive, c.isAutoRevocationsAgendaActiveErr
}
// IsSubsidySplitAgendaActive returns a mocked bool representing whether the
// modified subsidy split agenda is active or not for the block AFTER the given
// block.
func (c *fakeChain) IsSubsidySplitAgendaActive(prevHash *chainhash.Hash) (bool, error) {
return c.isSubsidySplitAgendaActive, c.isSubsidySplitAgendaActiveErr
}
// MaxTreasuryExpenditure returns a mocked maximum amount of funds that can be
// spent from the treasury by a set of TSpends for a block that extends the
// given block hash.
@ -668,6 +677,7 @@ func (p *fakeTxSource) maybeAcceptTransaction(tx *dcrutil.Tx, isNew bool) ([]*ch
nextHeight := height + 1
isTreasuryEnabled := p.chain.isTreasuryAgendaActive
isAutoRevocationsEnabled := p.chain.isAutoRevocationsAgendaActive
isSubsidySplitEnabled := p.chain.isSubsidySplitAgendaActive
// Get the best block and header.
bestHeader, err := p.chain.HeaderByHash(&best.Hash)
@ -741,7 +751,7 @@ func (p *fakeTxSource) maybeAcceptTransaction(tx *dcrutil.Tx, isNew bool) ([]*ch
txFee, err := blockchain.CheckTransactionInputs(p.subsidyCache, tx, nextHeight,
utxoView, false, p.chainParams, &bestHeader, isTreasuryEnabled,
isAutoRevocationsEnabled)
isAutoRevocationsEnabled, isSubsidySplitEnabled)
if err != nil {
return nil, err
}
@ -1214,7 +1224,8 @@ func newVoteScript(voteBits stake.VoteBits) ([]byte, error) {
func (m *miningHarness) CreateVote(ticket *dcrutil.Tx, mungers ...func(*wire.MsgTx)) (*dcrutil.Tx, error) {
// Calculate the vote subsidy.
best := m.chain.BestSnapshot()
subsidy := m.subsidyCache.CalcStakeVoteSubsidy(best.Height)
subsidy := m.subsidyCache.CalcStakeVoteSubsidyV2(best.Height,
m.chain.isSubsidySplitAgendaActive)
// Parse the ticket purchase transaction and generate the vote reward.
ticketPayKinds, ticketHash160s, ticketValues, _, _, _ :=
stake.TxSStxStakeOutputInfo(ticket.MsgTx())
@ -1441,11 +1452,11 @@ func newMiningHarness(chainParams *chaincfg.Params) (*miningHarness, []spendable
CheckTransactionInputs: func(tx *dcrutil.Tx, txHeight int64,
view *blockchain.UtxoViewpoint, checkFraudProof bool,
prevHeader *wire.BlockHeader, isTreasuryEnabled,
isAutoRevocationsEnabled bool) (int64, error) {
isAutoRevocationsEnabled, isSubsidySplitEnabled bool) (int64, error) {
return blockchain.CheckTransactionInputs(subsidyCache, tx, txHeight,
view, checkFraudProof, chainParams, prevHeader, isTreasuryEnabled,
isAutoRevocationsEnabled)
isAutoRevocationsEnabled, isSubsidySplitEnabled)
},
CheckTSpendHasVotes: chain.CheckTSpendHasVotes,
CountSigOps: blockchain.CountSigOps,
@ -1458,6 +1469,7 @@ func newMiningHarness(chainParams *chaincfg.Params) (*miningHarness, []spendable
IsHeaderCommitmentsAgendaActive: chain.IsHeaderCommitmentsAgendaActive,
IsTreasuryAgendaActive: chain.IsTreasuryAgendaActive,
IsAutoRevocationsAgendaActive: chain.IsAutoRevocationsAgendaActive,
IsSubsidySplitAgendaActive: chain.IsSubsidySplitAgendaActive,
MaxTreasuryExpenditure: chain.MaxTreasuryExpenditure,
NewUtxoViewpoint: chain.NewUtxoViewpoint,
TipGeneration: chain.TipGeneration,

View File

@ -397,6 +397,11 @@ type Chain interface {
// active for the block AFTER the given block.
IsAutoRevocationsAgendaActive(*chainhash.Hash) (bool, error)
// IsSubsidySplitAgendaActive returns whether or not the modified subsidy
// split agenda vote, as defined in DCP0010, has passed and is now active
// for the block AFTER the given block.
IsSubsidySplitAgendaActive(*chainhash.Hash) (bool, error)
// FetchTSpend returns all blocks where the treasury spend tx
// identified by the specified hash can be found.
FetchTSpend(chainhash.Hash) ([]chainhash.Hash, error)

View File

@ -2332,11 +2332,16 @@ func handleGetBlockSubsidy(_ context.Context, s *Server, cmd interface{}) (inter
if err != nil {
return nil, err
}
isSubsidyEnabled, err := s.isSubsidySplitAgendaActive(&prevBlkHash)
if err != nil {
return nil, err
}
dev := s.cfg.SubsidyCache.CalcTreasurySubsidy(height, voters,
isTreasuryEnabled)
pos := s.cfg.SubsidyCache.CalcStakeVoteSubsidy(height-1) * int64(voters)
pow := s.cfg.SubsidyCache.CalcWorkSubsidy(height, voters)
subsidyCache := s.cfg.SubsidyCache
dev := subsidyCache.CalcTreasurySubsidy(height, voters, isTreasuryEnabled)
pos := subsidyCache.CalcStakeVoteSubsidyV2(height-1, isSubsidyEnabled) *
int64(voters)
pow := subsidyCache.CalcWorkSubsidyV2(height, voters, isSubsidyEnabled)
total := dev + pos + pow
rep := types.GetBlockSubsidyResult{
@ -5656,6 +5661,19 @@ func (s *Server) isAutoRevocationsAgendaActive(prevBlkHash *chainhash.Hash) (boo
return isAutoRevocationsEnabled, nil
}
// isSubsidySplitAgendaActive returns if the modified subsidy split agenda is
// active or not for the block AFTER the provided block hash.
func (s *Server) isSubsidySplitAgendaActive(prevBlkHash *chainhash.Hash) (bool, error) {
chain := s.cfg.Chain
isSubsidySplitEnabled, err := chain.IsSubsidySplitAgendaActive(prevBlkHash)
if err != nil {
context := fmt.Sprintf("Could not obtain modified subsidy split "+
"agenda status for block %s", prevBlkHash)
return false, rpcInternalError(err.Error(), context)
}
return isSubsidySplitEnabled, nil
}
// httpStatusLine returns a response Status-Line (RFC 2616 Section 6.1) for the
// given request and response status code. This function was lifted and
// adapted from the standard library HTTP server code since it's not exported.

View File

@ -201,6 +201,8 @@ type testRPCChain struct {
tspendVotes tspendVotes
treasuryActive bool
treasuryActiveErr error
subsidySplitActive bool
subsidySplitActiveErr error
}
// BestSnapshot returns a mocked blockchain.BestState.
@ -446,6 +448,12 @@ func (c *testRPCChain) TSpendCountVotes(*chainhash.Hash, *dcrutil.Tx) (int64, in
return c.tspendVotes.yes, c.tspendVotes.no, c.tspendVotes.err
}
// IsSubsidySplitAgendaActive returns a mocked bool representing whether or
// not the modified subsidy split agenda is active.
func (c *testRPCChain) IsSubsidySplitAgendaActive(*chainhash.Hash) (bool, error) {
return c.subsidySplitActive, c.subsidySplitActiveErr
}
// testPeer provides a mock peer by implementing the Peer interface.
type testPeer struct {
addr string
@ -4399,6 +4407,38 @@ func TestHandleGetBlockSubsidy(t *testing.T) {
PoW: int64(887451661),
Total: int64(1479086101),
},
}, {
name: "handleGetBlockSubsidy: modified subsidy split ok",
handler: handleGetBlockSubsidy,
cmd: &types.GetBlockSubsidyCmd{
Height: 638977,
Voters: 5,
},
mockChain: func() *testRPCChain {
chain := defaultMockRPCChain()
chain.subsidySplitActive = true
return chain
}(),
result: types.GetBlockSubsidyResult{
Developer: int64(110834154),
PoS: int64(886673230),
PoW: int64(110834154),
Total: int64(1108341538),
},
}, {
name: "handleGetBlockSubsidy: modified subsidy split status failure",
handler: handleGetBlockSubsidy,
cmd: &types.GetBlockSubsidyCmd{
Height: 638977,
Voters: 5,
},
mockChain: func() *testRPCChain {
chain := defaultMockRPCChain()
chain.subsidySplitActiveErr = errors.New("error getting agenda status")
return chain
}(),
wantErr: true,
errCode: dcrjson.ErrRPCInternal.Code,
}})
}

View File

@ -509,7 +509,11 @@ func (w *VotingWallet) handleWinningTicketsNtfn(ntfn *winningTicketsNtfn) {
return
}
stakebaseValue := w.subsidyCache.CalcStakeVoteSubsidy(ntfn.blockHeight)
// Always consider the subsidy split enabled since the test voting wallet
// is only used with simnet where the agenda is always active.
const isSubsidySplitEnabled = true
stakebaseValue := w.subsidyCache.CalcStakeVoteSubsidyV2(ntfn.blockHeight,
isSubsidySplitEnabled)
// Create the votes. nbVotes is the number of tickets from the wallet that
// voted.

View File

@ -3630,6 +3630,10 @@ func newServer(ctx context.Context, listenAddrs []string, db database.DB,
tipHash := &s.chain.BestSnapshot().Hash
return s.chain.IsAutoRevocationsAgendaActive(tipHash)
},
IsSubsidySplitAgendaActive: func() (bool, error) {
tipHash := &s.chain.BestSnapshot().Hash
return s.chain.IsSubsidySplitAgendaActive(tipHash)
},
TSpendMinedOnAncestor: func(tspend chainhash.Hash) error {
tipHash := s.chain.BestSnapshot().Hash
return s.chain.CheckTSpendExists(tipHash, tspend)
@ -3697,11 +3701,11 @@ func newServer(ctx context.Context, listenAddrs []string, db database.DB,
CheckTransactionInputs: func(tx *dcrutil.Tx, txHeight int64,
view *blockchain.UtxoViewpoint, checkFraudProof bool,
prevHeader *wire.BlockHeader, isTreasuryEnabled,
isAutoRevocationsEnabled bool) (int64, error) {
isAutoRevocationsEnabled, isSubsidyEnabled bool) (int64, error) {
return blockchain.CheckTransactionInputs(s.subsidyCache, tx, txHeight,
view, checkFraudProof, s.chainParams, prevHeader, isTreasuryEnabled,
isAutoRevocationsEnabled)
isAutoRevocationsEnabled, isSubsidyEnabled)
},
CheckTSpendHasVotes: s.chain.CheckTSpendHasVotes,
CountSigOps: blockchain.CountSigOps,
@ -3714,6 +3718,7 @@ func newServer(ctx context.Context, listenAddrs []string, db database.DB,
IsHeaderCommitmentsAgendaActive: s.chain.IsHeaderCommitmentsAgendaActive,
IsTreasuryAgendaActive: s.chain.IsTreasuryAgendaActive,
IsAutoRevocationsAgendaActive: s.chain.IsAutoRevocationsAgendaActive,
IsSubsidySplitAgendaActive: s.chain.IsSubsidySplitAgendaActive,
MaxTreasuryExpenditure: s.chain.MaxTreasuryExpenditure,
NewUtxoViewpoint: func() *blockchain.UtxoViewpoint {
return blockchain.NewUtxoViewpoint(utxoCache)