diff --git a/internal/staging/primitives/subsidy.go b/internal/staging/primitives/subsidy.go new file mode 100644 index 0000000000..cf3b4832a5 --- /dev/null +++ b/internal/staging/primitives/subsidy.go @@ -0,0 +1,458 @@ +// Copyright (c) 2015-2022 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package primitives + +import ( + "sort" + "sync" +) + +// ----------------------------------------------------------------------------- +// Maintainer note: +// +// Since this code is part of the primitives module, major module version bumps +// should be avoided at nearly all costs given the headache major module version +// bumps typically cause consumers. +// +// Although changes to the subsidy are not expected, in light of DCP0010, they +// should not entirely be ruled out. Therefore, the following discusses the +// preferred approach for introducing new subsidy calculation semantics while +// achieving the goal of avoiding major module version bumps. +// +// Any modifications to the subsidy calculations that involve a consensus change +// will necessarily require some mechanism to allow calculating both the +// historical values and the new values according to new semantics defined by an +// associated DCP. While not the only possible approach, a good approach is +// typically a flag to specify when the agenda is active. In the case multiple +// booleans are required, bit flags should be preferred instead. However, keep +// in mind that adding a new parameter to a method is a major breaking change to +// the API. +// +// Further, ideally, callers should NOT be required to use an older version of +// the cache to calculate values according to those older semantics since that +// would be cumbersome to use for callers as it would require them to keep +// multiple versions of the cache around. It also would be less efficient +// because cache reuse would not be possible. +// +// Keeping the aforementioned in mind, the preferred way to introduce any such +// changes is to introduce a new type for the cache, such as SubsidyCacheV2, +// with the relevant calculation funcs defined on the new type updated to accept +// the new flags so that the updated version cache can be used for both whatever +// the new semantics are as well as all historical semantics. +// +// Since the cache only stores the full block subsidies prior to any splits or +// reductions, so long as the consensus change does not involve modifying the +// full block subsidies, using the same new version of a cache with flags for +// different splits and/or reduction semantics is straightforward. On the other +// hand, supporting a consensus change that involves modifying the full block +// subsidies requires greater care, but can still be done efficiently by storing +// additional information regarding which semantics were used to generate the +// cached entries and reacting accordingly. +// +// In the case there are new per-network parameters that must be accepted, a new +// type for the params, such as SubsidyParamsV2 should be introduced and passed +// to the constructor of the new cache type (e.g. NewSubsidyCacheV2) given +// modification of the existing params interface (including adding new methods) +// would be a major breaking change to the API. Note that the new version of +// the parameters will need all of the existing parameters as well, so that can +// be achieved by embedding the existing params interface in the new one. +// +// Finally, the existing type and methods (e.g. SubsidyCache and all calculation +// methods it provides) that are now handled by the newly introduced versions +// should then be marked as deprecated with a message instructing the caller to +// use the newest version of them instead. +// ----------------------------------------------------------------------------- + +// SubsidyParams defines an interface that is used to provide the parameters +// required when calculating block and vote subsidies. These values are +// typically well-defined and unique per network. +type SubsidyParams interface { + // BlockOneSubsidy returns the total subsidy of block height 1 for the + // network. This is separate since it encompasses the initial coin + // distribution. + BlockOneSubsidy() int64 + + // BaseSubsidyValue returns the starting base max potential subsidy amount + // for mined blocks. This value is reduced over time and then split + // proportionally between PoW, PoS, and the Treasury. The reduction is + // controlled by the SubsidyReductionInterval, SubsidyReductionMultiplier, + // and SubsidyReductionDivisor parameters. + // + // NOTE: This must be a max of 140,739,635,871,744 atoms or incorrect + // results will occur due to int64 overflow. This value comes from + // MaxInt64/MaxUint16 = (2^63 - 1)/(2^16 - 1) = 2^47 + 2^31 + 2^15. + BaseSubsidyValue() int64 + + // SubsidyReductionMultiplier returns the multiplier to use when performing + // the exponential subsidy reduction described by the CalcBlockSubsidy + // documentation. + SubsidyReductionMultiplier() int64 + + // SubsidyReductionDivisor returns the divisor to use when performing the + // exponential subsidy reduction described by the CalcBlockSubsidy + // documentation. + SubsidyReductionDivisor() int64 + + // SubsidyReductionIntervalBlocks returns the reduction interval in number + // of blocks. + SubsidyReductionIntervalBlocks() int64 + + // StakeValidationBeginHeight returns the height at which votes become + // required to extend a block. This height is the first that will be voted + // on, but will not include any votes itself. + StakeValidationBeginHeight() int64 + + // VotesPerBlock returns the maximum number of votes a block must contain to + // receive full subsidy once voting begins at StakeValidationBeginHeight. + VotesPerBlock() uint16 +} + +// totalProportions is the sum of the proportions for the subsidy split between +// PoW, PoS, and the Treasury. +// +// This value comes from the fact that the subsidy split is as follows: +// +// Prior to DCP0010: PoW: 6, PoS: 3, Treasury: 1 => 6+3+1 = 10 +// After DCP0010: PoW: 1, PoS: 8, Treasury: 1 => 1+8+1 = 10 +// +// Therefore, the subsidy split percentages are: +// +// Prior to DCP0010: PoW: 6/10 = 60%, PoS: 3/10 = 30%, Treasury: 1/10 = 10% +// After DCP0010: PoW: 1/10 = 10%, PoS: 8/10 = 80%, Treasury: 1/10 = 10% +const totalProportions = 10 + +// SubsidyCache provides efficient access to consensus-critical subsidy +// calculations for blocks and votes, including the max potential subsidy for +// given block heights, the proportional proof-of-work subsidy, the proportional +// proof of stake per-vote subsidy, and the proportional treasury subsidy. +// +// It makes use of caching to avoid repeated calculations. +type SubsidyCache struct { + // This mutex protects the following fields: + // + // cache houses the cached subsidies keyed by reduction interval. + // + // cachedIntervals contains an ordered list of all cached intervals. It is + // used to efficiently track sparsely cached intervals with O(log N) + // discovery of a prior cached interval. + mtx sync.RWMutex + cache map[uint64]int64 + cachedIntervals []uint64 + + // params stores the subsidy parameters to use during subsidy calculation. + params SubsidyParams + + // minVotesRequired is the minimum number of votes required for a block to + // be considered valid by consensus. It is calculated from the parameters + // at initialization time and stored in order to avoid repeated calculation. + minVotesRequired uint16 +} + +// NewSubsidyCache creates and initializes a new subsidy cache instance. See +// the SubsidyCache documentation for more details. +func NewSubsidyCache(params SubsidyParams) *SubsidyCache { + // Initialize the cache with the first interval set to the base subsidy and + // enough initial space for a few sparse entries for typical usage patterns. + const prealloc = 5 + baseSubsidy := params.BaseSubsidyValue() + cache := make(map[uint64]int64, prealloc) + cache[0] = baseSubsidy + + return &SubsidyCache{ + cache: cache, + cachedIntervals: make([]uint64, 1, prealloc), + params: params, + minVotesRequired: (params.VotesPerBlock() / 2) + 1, + } +} + +// uint64s implements sort.Interface for *[]uint64. +type uint64s []uint64 + +func (s *uint64s) Len() int { return len(*s) } +func (s *uint64s) Less(i, j int) bool { return (*s)[i] < (*s)[j] } +func (s *uint64s) Swap(i, j int) { (*s)[i], (*s)[j] = (*s)[j], (*s)[i] } + +// CalcBlockSubsidy returns the max potential subsidy for a block at the +// provided height. This value is reduced over time based on the height and +// then split proportionally between PoW, PoS, and the Treasury. +// +// Subsidy calculation for exponential reductions: +// +// subsidy := BaseSubsidyValue() +// for i := 0; i < (height / SubsidyReductionIntervalBlocks()); i++ { +// subsidy *= SubsidyReductionMultiplier() +// subsidy /= SubsidyReductionDivisor() +// } +// +// This function is safe for concurrent access. +func (c *SubsidyCache) CalcBlockSubsidy(height int64) int64 { + // Negative block heights are invalid and produce no subsidy. + // Block 0 is the genesis block and produces no subsidy. + // Block 1 subsidy is special as it is used for initial token distribution. + switch { + case height <= 0: + return 0 + case height == 1: + return c.params.BlockOneSubsidy() + } + + // Calculate the reduction interval associated with the requested height and + // attempt to look it up in cache. When it's not in the cache, look up the + // latest cached interval and subsidy while the mutex is still held for use + // below. + reqInterval := uint64(height / c.params.SubsidyReductionIntervalBlocks()) + c.mtx.RLock() + if cachedSubsidy, ok := c.cache[reqInterval]; ok { + c.mtx.RUnlock() + return cachedSubsidy + } + lastCachedInterval := c.cachedIntervals[len(c.cachedIntervals)-1] + lastCachedSubsidy := c.cache[lastCachedInterval] + c.mtx.RUnlock() + + // When the requested interval is after the latest cached interval, avoid + // additional work by either determining if the subsidy is already exhausted + // at that interval or using the interval as a starting point to calculate + // and store the subsidy for the requested interval. + // + // Otherwise, the requested interval is prior to the final cached interval, + // so use a binary search to find the latest cached interval prior to the + // requested one and use it as a starting point to calculate and store the + // subsidy for the requested interval. + if reqInterval > lastCachedInterval { + // Return zero for all intervals after the subsidy reaches zero. This + // enforces an upper bound on the number of entries in the cache. + if lastCachedSubsidy == 0 { + return 0 + } + } else { + c.mtx.RLock() + cachedIdx := sort.Search(len(c.cachedIntervals), func(i int) bool { + return c.cachedIntervals[i] >= reqInterval + }) + lastCachedInterval = c.cachedIntervals[cachedIdx-1] + lastCachedSubsidy = c.cache[lastCachedInterval] + c.mtx.RUnlock() + } + + // Finally, calculate the subsidy by applying the appropriate number of + // reductions per the starting and requested interval. + reductionMultiplier := c.params.SubsidyReductionMultiplier() + reductionDivisor := c.params.SubsidyReductionDivisor() + subsidy := lastCachedSubsidy + neededIntervals := reqInterval - lastCachedInterval + for i := uint64(0); i < neededIntervals; i++ { + subsidy *= reductionMultiplier + subsidy /= reductionDivisor + + // Stop once no further reduction is possible. This ensures a bounded + // computation for large requested intervals and that all future + // requests for intervals at or after the final reduction interval + // return 0 without recalculating. + if subsidy == 0 { + reqInterval = lastCachedInterval + i + 1 + break + } + } + + // Update the cache for the requested interval or the interval in which the + // subsidy became zero when applicable. The cached intervals are stored in + // a map for O(1) lookup and also tracked via a sorted array to support the + // binary searches for efficient sparse interval query support. + c.mtx.Lock() + c.cache[reqInterval] = subsidy + c.cachedIntervals = append(c.cachedIntervals, reqInterval) + sort.Sort((*uint64s)(&c.cachedIntervals)) + c.mtx.Unlock() + return subsidy +} + +// calcWorkSubsidy returns the proof of work subsidy for a block for a given +// number of votes using the provided work subsidy proportion which is expected +// to be an integer such that it produces the desired percentage when divided by +// 10. For example, a proportion of 6 will result in 6/10 = 60%. +// +// The calculated subsidy is 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 is the primary implementation logic used by CalcWorkSubsidy and only +// differs in that the proportion is parameterized. +// +// This function is safe for concurrent access. +func (c *SubsidyCache) calcWorkSubsidy(height int64, voters uint16, proportion uint16) int64 { + // The first block has special subsidy rules. + if height == 1 { + return c.params.BlockOneSubsidy() + } + + // The subsidy is zero if there are not enough voters once voting begins. A + // block without enough voters will fail to validate anyway. + stakeValidationHeight := c.params.StakeValidationBeginHeight() + if height >= stakeValidationHeight && voters < c.minVotesRequired { + return 0 + } + + // Calculate the full block subsidy and reduce it according to the PoW + // proportion. + subsidy := c.CalcBlockSubsidy(height) + subsidy *= int64(proportion) + subsidy /= int64(totalProportions) + + // Ignore any potential subsidy reductions due to the number of votes prior + // to the point voting begins. + if height < stakeValidationHeight { + return subsidy + } + + // Adjust for the number of voters. + return (int64(voters) * subsidy) / int64(c.params.VotesPerBlock()) +} + +// CalcWorkSubsidy 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) CalcWorkSubsidy(height int64, voters uint16, useDCP0010 bool) int64 { + if !useDCP0010 { + // The work subsidy proportion prior to DCP0010 is 60%. Thus it is 6 + // since 6/10 = 60%. + const workSubsidyProportion = 6 + return c.calcWorkSubsidy(height, voters, workSubsidyProportion) + } + + // The work subsidy proportion defined in DCP0010 is 10%. Thus it is 1 + // since 1/10 = 10%. + const workSubsidyProportion = 1 + return c.calcWorkSubsidy(height, voters, workSubsidyProportion) +} + +// calcStakeVoteSubsidy returns the subsidy for a single stake vote for a block +// using the provided stake vote subsidy proportion which is expected to be a +// value in the range [1,10] such that it produces the desired percentage when +// divided by 10. For example, a proportion of 3 will result in 3/10 = 30%. +// +// It is calculated as the provided proportion of the total subsidy and max +// potential number of votes per block. +// +// Unlike the Proof-of-Work subsidy, 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 is the primary implementation logic used by CalcStakeVoteSubsidy and +// only differs in that the proportion is parameterized. +// +// This function is safe for concurrent access. +func (c *SubsidyCache) calcStakeVoteSubsidy(height int64, proportion 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 + // included. + if height < c.params.StakeValidationBeginHeight()-1 { + return 0 + } + + // Calculate the full block subsidy and reduce it according to the stake + // proportion. Then divide it by the number of votes per block to arrive + // at the amount per vote. + subsidy := c.CalcBlockSubsidy(height) + subsidy *= int64(proportion) + subsidy /= (totalProportions * int64(c.params.VotesPerBlock())) + + return subsidy +} + +// CalcStakeVoteSubsidy 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 subsidy, 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) CalcStakeVoteSubsidy(height int64, useDCP0010 bool) int64 { + if !useDCP0010 { + // The vote subsidy proportion prior to DCP0010 is 30%. Thus it is 3 + // since 3/10 = 30%. + const voteSubsidyProportion = 3 + return c.calcStakeVoteSubsidy(height, voteSubsidyProportion) + } + + // The stake vote subsidy proportion defined in DCP0010 is 80%. Thus it is + // 8 since 8/10 = 80%. + const voteSubsidyProportion = 8 + return c.calcStakeVoteSubsidy(height, voteSubsidyProportion) +} + +// 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 +// 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. +// +// When the treasury agenda is active the subsidy rule changes from paying out +// a proportion based on the number of votes to always pay the full subsidy. +// +// This function is safe for concurrent access. +func (c *SubsidyCache) CalcTreasurySubsidy(height int64, voters uint16, useDCP0006 bool) int64 { + // The first two blocks have special subsidy rules. + if height <= 1 { + return 0 + } + + // The subsidy is zero if there are not enough voters once voting + // begins. A block without enough voters will fail to validate anyway. + stakeValidationHeight := c.params.StakeValidationBeginHeight() + if height >= stakeValidationHeight && voters < c.minVotesRequired { + return 0 + } + + // Calculate the full block subsidy and reduce it according to the treasury + // proportion. + // + // The treasury proportion is 10% both prior to DCP0010 and after it. Thus, + // it is 1 since 1/10 = 10%. + // + // However, there is no need to multiply by 1 since the result is the same. + subsidy := c.CalcBlockSubsidy(height) + subsidy /= totalProportions + + // Ignore any potential subsidy reductions due to the number of votes + // prior to the point voting begins or when DCP0006 is active. + if height < stakeValidationHeight || useDCP0006 { + return subsidy + } + + // Adjust for the number of voters. + return (int64(voters) * subsidy) / int64(c.params.VotesPerBlock()) +} diff --git a/internal/staging/primitives/subsidy_bench_test.go b/internal/staging/primitives/subsidy_bench_test.go new file mode 100644 index 0000000000..f2eeadc787 --- /dev/null +++ b/internal/staging/primitives/subsidy_bench_test.go @@ -0,0 +1,96 @@ +// Copyright (c) 2019-2022 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package primitives + +import ( + "testing" +) + +// BenchmarkCalcSubsidyCacheSparse benchmarks calculating the subsidy for +// various heights with a sparse access pattern. +func BenchmarkCalcSubsidyCacheSparse(b *testing.B) { + mockParams := mockMainNetParams() + reductionInterval := mockParams.SubsidyReductionIntervalBlocks() + + b.ResetTimer() + b.ReportAllocs() + const numIterations = 100 + for i := 0; i < b.N; i += (numIterations * 5) { + cache := NewSubsidyCache(mockParams) + for j := int64(0); j < numIterations; j++ { + cache.CalcBlockSubsidy(reductionInterval * (10000 + j)) + cache.CalcBlockSubsidy(reductionInterval * 1) + cache.CalcBlockSubsidy(reductionInterval * 5) + cache.CalcBlockSubsidy(reductionInterval * 25) + cache.CalcBlockSubsidy(reductionInterval * 13) + } + } +} + +// BenchmarkCalcWorkSubsidy benchmarks calculating the work subsidy proportion +// for various heights with a sparse access pattern, varying numbers of votes, +// and with and without DCP0010 active. +func BenchmarkCalcWorkSubsidy(b *testing.B) { + mockParams := mockMainNetParams() + reductionInterval := mockParams.SubsidyReductionIntervalBlocks() + + b.ResetTimer() + b.ReportAllocs() + const numIterations = 100 + for i := 0; i < b.N; i += (numIterations * 5) { + cache := NewSubsidyCache(mockParams) + for j := int64(0); j < numIterations; j++ { + cache.CalcWorkSubsidy(reductionInterval*(10000+j), 5, true) + cache.CalcWorkSubsidy(reductionInterval*1, 4, false) + cache.CalcWorkSubsidy(reductionInterval*5, 3, false) + cache.CalcWorkSubsidy(reductionInterval*25, 4, true) + cache.CalcWorkSubsidy(reductionInterval*13, 5, false) + } + } +} + +// BenchmarkCalcStakeVoteSubsidy benchmarks calculating the stake vote subsidy +// proportion for various heights with a sparse access pattern with and without +// DCP0010 active. +func BenchmarkCalcStakeVoteSubsidy(b *testing.B) { + mockParams := mockMainNetParams() + reductionInterval := mockParams.SubsidyReductionIntervalBlocks() + + b.ResetTimer() + b.ReportAllocs() + const numIterations = 100 + for i := 0; i < b.N; i += (numIterations * 5) { + cache := NewSubsidyCache(mockParams) + for j := int64(0); j < numIterations; j++ { + cache.CalcStakeVoteSubsidy(reductionInterval*(10000+j), true) + cache.CalcStakeVoteSubsidy(reductionInterval*1, false) + cache.CalcStakeVoteSubsidy(reductionInterval*5, false) + cache.CalcStakeVoteSubsidy(reductionInterval*25, true) + cache.CalcStakeVoteSubsidy(reductionInterval*13, false) + } + } +} + +// BenchmarkCalcTreasurySubsidy benchmarks calculating the treasury subsidy +// proportion for various heights with a sparse access pattern, varying numbers +// of votes, and with and without DCP0006 active. +func BenchmarkCalcTreasurySubsidy(b *testing.B) { + mockParams := mockMainNetParams() + reductionInterval := mockParams.SubsidyReductionIntervalBlocks() + + b.ResetTimer() + b.ReportAllocs() + const numIterations = 100 + for i := 0; i < b.N; i += (numIterations * 5) { + cache := NewSubsidyCache(mockParams) + for j := int64(0); j < numIterations; j++ { + cache.CalcTreasurySubsidy(reductionInterval*(10000+j), 5, true) + cache.CalcTreasurySubsidy(reductionInterval*1, 4, false) + cache.CalcTreasurySubsidy(reductionInterval*5, 3, false) + cache.CalcTreasurySubsidy(reductionInterval*25, 4, true) + cache.CalcTreasurySubsidy(reductionInterval*13, 5, true) + } + } +} diff --git a/internal/staging/primitives/subsidy_test.go b/internal/staging/primitives/subsidy_test.go new file mode 100644 index 0000000000..fe7da05f06 --- /dev/null +++ b/internal/staging/primitives/subsidy_test.go @@ -0,0 +1,736 @@ +// Copyright (c) 2019-2022 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package primitives + +import ( + "testing" +) + +// mockSubsidyParams implements the SubsidyParams interface and is used +// throughout the tests to mock networks. +type mockSubsidyParams struct { + blockOne int64 + baseSubsidy int64 + reductionMultiplier int64 + reductionDivisor int64 + reductionInterval int64 + stakeValidationHeight int64 + votesPerBlock uint16 +} + +// Ensure the mock subsidy params satisfy the SubsidyParams interface. +var _ SubsidyParams = (*mockSubsidyParams)(nil) + +// BlockOneSubsidy returns the value associated with the mock params for the +// total subsidy of block height 1 for the network. +func (p *mockSubsidyParams) BlockOneSubsidy() int64 { + return p.blockOne +} + +// BaseSubsidyValue returns the value associated with the mock params for the +// starting base max potential subsidy amount for mined blocks. +func (p *mockSubsidyParams) BaseSubsidyValue() int64 { + return p.baseSubsidy +} + +// SubsidyReductionMultiplier returns the value associated with the mock params +// for the multiplier to use when performing the exponential subsidy reduction +// described by the CalcBlockSubsidy documentation. +func (p *mockSubsidyParams) SubsidyReductionMultiplier() int64 { + return p.reductionMultiplier +} + +// SubsidyReductionDivisor returns the value associated with the mock params for +// the divisor to use when performing the exponential subsidy reduction +// described by the CalcBlockSubsidy documentation. +func (p *mockSubsidyParams) SubsidyReductionDivisor() int64 { + return p.reductionDivisor +} + +// SubsidyReductionIntervalBlocks returns the value associated with the mock +// params for the reduction interval in number of blocks. +func (p *mockSubsidyParams) SubsidyReductionIntervalBlocks() int64 { + return p.reductionInterval +} + +// StakeValidationBeginHeight returns the value associated with the mock params +// for the height at which votes become required to extend a block. +func (p *mockSubsidyParams) StakeValidationBeginHeight() int64 { + return p.stakeValidationHeight +} + +// VotesPerBlock returns the value associated with the mock params for the +// maximum number of votes a block must contain to receive full subsidy once +// voting begins at StakeValidationBeginHeight +func (p *mockSubsidyParams) VotesPerBlock() uint16 { + return p.votesPerBlock +} + +// mockMainNetParams returns mock mainnet subsidy parameters to use throughout +// the tests. They match the Decred mainnet params as of the time this comment +// was written. +func mockMainNetParams() *mockSubsidyParams { + return &mockSubsidyParams{ + blockOne: 168000000000000, + baseSubsidy: 3119582664, + reductionMultiplier: 100, + reductionDivisor: 101, + reductionInterval: 6144, + stakeValidationHeight: 4096, + votesPerBlock: 5, + } +} + +// TestSubsidyCacheCalcs ensures the subsidy cache calculates the various +// subsidy proportions and values as expected. +func TestSubsidyCacheCalcs(t *testing.T) { + t.Parallel() + + // Mock params used in tests. + params := mockMainNetParams() + + tests := []struct { + name string // test description + height int64 // height to calculate subsidy for + numVotes uint16 // number of votes + wantFull int64 // expected full block subsidy + wantWork int64 // expected pow subsidy w/o DCP0010 + wantWorkDCP0010 int64 // expected pow subsidy with DCP0010 + wantVote int64 // expected single vote subsidy + wantVoteDCP0010 int64 // expected single vote subsidy w/o DCP0010 + wantTrsy int64 // expected treasury subsidy w/o DCP0006 + wantTrsyDCP0006 int64 // expected treasury subsidy with DCP0006 + }{{ + name: "negative height", + height: -1, + numVotes: 0, + wantFull: 0, + wantWork: 0, + wantWorkDCP0010: 0, + wantVote: 0, + wantVoteDCP0010: 0, + wantTrsy: 0, + wantTrsyDCP0006: 0, + }, { + name: "height 0", + height: 0, + numVotes: 0, + wantFull: 0, + wantWork: 0, + wantWorkDCP0010: 0, + wantVote: 0, + wantVoteDCP0010: 0, + wantTrsy: 0, + wantTrsyDCP0006: 0, + }, { + name: "height 1 (initial payouts)", + height: 1, + numVotes: 0, + wantFull: 168000000000000, + wantWork: 168000000000000, + wantWorkDCP0010: 168000000000000, + wantVote: 0, + wantVoteDCP0010: 0, + wantTrsy: 0, + wantTrsyDCP0006: 0, + }, { + name: "height 2 (first non-special block prior voting start)", + height: 2, + numVotes: 0, + wantFull: 3119582664, + wantWork: 1871749598, + wantWorkDCP0010: 311958266, + wantVote: 0, + wantVoteDCP0010: 0, + wantTrsy: 311958266, + wantTrsyDCP0006: 311958266, + }, { + name: "height 4094 (two blocks prior to voting start)", + height: 4094, + numVotes: 0, + wantFull: 3119582664, + wantWork: 1871749598, + wantWorkDCP0010: 311958266, + wantVote: 0, + wantVoteDCP0010: 0, + wantTrsy: 311958266, + wantTrsyDCP0006: 311958266, + }, { + name: "height 4095 (final block prior to voting start)", + height: 4095, + numVotes: 0, + wantFull: 3119582664, + wantWork: 1871749598, + wantWorkDCP0010: 311958266, + wantVote: 187174959, + wantVoteDCP0010: 499133226, + wantTrsy: 311958266, + wantTrsyDCP0006: 311958266, + }, { + name: "height 4096 (voting start), 5 votes", + height: 4096, + numVotes: 5, + wantFull: 3119582664, + wantWork: 1871749598, + wantWorkDCP0010: 311958266, + wantVote: 187174959, + wantVoteDCP0010: 499133226, + wantTrsy: 311958266, + wantTrsyDCP0006: 311958266, + }, { + name: "height 4096 (voting start), 4 votes", + height: 4096, + numVotes: 4, + wantFull: 3119582664, + wantWork: 1497399678, + wantWorkDCP0010: 249566612, + wantVote: 187174959, + wantVoteDCP0010: 499133226, + wantTrsy: 249566612, + wantTrsyDCP0006: 311958266, // No reduction + }, { + name: "height 4096 (voting start), 3 votes", + height: 4096, + numVotes: 3, + wantFull: 3119582664, + wantWork: 1123049758, + wantWorkDCP0010: 187174959, + wantVote: 187174959, + wantVoteDCP0010: 499133226, + wantTrsy: 187174959, + wantTrsyDCP0006: 311958266, // No reduction + }, { + name: "height 4096 (voting start), 2 votes", + height: 4096, + numVotes: 2, + wantFull: 3119582664, + wantWork: 0, + wantWorkDCP0010: 0, + wantVote: 187174959, + wantVoteDCP0010: 499133226, + wantTrsy: 0, + wantTrsyDCP0006: 0, + }, { + name: "height 6143 (final block prior to 1st reduction), 5 votes", + height: 6143, + numVotes: 5, + wantFull: 3119582664, + wantWork: 1871749598, + wantWorkDCP0010: 311958266, + wantVote: 187174959, + wantVoteDCP0010: 499133226, + wantTrsy: 311958266, + wantTrsyDCP0006: 311958266, + }, { + name: "height 6144 (1st block in 1st reduction), 5 votes", + height: 6144, + numVotes: 5, + wantFull: 3088695706, + wantWork: 1853217423, + wantWorkDCP0010: 308869570, + wantVote: 185321742, + wantVoteDCP0010: 494191312, + wantTrsy: 308869570, + wantTrsyDCP0006: 308869570, + }, { + name: "height 6144 (1st block in 1st reduction), 4 votes", + height: 6144, + numVotes: 4, + wantFull: 3088695706, + wantWork: 1482573938, + wantWorkDCP0010: 247095656, + wantVote: 185321742, + wantVoteDCP0010: 494191312, + wantTrsy: 247095656, + wantTrsyDCP0006: 308869570, // No reduction + }, { + name: "height 12287 (last block in 1st reduction), 5 votes", + height: 12287, + numVotes: 5, + wantFull: 3088695706, + wantWork: 1853217423, + wantWorkDCP0010: 308869570, + wantVote: 185321742, + wantVoteDCP0010: 494191312, + wantTrsy: 308869570, + wantTrsyDCP0006: 308869570, + }, { + name: "height 12288 (1st block in 2nd reduction), 5 votes", + height: 12288, + numVotes: 5, + wantFull: 3058114560, + wantWork: 1834868736, + wantWorkDCP0010: 305811456, + wantVote: 183486873, + wantVoteDCP0010: 489298329, + wantTrsy: 305811456, + wantTrsyDCP0006: 305811456, + }, { + name: "height 307200 (1st block in 50th reduction), 5 votes", + height: 307200, + numVotes: 5, + wantFull: 1896827356, + wantWork: 1138096413, + wantWorkDCP0010: 189682735, + wantVote: 113809641, + wantVoteDCP0010: 303492376, + wantTrsy: 189682735, + wantTrsyDCP0006: 189682735, + }, { + name: "height 307200 (1st block in 50th reduction), 3 votes", + height: 307200, + numVotes: 3, + wantFull: 1896827356, + wantWork: 682857847, + wantWorkDCP0010: 113809641, + wantVote: 113809641, + wantVoteDCP0010: 303492376, + wantTrsy: 113809641, + wantTrsyDCP0006: 189682735, // No reduction + }, { + // First zero vote subsidy without DCP0010. + name: "height 10911744 (1776th reduction), 5 votes", + height: 10911744, + numVotes: 5, + wantFull: 16, + wantWork: 9, + wantWorkDCP0010: 1, + wantVote: 0, + wantVoteDCP0010: 2, + wantTrsy: 1, + wantTrsyDCP0006: 1, + }, { + // First zero treasury subsidy. + // First zero work subsidy with DCP0010 + name: "height 10954752 (1783rd reduction), 5 votes", + height: 10954752, + numVotes: 5, + wantFull: 9, + wantWork: 5, + wantWorkDCP0010: 0, + wantVote: 0, + wantVoteDCP0010: 1, + wantTrsy: 0, + wantTrsyDCP0006: 0, + }, { + // First zero vote subsidy with DCP0010. + name: "height 10973184 (1786th reduction), 5 votes", + height: 10973184, + numVotes: 5, + wantFull: 6, + wantWork: 3, + wantWorkDCP0010: 0, + wantVote: 0, + wantVoteDCP0010: 0, + wantTrsy: 0, + wantTrsyDCP0006: 0, + }, { + // First zero work subsidy without DCP0010. + name: "height 11003904 (1791st reduction), 5 votes", + height: 11003904, + numVotes: 5, + wantFull: 1, + wantWork: 0, + wantWorkDCP0010: 0, + wantVote: 0, + wantVoteDCP0010: 0, + wantTrsy: 0, + wantTrsyDCP0006: 0, + }, { + // First zero full subsidy. + name: "height 11010048 (1792nd reduction), 5 votes", + height: 11010048, + numVotes: 5, + wantFull: 0, + wantWork: 0, + wantWorkDCP0010: 0, + wantVote: 0, + wantVoteDCP0010: 0, + wantTrsy: 0, + wantTrsyDCP0006: 0, + }} + + for _, test := range tests { + // Ensure the full subsidy is the expected value. + cache := NewSubsidyCache(params) + fullSubsidyResult := cache.CalcBlockSubsidy(test.height) + if fullSubsidyResult != test.wantFull { + t.Errorf("%s: unexpected full subsidy result -- got %d, want %d", + test.name, fullSubsidyResult, test.wantFull) + continue + } + + // Ensure the PoW subsidy is the expected value both with and without + // DCP0010. + gotWork := cache.CalcWorkSubsidy(test.height, test.numVotes, false) + if gotWork != test.wantWork { + t.Errorf("%s: unexpected work subsidy result -- got %d, want %d", + test.name, gotWork, test.wantWork) + continue + } + gotWork = cache.CalcWorkSubsidy(test.height, test.numVotes, true) + if gotWork != test.wantWorkDCP0010 { + t.Errorf("%s: unexpected work subsidy result (DCP0010) -- got %d, "+ + "want %d", test.name, gotWork, test.wantWorkDCP0010) + continue + } + + // Ensure the vote subsidy is the expected value both with and without + // DCP00010. + gotVote := cache.CalcStakeVoteSubsidy(test.height, false) + if gotVote != test.wantVote { + t.Errorf("%s: unexpected vote subsidy result -- got %d, want %d", + test.name, gotVote, test.wantVote) + continue + } + gotVote = cache.CalcStakeVoteSubsidy(test.height, true) + if gotVote != test.wantVoteDCP0010 { + t.Errorf("%s: unexpected vote subsidy result (DCP0010) -- got %d, "+ + "want %d", test.name, gotVote, test.wantVoteDCP0010) + continue + } + + // Ensure the treasury subsidy is the expected value both with and + // without DCP0006. + gotTrsy := cache.CalcTreasurySubsidy(test.height, test.numVotes, false) + if gotTrsy != test.wantTrsy { + t.Errorf("%s: unexpected treasury subsidy result -- got %d, want %d", + test.name, gotTrsy, test.wantTrsy) + continue + } + gotTrsy = cache.CalcTreasurySubsidy(test.height, test.numVotes, true) + if gotTrsy != test.wantTrsyDCP0006 { + t.Errorf("%s: unexpected treasury subsidy result (DCP0006) -- got "+ + "%d, want %d", test.name, gotTrsy, test.wantTrsyDCP0006) + continue + } + } +} + +// TestTotalSubsidy ensures the total subsidy produced matches the expected +// value both with and without the flag for the decentralized treasury agenda +// defined in DCP0006 set. +func TestTotalSubsidy(t *testing.T) { + t.Parallel() + + // Locals for convenience. + mockMainNetParams := mockMainNetParams() + reductionInterval := mockMainNetParams.SubsidyReductionIntervalBlocks() + stakeValidationHeight := mockMainNetParams.StakeValidationBeginHeight() + votesPerBlock := mockMainNetParams.VotesPerBlock() + + checkTotalSubsidy := func(useDCP0006 bool) { + t.Helper() + + // subsidySum returns the sum of the individual subsidy types for the + // given height. 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) int64 { + const useDCP0010 = false + work := cache.CalcWorkSubsidy(height, votesPerBlock, useDCP0010) + vote := cache.CalcStakeVoteSubsidy(height, useDCP0010) + vote *= int64(votesPerBlock) + trsy := cache.CalcTreasurySubsidy(height, votesPerBlock, useDCP0006) + return work + vote + trsy + } + + // 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) * nonVotingBlocks + + // Account for the blocks remaining in the interval once voting + // begins. + subsidyCalcHeight = stakeValidationHeight + votingBlocks := reductionInterval - subsidyCalcHeight + totalSubsidy += subsidySum(subsidyCalcHeight) * votingBlocks + continue + } + + // Account for the all other reduction intervals until all subsidy + // has been produced. + subsidyCalcHeight := reductionNum * reductionInterval + sum := subsidySum(subsidyCalcHeight) + if sum == 0 { + break + } + totalSubsidy += sum * reductionInterval + } + + const expectedTotalSubsidy = 2099999999800912 + if totalSubsidy != expectedTotalSubsidy { + t.Fatalf("mismatched total subsidy (treasury flag: %v) -- got %d, "+ + "want %d", useDCP0006, totalSubsidy, expectedTotalSubsidy) + } + } + + // Ensure the total calculated subsidy is the expected value both with and + // without the flag for the decentralized treasury agenda + // defined in DCP0006 set + checkTotalSubsidy(false) + checkTotalSubsidy(true) +} + +// TestTotalSubsidyDCP0010 ensures the estimated total subsidy produced with the +// subsidy split defined in DCP0010 matches the expected value. +func TestTotalSubsidyDCP0010(t *testing.T) { + t.Parallel() + + // 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 { + const useDCP0006 = false + work := cache.CalcWorkSubsidy(height, votesPerBlock, useDCP0010) + vote := cache.CalcStakeVoteSubsidy(height, useDCP0010) * + int64(votesPerBlock) + treasury := cache.CalcTreasurySubsidy(height, votesPerBlock, useDCP0006) + 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) { + t.Parallel() + + // Mock params used in tests. + mockMainNetParams := mockMainNetParams() + + // perCacheTest describes a test to run against the same cache. + type perCacheTest struct { + name string // test description + height int64 // height to calculate subsidy for + want int64 // expected subsidy + } + + tests := []struct { + name string // test description + params SubsidyParams // params to use in subsidy calculations + perCacheTests []perCacheTest // tests to run against same cache instance + }{{ + name: "negative/zero/one (special cases, no cache)", + params: mockMainNetParams, + perCacheTests: []perCacheTest{{ + name: "would be negative interval", + height: -6144, + want: 0, + }, { + name: "negative one", + height: -1, + want: 0, + }, { + name: "height 0", + height: 0, + want: 0, + }, { + name: "height 1", + height: 1, + want: 168000000000000, + }}, + }, { + name: "clean cache, negative height", + params: mockMainNetParams, + perCacheTests: []perCacheTest{{ + name: "would be negative interval", + height: -6144, + want: 0, + }, { + name: "height 0", + height: 0, + want: 0, + }}, + }, { + name: "clean cache, max int64 height twice", + params: mockMainNetParams, + perCacheTests: []perCacheTest{{ + name: "max int64", + height: 9223372036854775807, + want: 0, + }, { + name: "second max int64", + height: 9223372036854775807, + want: 0, + }}, + }, { + name: "sparse out order interval requests with cache hits", + params: mockMainNetParams, + perCacheTests: []perCacheTest{{ + name: "height 0", + height: 0, + want: 0, + }, { + name: "height 1", + height: 1, + want: 168000000000000, + }, { + name: "height 2 (cause interval 0 cache addition)", + height: 2, + want: 3119582664, + }, { + name: "height 2 (interval 0 cache hit)", + height: 2, + want: 3119582664, + }, { + name: "height 3 (interval 0 cache hit)", + height: 2, + want: 3119582664, + }, { + name: "height 6145 (interval 1 cache addition)", + height: 6145, + want: 3088695706, + }, { + name: "height 6145 (interval 1 cache hit)", + height: 6145, + want: 3088695706, + }, { + name: "interval 20 cache addition most recent cache interval 1", + height: 6144 * 20, + want: 2556636713, + }, { + name: "interval 20 cache hit", + height: 6144 * 20, + want: 2556636713, + }, { + name: "interval 10 cache addition most recent cache interval 20", + height: 6144 * 10, + want: 2824117486, + }, { + name: "interval 10 cache hit", + height: 6144 * 10, + want: 2824117486, + }, { + name: "interval 15 cache addition between cached 10 and 20", + height: 6144 * 15, + want: 2687050883, + }, { + name: "interval 15 cache hit", + height: 6144 * 15, + want: 2687050883, + }, { + name: "interval 1792 (first with 0 subsidy) cache addition", + height: 6144 * 1792, + want: 0, + }, { + name: "interval 1792 cache hit", + height: 6144 * 1792, + want: 0, + }, { + name: "interval 1795 (skipping final 0 subsidy)", + height: 6144 * 1795, + want: 0, + }}, + }, { + name: "clean cache, reverse interval requests", + params: mockMainNetParams, + perCacheTests: []perCacheTest{{ + name: "interval 5 cache addition", + height: 6144 * 5, + want: 2968175862, + }, { + name: "interval 3 cache addition", + height: 6144 * 3, + want: 3027836198, + }, { + name: "interval 3 cache hit", + height: 6144 * 3, + want: 3027836198, + }}, + }, { + name: "clean cache, forward non-zero start interval requests", + params: mockMainNetParams, + perCacheTests: []perCacheTest{{ + name: "interval 2 cache addition", + height: 6144 * 2, + want: 3058114560, + }, { + name: "interval 12 cache addition", + height: 6144 * 12, + want: 2768471213, + }, { + name: "interval 12 cache hit", + height: 6144 * 12, + want: 2768471213, + }}, + }} + + for _, test := range tests { + cache := NewSubsidyCache(test.params) + for _, pcTest := range test.perCacheTests { + result := cache.CalcBlockSubsidy(pcTest.height) + if result != pcTest.want { + t.Errorf("%q-%q: mismatched subsidy -- got %d, want %d", + test.name, pcTest.name, result, pcTest.want) + continue + } + } + } +}