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 + } + } + } +}