diff --git a/blockchain/agendas_test.go b/blockchain/agendas_test.go index 04f8e102..62be9ef1 100644 --- a/blockchain/agendas_test.go +++ b/blockchain/agendas_test.go @@ -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()) +} diff --git a/blockchain/go.mod b/blockchain/go.mod index d29d7fe9..7167aa70 100644 --- a/blockchain/go.mod +++ b/blockchain/go.mod @@ -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 diff --git a/blockchain/go.sum b/blockchain/go.sum index f77e78f9..c3bbd251 100644 --- a/blockchain/go.sum +++ b/blockchain/go.sum @@ -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= diff --git a/blockchain/thresholdstate.go b/blockchain/thresholdstate.go index 166488b8..81b5243e 100644 --- a/blockchain/thresholdstate.go +++ b/blockchain/thresholdstate.go @@ -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 diff --git a/blockchain/validate.go b/blockchain/validate.go index 238f3f91..eb858ff7 100644 --- a/blockchain/validate.go +++ b/blockchain/validate.go @@ -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 diff --git a/blockchain/validate_test.go b/blockchain/validate_test.go index 8debff37..ce2bdce0 100644 --- a/blockchain/validate_test.go +++ b/blockchain/validate_test.go @@ -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() +} diff --git a/go.mod b/go.mod index e7d13612..3b08c9e0 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/internal/mempool/mempool.go b/internal/mempool/mempool.go index 97d23675..bb1986ad 100644 --- a/internal/mempool/mempool.go +++ b/internal/mempool/mempool.go @@ -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 } diff --git a/internal/mempool/mempool_test.go b/internal/mempool/mempool_test.go index 239de5a7..8b101917 100644 --- a/internal/mempool/mempool_test.go +++ b/internal/mempool/mempool_test.go @@ -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) +} diff --git a/internal/mining/mining.go b/internal/mining/mining.go index 4e42f053..3a880744 100644 --- a/internal/mining/mining.go +++ b/internal/mining/mining.go @@ -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 diff --git a/internal/mining/mining_harness_test.go b/internal/mining/mining_harness_test.go index f7bd8421..9ce10d6f 100644 --- a/internal/mining/mining_harness_test.go +++ b/internal/mining/mining_harness_test.go @@ -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, diff --git a/internal/rpcserver/interface.go b/internal/rpcserver/interface.go index f768f65d..506b90d8 100644 --- a/internal/rpcserver/interface.go +++ b/internal/rpcserver/interface.go @@ -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) diff --git a/internal/rpcserver/rpcserver.go b/internal/rpcserver/rpcserver.go index 31a09c4d..7d166ade 100644 --- a/internal/rpcserver/rpcserver.go +++ b/internal/rpcserver/rpcserver.go @@ -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. diff --git a/internal/rpcserver/rpcserverhandlers_test.go b/internal/rpcserver/rpcserverhandlers_test.go index 353b8e81..9ddbc7db 100644 --- a/internal/rpcserver/rpcserverhandlers_test.go +++ b/internal/rpcserver/rpcserverhandlers_test.go @@ -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, }}) } diff --git a/rpctest/votingwallet.go b/rpctest/votingwallet.go index 08af283c..c762e9de 100644 --- a/rpctest/votingwallet.go +++ b/rpctest/votingwallet.go @@ -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. diff --git a/server.go b/server.go index 34c97f36..250e3a9d 100644 --- a/server.go +++ b/server.go @@ -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)