-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathsharder_fee_rewards_test.go
328 lines (308 loc) · 11.9 KB
/
sharder_fee_rewards_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
package cli_tests
import (
"strings"
"testing"
"time"
"github.com/0chain/system_test/internal/api/util/test"
climodel "github.com/0chain/system_test/internal/cli/model"
cliutil "github.com/0chain/system_test/internal/cli/util"
"github.com/stretchr/testify/require"
)
func TestSharderFeeRewards(testSetup *testing.T) { // nolint:gocyclo // team preference is to have codes all within test.
t := test.NewSystemTest(testSetup)
// Take a snapshot of the chains sharders, then repeat a transaction with a fee a few times, take another snapshot.
// Examine the rewards paid between the two snapshot and confirm the self-consistency
// of the reward payments
//
// Each round a random sharder is chosen to receive the block reward.
// The sharder's service charge is used to determine the fraction received by the sharder's wallet.
// The remaining reward is then distributed amongst the sharder's delegates.
// A subset of the delegates chosen at random to receive a portion of the block reward.
// The total received by each stake pool is proportional to the tokens they have locked
// wither respect to the total locked by the chosen delegate pools.
t.RunSequentially("Sharder share of fee rewards for transactions", func(t *test.SystemTest) {
createWallet(t)
wallet, err := getWalletForName(t, configPath, escapedTestName(t)+"_TARGET")
require.NoError(t, err, "error getting target wallet")
if !confirmDebugBuild(t) {
t.Skip("sharder fee rewards test skipped as it requires a debug event database")
}
sharderUrl := getSharderUrl(t)
var sharderIds []string
var beforeSharders climodel.NodeList
sharderIds, beforeSharders = waitForNSharder(t, sharderUrl, 1)
// ------------------------------------
const numPaidTransactions = 3
const fee = 0.1
for i := 0; i < numPaidTransactions; i++ {
output, err := sendTokens(t, configPath, wallet.ClientID, 0.5, escapedTestName(t), fee)
require.NoError(t, err, "error sending tokens", strings.Join(output, "\n"))
}
time.Sleep(time.Second) // give time for last round to be saved
// ------------------------------------
afterSharders := getNodes(t, sharderIds, sharderUrl)
// we add rewards at the end of the round, and they don't appear until the next round
startRound, endRound := getStartAndEndRounds(
t, nil, nil, beforeSharders.Nodes, afterSharders.Nodes,
)
time.Sleep(time.Second) // give time for last round to be saved
history := cliutil.NewHistory(startRound, endRound)
history.Read(t, sharderUrl, true)
balanceSharderIncome(
t, startRound, endRound, sharderIds, beforeSharders.Nodes, afterSharders.Nodes, history,
)
})
}
func balanceSharderIncome(
t *test.SystemTest,
startRound, endRound int64,
sharderIds []string,
beforeSharders, afterSharders []climodel.Node,
history *cliutil.ChainHistory,
) {
minerScConfig := getMinerScMap(t)
numSharderDelegatesRewarded := int(minerScConfig["num_sharder_delegates_rewarded"])
var numShardersRewarded int
if len(sharderIds) > int(minerScConfig["num_sharders_rewarded"]) {
numShardersRewarded = int(minerScConfig["num_sharders_rewarded"])
} else {
numShardersRewarded = len(sharderIds)
}
minerShare := minerScConfig["share_ratio"]
checkSharderFeeAmounts(
t,
sharderIds,
minerShare,
numShardersRewarded,
beforeSharders, afterSharders,
history,
)
checkSharderFeeRewardFrequency(
t, startRound+1, endRound-1, numShardersRewarded, history,
)
checkSharderDelegatePoolFeeRewardFrequency(
t,
numSharderDelegatesRewarded,
sharderIds,
beforeSharders,
history,
)
checkSharderDelegatePoolFeeAmounts(
t,
sharderIds,
minerShare,
numShardersRewarded, numSharderDelegatesRewarded,
beforeSharders, afterSharders,
history,
)
}
// checkSharderFeeAmounts
// Each round we select a subset of sharders to receive that round's rewards.
//
// The reward payments retrieved from the provider reward table.
// The reward is evenly spread between the sharders receiving rewards each
// round. Each round each sharder receiving a reward gets a fraction
// determined by that sharder's service charge. If the sharder has
// no stake pools then the reward becomes the full block reward.
//
// Firstly we confirm the self-consistency of the reward tables.
// We calculate the change in each sharder's rewards during and confirm that this
// equals the total of the reward payments as read from the provider rewards table.
func checkSharderFeeAmounts(
t *test.SystemTest,
sharderIds []string,
minerShare float64,
numShardersRewarded int,
beforeSharders, afterSharders []climodel.Node,
history *cliutil.ChainHistory,
) {
t.Log("checking sharder fee payment amounts...")
for i, id := range sharderIds {
var blockRewards, feeRewards int64
var startRound int64
if beforeSharders[i].RoundServiceChargeLastUpdated+1 < history.From() {
startRound = history.From()
} else {
startRound = beforeSharders[i].RoundServiceChargeLastUpdated + 1
}
for round := startRound; round <= afterSharders[i].RoundServiceChargeLastUpdated; round++ {
var recordedRoundRewards int64
fees := int64(float64(history.FeesForRound(t, round)) / float64(numShardersRewarded))
roundHistory := history.RoundHistory(t, round)
var feesForSharder int64
if len(beforeSharders[i].StakePool.Pools) > 0 {
feesForSharder = int64(float64(fees) * beforeSharders[i].Settings.ServiceCharge * (1 - minerShare))
} else {
feesForSharder = int64(float64(fees) * (1 - minerShare))
}
for _, pReward := range roundHistory.ProviderRewards {
if pReward.ProviderId != id {
continue
}
switch pReward.RewardType {
case climodel.FeeRewardSharder:
require.Falsef(t, beforeSharders[i].IsKilled,
"killed sharders cannot receive fees, %s is killed", id)
require.Greaterf(t, feesForSharder, int64(0), "fee reward with no fees, reward %v", pReward)
feeRewards += pReward.Amount
recordedRoundRewards += pReward.Amount
case climodel.BlockRewardSharder:
blockRewards += pReward.Amount
default:
require.Failf(t, "", "reward type %s is not available for sharders", pReward.RewardType.String())
}
}
// If sharder is one of the chosen sharders, check fee payment is correct
if recordedRoundRewards > 0 {
require.InDeltaf(t, feesForSharder, recordedRoundRewards, delta,
"incorrect service charge %v for round %d"+
" service charge should be fees %d multiplied by service ratio %v."+
"length stake pools %d",
recordedRoundRewards, round, fees, beforeSharders[i].Settings.ServiceCharge,
len(beforeSharders[i].StakePool.Pools))
}
}
actualReward := afterSharders[i].Reward - beforeSharders[i].Reward
require.InDeltaf(t, actualReward, blockRewards+feeRewards, delta,
"rewards expected %v, change in sharder reward during the test is %v", actualReward, blockRewards+feeRewards)
}
}
// Each round there is a fee, there should be exactly num_sharders_rewarded sharder fee reward payment.
func checkSharderFeeRewardFrequency(
t *test.SystemTest,
start, end int64,
numShardersRewarded int,
history *cliutil.ChainHistory,
) {
t.Log("checking number of fee payments...")
for round := start; round <= end; round++ {
if history.FeesForRound(t, round) == 0 {
continue
}
roundHistory := history.RoundHistory(t, round)
shardersPaid := make(map[string]bool)
for _, pReward := range roundHistory.ProviderRewards {
if pReward.RewardType == climodel.FeeRewardSharder {
_, found := shardersPaid[pReward.ProviderId]
require.Falsef(t, found, "sharder %s receives more than one block reward on round %d", pReward.ProviderId, round)
shardersPaid[pReward.ProviderId] = true
}
}
require.Equal(t, numShardersRewarded, len(shardersPaid),
"mismatch between expected count of sharders rewarded and actual number on round %d", round)
}
}
// checkSharderDelegatePoolFeeRewardFrequency
// Each round there is a fee each sharder rewarded should have num_sharder_delegates_rewarded of
// their delegates rewarded, or all delegates if less.
func checkSharderDelegatePoolFeeRewardFrequency(
t *test.SystemTest,
numSharderDelegatesRewarded int,
sharderIds []string,
sharders []climodel.Node,
history *cliutil.ChainHistory,
) {
t.Log("checking delegate pool reward frequencies...")
for round := history.From(); round <= history.To(); round++ {
if history.FeesForRound(t, round) == 0 {
continue
}
roundHistory := history.RoundHistory(t, round)
for i, id := range sharderIds {
poolsPaid := make(map[string]bool)
for poolId := range sharders[i].Pools {
for _, dReward := range roundHistory.DelegateRewards {
if dReward.RewardType != climodel.FeeRewardSharder || dReward.PoolID != poolId {
continue
}
_, found := poolsPaid[poolId]
if found {
require.Falsef(t, found, "pool %s should have only received block reward once, round %d", poolId, round)
}
poolsPaid[poolId] = true
}
}
numShouldPay := numSharderDelegatesRewarded
if numShouldPay > len(sharders[i].Pools) {
numShouldPay = len(sharders[i].Pools)
}
require.Len(t, poolsPaid, numShouldPay,
"should pay %d pools for shader %s on round %d; %d pools actually paid",
numShouldPay, id, round, len(poolsPaid))
}
}
}
// checkSharderDelegatePoolFeeAmounts
// Each round confirm payments to delegates of the selected sharders.
// There should be exactly `num_sharder_delegates_rewarded` delegates rewarded each round,
// or all delegates if less.
//
// Delegates should be rewarded in proportional to their locked tokens.
// We check the self-consistency of the reward payments each round using
// the delegate reward table.
//
// Next we compare the actual change in rewards to each sharder's delegates, with the
// change as read from the delegate reward table.
func checkSharderDelegatePoolFeeAmounts(
t *test.SystemTest,
sharderIds []string,
minerShare float64,
numShardersRewarded, numSharderDelegatesRewarded int,
beforeSharders, afterSharders []climodel.Node,
history *cliutil.ChainHistory,
) {
t.Log("checking sharder delegate pools fee rewards")
for i, id := range sharderIds {
numPools := len(afterSharders[i].StakePool.Pools)
rewards := make(map[string]int64, numPools)
for poolId := range afterSharders[i].StakePool.Pools {
rewards[poolId] = 0
}
for round := beforeSharders[i].RoundServiceChargeLastUpdated + 1; round <= afterSharders[i].RoundServiceChargeLastUpdated; round++ {
fees := history.FeesForRound(t, round)
poolsBlockRewarded := make(map[string]int64)
roundHistory := history.RoundHistory(t, round)
for _, dReward := range roundHistory.DelegateRewards {
if dReward.ProviderID != id {
continue
}
_, isSharderPool := rewards[dReward.PoolID]
require.Truef(t, isSharderPool, "round %d, invalid pool id, reward %v", round, dReward)
switch dReward.RewardType {
case climodel.FeeRewardSharder:
require.Greater(t, fees, int64(0), "fee reward with no fees")
_, found := poolsBlockRewarded[dReward.PoolID]
require.False(t, found, "delegate pool %s paid a fee reward more than once on round %d",
dReward.PoolID, round)
poolsBlockRewarded[dReward.PoolID] = dReward.Amount
rewards[dReward.PoolID] += dReward.Amount
case climodel.BlockRewardSharder:
rewards[dReward.PoolID] += dReward.Amount
default:
require.Failf(t, "mismatched reward type",
"", "reward type %s not paid to sharder delegate pools", dReward.RewardType)
}
}
if fees > 0 {
confirmPoolPayments(
t,
delegateFeeRewards(
fees,
1-minerShare,
beforeSharders[i].Settings.ServiceCharge,
numShardersRewarded,
),
poolsBlockRewarded,
afterSharders[i].StakePool.Pools,
numSharderDelegatesRewarded,
)
}
}
for poolId := range afterSharders[i].StakePool.Pools {
actualReward := afterSharders[i].StakePool.Pools[poolId].Reward - beforeSharders[i].StakePool.Pools[poolId].Reward
require.InDeltaf(t, actualReward, rewards[poolId], delta,
"poolID %s, rewards expected %v change in pools reward during test", poolId, rewards[poolId],
)
}
}
}