From bf2015b51028f3f03d600604659576781d70ebb2 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Sun, 4 Aug 2019 23:44:37 -0500 Subject: [PATCH] blockchain/standalone: Add subsidy calc funcs. This adds a subsidy cache with funcs to calculate subsidies for proof-of-work, votes, and the treasury to the blockchain/standalone module. Since the existing functions in blockchain are easy to use improperly and have poor performance under certain access patterns, rather than copying them over, this provide new implementations of better optimized functions with less potential cases for misuse. Note that because subsidy generation is consensus critical, great care has been taken to ensure the same semantics are retained under the scenarios which the consensus code invokes the functions, although the semantics of the functions themselves have changed when compared to their existing counterparts when called in circumstances that the consensus code avoids as discussed below. The existing subsidy code in blockchain assumes that it will never be called in circumstances which would violate consensus such as with negative block heights and when there are less than the minimum required number of votes and consequently produces invalid subsidy values in those types of cases. This does not pose a problem for the existing consensus code itself because it does, in fact, correctly avoid calling the functions in scenarios that would produce incorrect results. However, since these functions are exported for external use, forcing consumers to only call the functions under the (previously undocumented) correct scenarios is highly error prone. Consequently, these new functions produce results that match the consensus behavior even if called under those circumstances which are currently avoided. The following is an overview of the changes to the results of the functions as compared to their existing counterparts: - Negative block heights now returns 0 subsidy in all cases - Height 0 now returns 0 subsidy in all cases - Height 1 now returns the full amount for the PoW subsidy and 0 for the vote and treasury subsidies as compared to reduced values that the actual consensus rules do not use - Heights prior to one block before the point at which voting begins now return 0 vote subsidy as compared to reduced values that the actual consensus rules ignore at those heights - Specifying less than the minimum number of votes required for a block to be considered valid by consensus along with heights at or after the point voting begins now returns 0 PoW and Treasury subsidies as compared to further reduced values that the actual consensus rules ignore due to the block being invalid A few other improvements include: - All subsidy calculation funcs are now methods on the cache instance which means it is no longer possible to corrupt the cache by calling the functions with different network parameters - The cache can no longer be made to grow indefinitely - Note that this is not an exploit in the existing code since the internal code is very restrictive in how it uses the functions as previously mentioned - Improves future scalability by avoiding recalculations on all intervals after the max potential block subsidy has been fully reduced to 0 - Improve sparse access efficiency to O(log N) vs O(N) Finally, it also adds a benchmark, updates the documentation, and includes comprehensive tests. The following is a before and after comparison of calculating subsidies for various sparse intervals: benchmark old ns/op new ns/op delta ------------------------------------------------------------------- BenchmarkCalcSubsidyCacheSparse 56766 17243 -69.62% benchmark old allocs new allocs delta ------------------------------------------------------------------- BenchmarkCalcSubsidyCacheSparse 6 5 -16.67% benchmark old bytes new bytes delta ------------------------------------------------------------------- BenchmarkCalcSubsidyCacheSparse 1163 416 -64.23% --- blockchain/standalone/README.md | 5 +- blockchain/standalone/bench_test.go | 20 + blockchain/standalone/doc.go | 5 +- blockchain/standalone/go.sum | 1 + blockchain/standalone/subsidy.go | 347 +++++++++++++++ blockchain/standalone/subsidy_test.go | 610 ++++++++++++++++++++++++++ 6 files changed, 986 insertions(+), 2 deletions(-) create mode 100644 blockchain/standalone/subsidy.go create mode 100644 blockchain/standalone/subsidy_test.go diff --git a/blockchain/standalone/README.md b/blockchain/standalone/README.md index 08eb0360..1b7c1440 100644 --- a/blockchain/standalone/README.md +++ b/blockchain/standalone/README.md @@ -29,7 +29,10 @@ The provided functions fall into the following categories: - Merkle root calculation - Calculation from individual leaf hashes - Calculation from a slice of transactions -- Subsidy calculation (WIP - not yet available) +- Subsidy calculation + - Proof-of-work subsidy for a given height and number of votes + - Stake vote subsidy for a given height + - Treasury subsidy for a given height and number of votes - Coinbase transaction identification (WIP - not yet available) ## Installation and Updating diff --git a/blockchain/standalone/bench_test.go b/blockchain/standalone/bench_test.go index 4fe75168..34e85b1e 100644 --- a/blockchain/standalone/bench_test.go +++ b/blockchain/standalone/bench_test.go @@ -56,3 +56,23 @@ func BenchmarkCalcMerkleRoot(b *testing.B) { }) } } + +// 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() + for i := 0; i < b.N; i++ { + cache := NewSubsidyCache(mockParams) + for j := int64(0); j < 10; j++ { + cache.CalcBlockSubsidy(reductionInterval * (10000 + j)) + cache.CalcBlockSubsidy(reductionInterval * 1) + cache.CalcBlockSubsidy(reductionInterval * 5) + cache.CalcBlockSubsidy(reductionInterval * 25) + cache.CalcBlockSubsidy(reductionInterval * 13) + } + } +} diff --git a/blockchain/standalone/doc.go b/blockchain/standalone/doc.go index 7231364a..145b69ef 100644 --- a/blockchain/standalone/doc.go +++ b/blockchain/standalone/doc.go @@ -27,7 +27,10 @@ The provided functions fall into the following categories: - Merkle root calculation - Calculation from individual leaf hashes - Calculation from a slice of transactions - - Subsidy calculation (WIP - not yet available) + - Subsidy calculation + - Proof-of-work subsidy for a given height and number of votes + - Stake vote subsidy for a given height + - Treasury subsidy for a given height and number of votes - Coinbase transaction identification (WIP - not yet available) Errors diff --git a/blockchain/standalone/go.sum b/blockchain/standalone/go.sum index 1908e732..0b3a7ea2 100644 --- a/blockchain/standalone/go.sum +++ b/blockchain/standalone/go.sum @@ -1,3 +1,4 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dchest/blake256 v1.0.0 h1:6gUgI5MHdz9g0TdrgKqXsoDX+Zjxmm1Sc6OsoGru50I= github.com/dchest/blake256 v1.0.0/go.mod h1:xXNWCE1jsAP8DAjP+rKw2MbeqLczjI3TRx2VK+9OEYY= diff --git a/blockchain/standalone/subsidy.go b/blockchain/standalone/subsidy.go new file mode 100644 index 00000000..10fca517 --- /dev/null +++ b/blockchain/standalone/subsidy.go @@ -0,0 +1,347 @@ +// Copyright (c) 2015-2019 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package standalone + +import ( + "sort" + "sync" +) + +// 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: BaseSubsidy 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 + + // These parameters control the proportional split of the max potential + // block subsidy between PoW, PoS, and the Treasury. + + // WorkSubsidyProportion returns the comparative proportion of the subsidy + // generated for creating a block (PoW). + // + // 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 + // the three proportional parameters: WorkSubsidyProportion, + // StakeSubsidyProportion, and TreasurySubsidyProportion. + // + // For example: + // WorkSubsidyProportion: 6 => 6 / (6+3+1) => 6/10 => 60% + // StakeSubsidyProportion: 3 => 3 / (6+3+1) => 3/10 => 30% + // TreasurySubsidyProportion: 1 => 1 / (6+3+1) => 1/10 => 10% + WorkSubsidyProportion() uint16 + + // StakeSubsidyProportion returns the comparative proportion of the subsidy + // generated for casting stake votes (collectively, per block). See the + // documentation for WorkSubsidyProportion for more details on how the + // parameter is used. + StakeSubsidyProportion() uint16 + + // TreasurySubsidyProportion returns the comparative proportion of the + // subsidy allocated to the project treasury. See the documentation for + // WorkSubsidyProportion for more details on how the parameter is used. + TreasurySubsidyProportion() uint16 + + // 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 +} + +// 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 using of caching to avoid repeated calculations. +type SubsidyCache struct { + // The following fields are protected by the mtx mutex. + // + // 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 + + // These fields house values calculated from the parameters in order to + // avoid repeated calculation. + // + // minVotesRequired is the minimum number of votes required for a block to + // be consider valid by consensus. + // + // totalProportions is the sum of the PoW, PoS, and Treasury proportions. + minVotesRequired uint16 + totalProportions 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, + totalProportions: params.WorkSubsidyProportion() + + params.StakeSubsidyProportion() + + params.TreasurySubsidyProportion(), + } +} + +// 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 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. 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) 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(c.params.WorkSubsidyProportion()) + subsidy /= int64(c.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()) +} + +// 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. +// +// 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) CalcStakeVoteSubsidy(height int64) 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) + proportions := int64(c.totalProportions) + subsidy *= int64(c.params.StakeSubsidyProportion()) + subsidy /= (proportions * int64(c.params.VotesPerBlock())) + + return subsidy +} + +// 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. +// +// This function is safe for concurrent access. +func (c *SubsidyCache) CalcTreasurySubsidy(height int64, voters uint16) 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. + subsidy := c.CalcBlockSubsidy(height) + subsidy *= int64(c.params.TreasurySubsidyProportion()) + subsidy /= int64(c.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()) +} diff --git a/blockchain/standalone/subsidy_test.go b/blockchain/standalone/subsidy_test.go new file mode 100644 index 00000000..c17bc6e3 --- /dev/null +++ b/blockchain/standalone/subsidy_test.go @@ -0,0 +1,610 @@ +// Copyright (c) 2019 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package standalone + +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 + workProportion uint16 + voteProportion uint16 + treasuryProportion uint16 + 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. +// +// This is part of the SubsidyParams interface. +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. +// +// This is part of the SubsidyParams interface. +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. +// +// This is part of the SubsidyParams interface. +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. +// +// This is part of the SubsidyParams interface. +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. +// +// This is part of the SubsidyParams interface. +func (p *mockSubsidyParams) SubsidyReductionIntervalBlocks() int64 { + return p.reductionInterval +} + +// WorkSubsidyProportion returns the value associated with the mock params for +// the comparative proportion of the subsidy generated for creating a block +// (PoW). +// +// This is part of the SubsidyParams interface. +func (p *mockSubsidyParams) WorkSubsidyProportion() uint16 { + return p.workProportion +} + +// StakeSubsidyProportion returns the value associated with the mock params for +// the comparative proportion of the subsidy generated for casting stake votes +// (collectively, per block). +// +// This is part of the SubsidyParams interface. +func (p *mockSubsidyParams) StakeSubsidyProportion() uint16 { + return p.voteProportion +} + +// TreasurySubsidyProportion returns the value associated with the mock params +// for the comparative proportion of the subsidy allocated to the project +// treasury. +// +// This is part of the SubsidyParams interface. +func (p *mockSubsidyParams) TreasurySubsidyProportion() uint16 { + return p.treasuryProportion +} + +// StakeValidationBeginHeight returns the value associated with the mock params +// for the height at which votes become required to extend a block. +// +// This is part of the SubsidyParams interface. +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 +// +// This is part of the SubsidyParams interface. +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, + workProportion: 6, + voteProportion: 3, + treasuryProportion: 1, + stakeValidationHeight: 4096, + votesPerBlock: 5, + } +} + +// TestSubsidyCacheCalcs ensures the subsidy cache calculates the various +// subsidy proportions and values as expected. +func TestSubsidyCacheCalcs(t *testing.T) { + // Mock params used in tests. + mockMainNetParams := mockMainNetParams() + + tests := []struct { + name string // test description + params SubsidyParams // params to use in subsidy calculations + height int64 // height to calculate subsidy for + numVotes uint16 // number of votes + wantFull int64 // expected full block subsidy + wantWork int64 // expected pow subsidy + wantVote int64 // expected single vote subsidy + wantTreasury int64 // expected treasury subsidy + }{{ + name: "negative height", + params: mockMainNetParams, + height: -1, + numVotes: 0, + wantFull: 0, + wantWork: 0, + wantVote: 0, + wantTreasury: 0, + }, { + name: "height 0", + params: mockMainNetParams, + height: 0, + numVotes: 0, + wantFull: 0, + wantWork: 0, + wantVote: 0, + wantTreasury: 0, + }, { + name: "height 1 (initial payouts)", + params: mockMainNetParams, + height: 1, + numVotes: 0, + wantFull: 168000000000000, + wantWork: 168000000000000, + wantVote: 0, + wantTreasury: 0, + }, { + name: "height 2 (first non-special block prior voting start)", + params: mockMainNetParams, + height: 2, + numVotes: 0, + wantFull: 3119582664, + wantWork: 1871749598, + wantVote: 0, + wantTreasury: 311958266, + }, { + name: "height 4094 (two blocks prior to voting start)", + params: mockMainNetParams, + height: 4094, + numVotes: 0, + wantFull: 3119582664, + wantWork: 1871749598, + wantVote: 0, + wantTreasury: 311958266, + }, { + name: "height 4095 (final block prior to voting start)", + params: mockMainNetParams, + height: 4095, + numVotes: 0, + wantFull: 3119582664, + wantWork: 1871749598, + wantVote: 187174959, + wantTreasury: 311958266, + }, { + name: "height 4096 (voting start), 5 votes", + params: mockMainNetParams, + height: 4096, + numVotes: 5, + wantFull: 3119582664, + wantWork: 1871749598, + wantVote: 187174959, + wantTreasury: 311958266, + }, { + name: "height 4096 (voting start), 4 votes", + params: mockMainNetParams, + height: 4096, + numVotes: 4, + wantFull: 3119582664, + wantWork: 1497399678, + wantVote: 187174959, + wantTreasury: 249566612, + }, { + name: "height 4096 (voting start), 3 votes", + params: mockMainNetParams, + height: 4096, + numVotes: 3, + wantFull: 3119582664, + wantWork: 1123049758, + wantVote: 187174959, + wantTreasury: 187174959, + }, { + name: "height 4096 (voting start), 2 votes", + params: mockMainNetParams, + height: 4096, + numVotes: 2, + wantFull: 3119582664, + wantWork: 0, + wantVote: 187174959, + wantTreasury: 0, + }, { + name: "height 6143 (final block prior to 1st reduction), 5 votes", + params: mockMainNetParams, + height: 6143, + numVotes: 5, + wantFull: 3119582664, + wantWork: 1871749598, + wantVote: 187174959, + wantTreasury: 311958266, + }, { + name: "height 6144 (1st block in 1st reduction), 5 votes", + params: mockMainNetParams, + height: 6144, + numVotes: 5, + wantFull: 3088695706, + wantWork: 1853217423, + wantVote: 185321742, + wantTreasury: 308869570, + }, { + name: "height 6144 (1st block in 1st reduction), 4 votes", + params: mockMainNetParams, + height: 6144, + numVotes: 4, + wantFull: 3088695706, + wantWork: 1482573938, + wantVote: 185321742, + wantTreasury: 247095656, + }, { + name: "height 12287 (last block in 1st reduction), 5 votes", + params: mockMainNetParams, + height: 12287, + numVotes: 5, + wantFull: 3088695706, + wantWork: 1853217423, + wantVote: 185321742, + wantTreasury: 308869570, + }, { + name: "height 12288 (1st block in 2nd reduction), 5 votes", + params: mockMainNetParams, + height: 12288, + numVotes: 5, + wantFull: 3058114560, + wantWork: 1834868736, + wantVote: 183486873, + wantTreasury: 305811456, + }, { + name: "height 307200 (1st block in 50th reduction), 5 votes", + params: mockMainNetParams, + height: 307200, + numVotes: 5, + wantFull: 1896827356, + wantWork: 1138096413, + wantVote: 113809641, + wantTreasury: 189682735, + }, { + name: "height 307200 (1st block in 50th reduction), 3 votes", + params: mockMainNetParams, + height: 307200, + numVotes: 3, + wantFull: 1896827356, + wantWork: 682857847, + wantVote: 113809641, + wantTreasury: 113809641, + }, { + name: "height 10911744 (first zero vote subsidy 1776th reduction), 5 votes", + params: mockMainNetParams, + height: 10911744, + numVotes: 5, + wantFull: 16, + wantWork: 9, + wantVote: 0, + wantTreasury: 1, + }, { + name: "height 10954752 (first zero treasury subsidy 1783rd reduction), 5 votes", + params: mockMainNetParams, + height: 10954752, + numVotes: 5, + wantFull: 9, + wantWork: 5, + wantVote: 0, + wantTreasury: 0, + }, { + name: "height 11003904 (first zero work subsidy 1791st reduction), 5 votes", + params: mockMainNetParams, + height: 11003904, + numVotes: 5, + wantFull: 1, + wantWork: 0, + wantVote: 0, + wantTreasury: 0, + }, { + name: "height 11010048 (first zero full subsidy 1792nd reduction), 5 votes", + params: mockMainNetParams, + height: 11010048, + numVotes: 5, + wantFull: 0, + wantWork: 0, + wantVote: 0, + wantTreasury: 0, + }} + + for _, test := range tests { + // Ensure the full subsidy is the expected value. + cache := NewSubsidyCache(test.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. + workResult := cache.CalcWorkSubsidy(test.height, test.numVotes) + if workResult != test.wantWork { + t.Errorf("%s: unexpected work subsidy result -- got %d, want %d", + test.name, workResult, test.wantWork) + continue + } + + // Ensure the vote subsidy is the expected value. + voteResult := cache.CalcStakeVoteSubsidy(test.height) + if voteResult != test.wantVote { + t.Errorf("%s: unexpected vote subsidy result -- got %d, want %d", + test.name, voteResult, test.wantVote) + continue + } + + // Ensure the treasury subsidy is the expected value. + treasuryResult := cache.CalcTreasurySubsidy(test.height, test.numVotes) + if treasuryResult != test.wantTreasury { + t.Errorf("%s: unexpected treasury subsidy result -- got %d, want %d", + test.name, treasuryResult, test.wantTreasury) + continue + } + } +} + +// TestTotalSubsidy ensures the total subsidy produced matches the expected +// value. +func TestTotalSubsidy(t *testing.T) { + // Locals for convenience. + mockMainNetParams := mockMainNetParams() + reductionInterval := mockMainNetParams.SubsidyReductionIntervalBlocks() + stakeValidationHeight := mockMainNetParams.StakeValidationBeginHeight() + votesPerBlock := mockMainNetParams.VotesPerBlock() + + // 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 { + work := cache.CalcWorkSubsidy(height, votesPerBlock) + vote := cache.CalcStakeVoteSubsidy(height) * int64(votesPerBlock) + treasury := cache.CalcTreasurySubsidy(height, votesPerBlock) + 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) * 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 + } + + // Ensure the total calculated subsidy is the expected value. + const expectedTotalSubsidy = 2099999999800912 + 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) { + // 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 + } + } + } +}