diff --git a/blockchain/standalone/subsidy.go b/blockchain/standalone/subsidy.go index 4f7120ee..24cd8a45 100644 --- a/blockchain/standalone/subsidy.go +++ b/blockchain/standalone/subsidy.go @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2020 The Decred developers +// Copyright (c) 2015-2021 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -47,7 +47,8 @@ type SubsidyParams interface { // block subsidy between PoW, PoS, and the Treasury. // WorkSubsidyProportion returns the comparative proportion of the subsidy - // generated for creating a block (PoW). + // generated for creating a block (PoW) that was in effect prior to the + // modified value defined in DCP0010. // // The proportional split between PoW, PoS, and the Treasury is calculated // by treating each of the proportional parameters as a ratio to the sum of @@ -61,7 +62,8 @@ type SubsidyParams interface { WorkSubsidyProportion() uint16 // StakeSubsidyProportion returns the comparative proportion of the subsidy - // generated for casting stake votes (collectively, per block). See the + // generated for casting stake votes (collectively, per block) that was in + // effect prior to the modified value defined in DCP0010. See the // documentation for WorkSubsidyProportion for more details on how the // parameter is used. StakeSubsidyProportion() uint16 @@ -236,17 +238,17 @@ func (c *SubsidyCache) CalcBlockSubsidy(height int64) int64 { return subsidy } -// CalcWorkSubsidy returns the proof of work subsidy for a block for a given -// number of votes. It is calculated as a proportion of the total subsidy and -// further reduced proportionally depending on the number of votes once the -// height at which voting begins has been reached. +// calcWorkSubsidy returns the proof of work subsidy for a block for a given +// number of votes using the provided work subsidy proportion and total +// proportions. This is the primary implementation logic used by +// CalcWorkSubsidy and CalcWorkSubsidyV2. // -// Note that passing a number of voters fewer than the minimum required for a -// block to be valid by consensus along with a height greater than or equal to -// the height at which voting begins will return zero. +// See the comments of those functions for further details. // // This function is safe for concurrent access. -func (c *SubsidyCache) CalcWorkSubsidy(height int64, voters uint16) int64 { +func (c *SubsidyCache) calcWorkSubsidy(height int64, voters uint16, proportion, + totalProportions uint16) int64 { + // The first block has special subsidy rules. if height == 1 { return c.params.BlockOneSubsidy() @@ -262,8 +264,8 @@ func (c *SubsidyCache) CalcWorkSubsidy(height int64, voters uint16) int64 { // Calculate the full block subsidy and reduce it according to the PoW // proportion. subsidy := c.CalcBlockSubsidy(height) - subsidy *= int64(c.params.WorkSubsidyProportion()) - subsidy /= int64(c.totalProportions) + subsidy *= int64(proportion) + subsidy /= int64(totalProportions) // Ignore any potential subsidy reductions due to the number of votes prior // to the point voting begins. @@ -275,19 +277,67 @@ func (c *SubsidyCache) CalcWorkSubsidy(height int64, voters uint16) int64 { return (int64(voters) * subsidy) / int64(c.params.VotesPerBlock()) } -// CalcStakeVoteSubsidy returns the subsidy for a single stake vote for a block. -// It is calculated as a proportion of the total subsidy and max potential -// number of votes per block. +// CalcWorkSubsidy returns the proof of work subsidy for a block for a given +// number of votes using the subsidy split that was in effect prior to the +// modifed value defined in DCP0010. // -// Unlike the Proof-of-Work and Treasury subsidies, the subsidy that votes -// receive is not reduced when a block contains less than the maximum number of -// votes. Consequently, this does not accept the number of votes. However, it -// is important to note that blocks that do not receive the minimum required -// number of votes for a block to be valid by consensus won't actually produce -// any vote subsidy either since they are invalid. +// It is calculated as a proportion of the total subsidy and further reduced +// proportionally depending on the number of votes once the height at which +// voting begins has been reached. +// +// Note that passing a number of voters fewer than the minimum required for a +// block to be valid by consensus along with a height greater than or equal to +// the height at which voting begins will return zero. // // This function is safe for concurrent access. -func (c *SubsidyCache) CalcStakeVoteSubsidy(height int64) int64 { +// +// Deprecated: Use CalcWorkSubsidyV2 instead. +func (c *SubsidyCache) CalcWorkSubsidy(height int64, voters uint16) int64 { + return c.calcWorkSubsidy(height, voters, c.params.WorkSubsidyProportion(), + c.totalProportions) +} + +// CalcWorkSubsidyV2 returns the proof of work subsidy for a block for a given +// number of votes using either the original subsidy split that was in effect at +// Decred launch or the modified subsidy split defined in DCP0010 according to +// the provided flag. +// +// It is calculated as a proportion of the total subsidy and further reduced +// proportionally depending on the number of votes once the height at which +// voting begins has been reached. +// +// Note that passing a number of voters fewer than the minimum required for a +// block to be valid by consensus along with a height greater than or equal to +// the height at which voting begins will return zero. +// +// This function is safe for concurrent access. +func (c *SubsidyCache) CalcWorkSubsidyV2(height int64, voters uint16, useDCP0010 bool) int64 { + if !useDCP0010 { + return c.CalcWorkSubsidy(height, voters) + } + + // The work subsidy proportion defined in DCP0010 is 10%. Thus it is 1 + // since 1/10 = 10%. + // + // Note that the value is hard coded here as opposed to using the subsidy + // params in order to avoid the need for a major module bump that would be + // required if the subsidy params interface were changed. + const workSubsidyProportion = 1 + const totalProportions = 10 + return c.calcWorkSubsidy(height, voters, workSubsidyProportion, + totalProportions) +} + +// calcStakeVoteSubsidy returns the subsidy for a single stake vote for a block +// using the provided stake vote subsidy proportion. It is calculated as the +// provided proportion of the total subsidy and max potential number of votes +// per block. This is the primary implementation logic used by +// CalcStakeVoteSubsidy and CalcStakeVoteSubsidyV2. +// +// See the comments of those functions for further details. +// +// This function is safe for concurrent access. +func (c *SubsidyCache) calcStakeVoteSubsidy(height int64, proportion, totalProportions uint16) int64 { // Votes have no subsidy prior to the point voting begins. The minus one // accounts for the fact that vote subsidy are, unfortunately, based on the // height that is being voted on as opposed to the block in which they are @@ -300,13 +350,68 @@ func (c *SubsidyCache) CalcStakeVoteSubsidy(height int64) int64 { // proportion. Then divide it by the number of votes per block to arrive // at the amount per vote. subsidy := c.CalcBlockSubsidy(height) - proportions := int64(c.totalProportions) - subsidy *= int64(c.params.StakeSubsidyProportion()) + proportions := int64(totalProportions) + subsidy *= int64(proportion) subsidy /= (proportions * int64(c.params.VotesPerBlock())) return subsidy } +// CalcStakeVoteSubsidy returns the subsidy for a single stake vote for a block +// using the subsidy split that was in effect prior to the modifed value defined +// in DCP0010. +// +// It is calculated as a proportion of the total subsidy and max potential +// number of votes per block. +// +// Unlike the Proof-of-Work and Treasury subsidies, the subsidy that votes +// receive is not reduced when a block contains less than the maximum number of +// votes. Consequently, this does not accept the number of votes. However, it +// is important to note that blocks that do not receive the minimum required +// number of votes for a block to be valid by consensus won't actually produce +// any vote subsidy either since they are invalid. +// +// This function is safe for concurrent access. +// +// Deprecated: Use CalcStakeVoteSubsidyV2 instead. +func (c *SubsidyCache) CalcStakeVoteSubsidy(height int64) int64 { + return c.calcStakeVoteSubsidy(height, c.params.StakeSubsidyProportion(), + c.totalProportions) +} + +// CalcStakeVoteSubsidyV2 returns the subsidy for a single stake vote for a +// block using either the original subsidy split that was in effect at Decred +// launch or the modified subsidy split defined in DCP0010 according to the +// provided flag. +// +// It is calculated as a proportion of the total subsidy and max potential +// number of votes per block. +// +// Unlike the Proof-of-Work and Treasury subsidies, the subsidy that votes +// receive is not reduced when a block contains less than the maximum number of +// votes. Consequently, this does not accept the number of votes. However, it +// is important to note that blocks that do not receive the minimum required +// number of votes for a block to be valid by consensus won't actually produce +// any vote subsidy either since they are invalid. +// +// This function is safe for concurrent access. +func (c *SubsidyCache) CalcStakeVoteSubsidyV2(height int64, useDCP0010 bool) int64 { + if !useDCP0010 { + return c.CalcStakeVoteSubsidy(height) + } + + // The stake vote subsidy proportion defined in DCP0010 is 80%. Thus it is + // 8 since 8/10 = 80%. + // + // Note that the value is hard coded here as opposed to using the subsidy + // params in order to avoid the need for a major module bump that would be + // required if the subsidy params interface were changed. + const voteSubsidyProportion = 8 + const totalProportions = 10 + return c.calcStakeVoteSubsidy(height, voteSubsidyProportion, + totalProportions) +} + // CalcTreasurySubsidy returns the subsidy required to go to the treasury for // a block. It is calculated as a proportion of the total subsidy and further // reduced proportionally depending on the number of votes once the height at diff --git a/blockchain/standalone/subsidy_test.go b/blockchain/standalone/subsidy_test.go index 54db2e66..363975ed 100644 --- a/blockchain/standalone/subsidy_test.go +++ b/blockchain/standalone/subsidy_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2020 The Decred developers +// Copyright (c) 2019-2021 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -157,6 +157,7 @@ func TestSubsidyCacheCalcs(t *testing.T) { wantWork int64 // expected pow subsidy wantVote int64 // expected single vote subsidy wantTreasury int64 // expected treasury subsidy + useDCP0010 bool // use subsidy split defined in DCP0010 }{{ name: "negative height", params: mockMainNetParams, @@ -166,6 +167,16 @@ func TestSubsidyCacheCalcs(t *testing.T) { wantWork: 0, wantVote: 0, wantTreasury: 0, + }, { + name: "negative height, use DCP0010", + params: mockMainNetParams, + height: -1, + numVotes: 0, + wantFull: 0, + wantWork: 0, + wantVote: 0, + wantTreasury: 0, + useDCP0010: true, }, { name: "height 0", params: mockMainNetParams, @@ -175,6 +186,16 @@ func TestSubsidyCacheCalcs(t *testing.T) { wantWork: 0, wantVote: 0, wantTreasury: 0, + }, { + name: "height 0, use DCP0010", + params: mockMainNetParams, + height: 0, + numVotes: 0, + wantFull: 0, + wantWork: 0, + wantVote: 0, + wantTreasury: 0, + useDCP0010: true, }, { name: "height 1 (initial payouts)", params: mockMainNetParams, @@ -184,6 +205,16 @@ func TestSubsidyCacheCalcs(t *testing.T) { wantWork: 168000000000000, wantVote: 0, wantTreasury: 0, + }, { + name: "height 1 (initial payouts), use DCP0010", + params: mockMainNetParams, + height: 1, + numVotes: 0, + wantFull: 168000000000000, + wantWork: 168000000000000, + wantVote: 0, + wantTreasury: 0, + useDCP0010: true, }, { name: "height 2 (first non-special block prior voting start)", params: mockMainNetParams, @@ -193,6 +224,16 @@ func TestSubsidyCacheCalcs(t *testing.T) { wantWork: 1871749598, wantVote: 0, wantTreasury: 311958266, + }, { + name: "height 2 (first non-special block prior voting start), use DCP0010", + params: mockMainNetParams, + height: 2, + numVotes: 0, + wantFull: 3119582664, + wantWork: 311958266, + wantVote: 0, + wantTreasury: 311958266, + useDCP0010: true, }, { name: "height 4094 (two blocks prior to voting start)", params: mockMainNetParams, @@ -202,6 +243,16 @@ func TestSubsidyCacheCalcs(t *testing.T) { wantWork: 1871749598, wantVote: 0, wantTreasury: 311958266, + }, { + name: "height 4094 (two blocks prior to voting start), use DCP0010", + params: mockMainNetParams, + height: 4094, + numVotes: 0, + wantFull: 3119582664, + wantWork: 311958266, + wantVote: 0, + wantTreasury: 311958266, + useDCP0010: true, }, { name: "height 4095 (final block prior to voting start)", params: mockMainNetParams, @@ -211,6 +262,16 @@ func TestSubsidyCacheCalcs(t *testing.T) { wantWork: 1871749598, wantVote: 187174959, wantTreasury: 311958266, + }, { + name: "height 4095 (final block prior to voting start), use DCP0010", + params: mockMainNetParams, + height: 4095, + numVotes: 0, + wantFull: 3119582664, + wantWork: 311958266, + wantVote: 499133226, + wantTreasury: 311958266, + useDCP0010: true, }, { name: "height 4096 (voting start), 5 votes", params: mockMainNetParams, @@ -220,6 +281,16 @@ func TestSubsidyCacheCalcs(t *testing.T) { wantWork: 1871749598, wantVote: 187174959, wantTreasury: 311958266, + }, { + name: "height 4096 (voting start), 5 votes, use DCP0010", + params: mockMainNetParams, + height: 4096, + numVotes: 5, + wantFull: 3119582664, + wantWork: 311958266, + wantVote: 499133226, + wantTreasury: 311958266, + useDCP0010: true, }, { name: "height 4096 (voting start), 4 votes", params: mockMainNetParams, @@ -229,6 +300,16 @@ func TestSubsidyCacheCalcs(t *testing.T) { wantWork: 1497399678, wantVote: 187174959, wantTreasury: 249566612, + }, { + name: "height 4096 (voting start), 4 votes, use DCP0010", + params: mockMainNetParams, + height: 4096, + numVotes: 4, + wantFull: 3119582664, + wantWork: 249566612, + wantVote: 499133226, + wantTreasury: 249566612, + useDCP0010: true, }, { name: "height 4096 (voting start), 3 votes", params: mockMainNetParams, @@ -238,6 +319,16 @@ func TestSubsidyCacheCalcs(t *testing.T) { wantWork: 1123049758, wantVote: 187174959, wantTreasury: 187174959, + }, { + name: "height 4096 (voting start), 3 votes, use DCP0010", + params: mockMainNetParams, + height: 4096, + numVotes: 3, + wantFull: 3119582664, + wantWork: 187174959, + wantVote: 499133226, + wantTreasury: 187174959, + useDCP0010: true, }, { name: "height 4096 (voting start), 2 votes", params: mockMainNetParams, @@ -247,6 +338,16 @@ func TestSubsidyCacheCalcs(t *testing.T) { wantWork: 0, wantVote: 187174959, wantTreasury: 0, + }, { + name: "height 4096 (voting start), 2 votes, use DCP0010", + params: mockMainNetParams, + height: 4096, + numVotes: 2, + wantFull: 3119582664, + wantWork: 0, + wantVote: 499133226, + wantTreasury: 0, + useDCP0010: true, }, { name: "height 6143 (final block prior to 1st reduction), 5 votes", params: mockMainNetParams, @@ -256,6 +357,16 @@ func TestSubsidyCacheCalcs(t *testing.T) { wantWork: 1871749598, wantVote: 187174959, wantTreasury: 311958266, + }, { + name: "height 6143 (final block prior to 1st reduction), 5 votes, use DCP0010", + params: mockMainNetParams, + height: 6143, + numVotes: 5, + wantFull: 3119582664, + wantWork: 311958266, + wantVote: 499133226, + wantTreasury: 311958266, + useDCP0010: true, }, { name: "height 6144 (1st block in 1st reduction), 5 votes", params: mockMainNetParams, @@ -265,6 +376,16 @@ func TestSubsidyCacheCalcs(t *testing.T) { wantWork: 1853217423, wantVote: 185321742, wantTreasury: 308869570, + }, { + name: "height 6144 (1st block in 1st reduction), 5 votes, use DCP0010", + params: mockMainNetParams, + height: 6144, + numVotes: 5, + wantFull: 3088695706, + wantWork: 308869570, + wantVote: 494191312, + wantTreasury: 308869570, + useDCP0010: true, }, { name: "height 6144 (1st block in 1st reduction), 4 votes", params: mockMainNetParams, @@ -274,6 +395,16 @@ func TestSubsidyCacheCalcs(t *testing.T) { wantWork: 1482573938, wantVote: 185321742, wantTreasury: 247095656, + }, { + name: "height 6144 (1st block in 1st reduction), 4 votes, use DCP0010", + params: mockMainNetParams, + height: 6144, + numVotes: 4, + wantFull: 3088695706, + wantWork: 247095656, + wantVote: 494191312, + wantTreasury: 247095656, + useDCP0010: true, }, { name: "height 12287 (last block in 1st reduction), 5 votes", params: mockMainNetParams, @@ -283,6 +414,16 @@ func TestSubsidyCacheCalcs(t *testing.T) { wantWork: 1853217423, wantVote: 185321742, wantTreasury: 308869570, + }, { + name: "height 12287 (last block in 1st reduction), 5 votes, use DCP0010", + params: mockMainNetParams, + height: 12287, + numVotes: 5, + wantFull: 3088695706, + wantWork: 308869570, + wantVote: 494191312, + wantTreasury: 308869570, + useDCP0010: true, }, { name: "height 12288 (1st block in 2nd reduction), 5 votes", params: mockMainNetParams, @@ -292,6 +433,16 @@ func TestSubsidyCacheCalcs(t *testing.T) { wantWork: 1834868736, wantVote: 183486873, wantTreasury: 305811456, + }, { + name: "height 12288 (1st block in 2nd reduction), 5 votes, use DCP0010", + params: mockMainNetParams, + height: 12288, + numVotes: 5, + wantFull: 3058114560, + wantWork: 305811456, + wantVote: 489298329, + wantTreasury: 305811456, + useDCP0010: true, }, { name: "height 307200 (1st block in 50th reduction), 5 votes", params: mockMainNetParams, @@ -301,6 +452,16 @@ func TestSubsidyCacheCalcs(t *testing.T) { wantWork: 1138096413, wantVote: 113809641, wantTreasury: 189682735, + }, { + name: "height 307200 (1st block in 50th reduction), 5 votes, use DCP0010", + params: mockMainNetParams, + height: 307200, + numVotes: 5, + wantFull: 1896827356, + wantWork: 189682735, + wantVote: 303492376, + wantTreasury: 189682735, + useDCP0010: true, }, { name: "height 307200 (1st block in 50th reduction), 3 votes", params: mockMainNetParams, @@ -310,6 +471,16 @@ func TestSubsidyCacheCalcs(t *testing.T) { wantWork: 682857847, wantVote: 113809641, wantTreasury: 113809641, + }, { + name: "height 307200 (1st block in 50th reduction), 3 votes, use DCP0010", + params: mockMainNetParams, + height: 307200, + numVotes: 3, + wantFull: 1896827356, + wantWork: 113809641, + wantVote: 303492376, + wantTreasury: 113809641, + useDCP0010: true, }, { name: "height 10911744 (first zero vote subsidy 1776th reduction), 5 votes", params: mockMainNetParams, @@ -328,6 +499,26 @@ func TestSubsidyCacheCalcs(t *testing.T) { wantWork: 5, wantVote: 0, wantTreasury: 0, + }, { + name: "height 10954752 (first zero work subsidy with DCP0010 1783rd reduction), 5 votes", + params: mockMainNetParams, + height: 10954752, + numVotes: 5, + wantFull: 9, + wantWork: 0, + wantVote: 1, + wantTreasury: 0, + useDCP0010: true, + }, { + name: "height 10973184 (first zero vote subsidy with DCP0010 1786th reduction), 5 votes", + params: mockMainNetParams, + height: 10973184, + numVotes: 5, + wantFull: 6, + wantWork: 0, + wantVote: 0, + wantTreasury: 0, + useDCP0010: true, }, { name: "height 11003904 (first zero work subsidy 1791st reduction), 5 votes", params: mockMainNetParams, @@ -346,6 +537,16 @@ func TestSubsidyCacheCalcs(t *testing.T) { wantWork: 0, wantVote: 0, wantTreasury: 0, + }, { + name: "height 11010048 (first zero full subsidy 1792nd reduction), 5 votes, use DCP0010", + params: mockMainNetParams, + height: 11010048, + numVotes: 5, + wantFull: 0, + wantWork: 0, + wantVote: 0, + wantTreasury: 0, + useDCP0010: true, }} for _, test := range tests { @@ -359,7 +560,8 @@ func TestSubsidyCacheCalcs(t *testing.T) { } // Ensure the PoW subsidy is the expected value. - workResult := cache.CalcWorkSubsidy(test.height, test.numVotes) + workResult := cache.CalcWorkSubsidyV2(test.height, test.numVotes, + test.useDCP0010) if workResult != test.wantWork { t.Errorf("%s: unexpected work subsidy result -- got %d, want %d", test.name, workResult, test.wantWork) @@ -367,7 +569,7 @@ func TestSubsidyCacheCalcs(t *testing.T) { } // Ensure the vote subsidy is the expected value. - voteResult := cache.CalcStakeVoteSubsidy(test.height) + voteResult := cache.CalcStakeVoteSubsidyV2(test.height, test.useDCP0010) if voteResult != test.wantVote { t.Errorf("%s: unexpected vote subsidy result -- got %d, want %d", test.name, voteResult, test.wantVote) @@ -753,6 +955,76 @@ func TestTotalSubsidyTreasury(t *testing.T) { } } +// TestTotalSubsidyDCP0010 ensures the estimated total subsidy produced with the +// subsidy split defined in DCP0010 matches the expected value. +func TestTotalSubsidyDCP0010(t *testing.T) { + // Locals for convenience. + mockMainNetParams := mockMainNetParams() + reductionInterval := mockMainNetParams.SubsidyReductionIntervalBlocks() + stakeValidationHeight := mockMainNetParams.StakeValidationBeginHeight() + votesPerBlock := mockMainNetParams.VotesPerBlock() + + // subsidySum returns the sum of the individual subsidies for the given + // height using either the original subsidy split or the modified split + // defined in DCP0010. Note that this value is not exactly the same as the + // full subsidy originally used to calculate the individual proportions due + // to the use of integer math. + cache := NewSubsidyCache(mockMainNetParams) + subsidySum := func(height int64, useDCP0010 bool) int64 { + work := cache.CalcWorkSubsidyV2(height, votesPerBlock, useDCP0010) + vote := cache.CalcStakeVoteSubsidyV2(height, useDCP0010) * + int64(votesPerBlock) + treasury := cache.CalcTreasurySubsidy(height, votesPerBlock, noTreasury) + return work + vote + treasury + } + + // Calculate the total possible subsidy. + totalSubsidy := mockMainNetParams.BlockOneSubsidy() + for reductionNum := int64(0); ; reductionNum++ { + // The first interval contains a few special cases: + // 1) Block 0 does not produce any subsidy + // 2) Block 1 consists of a special initial coin distribution + // 3) Votes do not produce subsidy until voting begins + if reductionNum == 0 { + // Account for the block up to the point voting begins ignoring the + // first two special blocks. + subsidyCalcHeight := int64(2) + nonVotingBlocks := stakeValidationHeight - subsidyCalcHeight + totalSubsidy += subsidySum(subsidyCalcHeight, false) * nonVotingBlocks + + // Account for the blocks remaining in the interval once voting + // begins. + subsidyCalcHeight = stakeValidationHeight + votingBlocks := reductionInterval - subsidyCalcHeight + totalSubsidy += subsidySum(subsidyCalcHeight, false) * votingBlocks + continue + } + + // Account for the all other reduction intervals until all subsidy has + // been produced. + // + // Note that this is necessarily an estimate since the exact height at + // which DCP0010 should be activated is impossible to know at the time + // of this writing. For testing purposes, the activation height is + // estimated to be 638976, or in other words, the 104th reduction + // interval on mainnet. + subsidyCalcHeight := reductionNum * reductionInterval + useDCP0010 := subsidyCalcHeight >= reductionInterval*104 + sum := subsidySum(subsidyCalcHeight, useDCP0010) + if sum == 0 { + break + } + totalSubsidy += sum * reductionInterval + } + + // Ensure the total calculated subsidy is the expected value. + const expectedTotalSubsidy = 2100000000015952 + if totalSubsidy != expectedTotalSubsidy { + t.Fatalf("mismatched total subsidy -- got %d, want %d", totalSubsidy, + expectedTotalSubsidy) + } +} + // TestCalcBlockSubsidySparseCaching ensures the cache calculations work // properly when accessed sparsely and out of order. func TestCalcBlockSubsidySparseCaching(t *testing.T) {