-
Notifications
You must be signed in to change notification settings - Fork 106
/
commission.go
421 lines (373 loc) · 15.5 KB
/
commission.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
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
package api
import (
"context"
"fmt"
"io"
"math/big"
beacon "github.com/oasisprotocol/oasis-core/go/beacon/api"
"github.com/oasisprotocol/oasis-core/go/common/prettyprint"
"github.com/oasisprotocol/oasis-core/go/common/quantity"
)
// commissionRateDenominatorExponent is the commission rate denominator's
// base-10 exponent.
//
// NOTE: Setting it to 5 means commission rates are denominated in 1000ths of a
// percent.
const commissionRateDenominatorExponent uint8 = 5
var (
// CommissionRateDenominator is the denominator for the commission rate.
CommissionRateDenominator *quantity.Quantity
_ prettyprint.PrettyPrinter = (*CommissionRateStep)(nil)
_ prettyprint.PrettyPrinter = (*CommissionRateBoundStep)(nil)
_ prettyprint.PrettyPrinter = (*CommissionSchedule)(nil)
)
// CommissionScheduleRules controls how commission schedule rates and rate
// bounds are allowed to be changed.
type CommissionScheduleRules struct {
// Epoch period when commission rates are allowed to be changed (e.g.
// setting it to 3 means they can be changed every third epoch).
RateChangeInterval beacon.EpochTime `json:"rate_change_interval,omitempty"`
// Number of epochs a commission rate bound change must specified in advance.
RateBoundLead beacon.EpochTime `json:"rate_bound_lead,omitempty"`
// Maximum number of commission rate steps a commission schedule can specify.
MaxRateSteps uint16 `json:"max_rate_steps,omitempty"`
// Maximum number of commission rate bound steps a commission schedule can specify.
MaxBoundSteps uint16 `json:"max_bound_steps,omitempty"`
// MinCommissionRate is the minimum commission rate an account can configure.
// The rate is obtained by dividing this value with the `CommissionRateDenominator`.
MinCommissionRate quantity.Quantity `json:"min_commission_rate"`
}
// CommissionRateStep sets a commission rate and its starting time.
type CommissionRateStep struct {
// Epoch when the commission rate will go in effect.
Start beacon.EpochTime `json:"start,omitempty"`
// Commission rate numerator. The rate is this value divided by CommissionRateDenominator.
Rate quantity.Quantity `json:"rate,omitempty"`
}
// PrettyPrint writes a pretty-printed representation of CommissionRateStep to
// the given writer.
func (crs CommissionRateStep) PrettyPrint(ctx context.Context, prefix string, w io.Writer) {
indexInfix, emptyInfix := PrettyPrintCommissionScheduleIndexInfixes(ctx)
fmt.Fprintf(w, "%s%sstart: epoch %d\n", prefix, indexInfix, crs.Start)
fmt.Fprintf(w, "%s%srate: %s\n", prefix, emptyInfix, PrettyPrintCommissionRatePercentage(crs.Rate))
}
// PrettyType returns a representation of CommissionRateStep that can be used
// for pretty printing.
func (crs CommissionRateStep) PrettyType() (interface{}, error) {
return crs, nil
}
// CommissionRateBoundStep sets a commission rate bound (i.e. the minimum and
// maximum commission rate) and its starting time.
type CommissionRateBoundStep struct {
// Epoch when the commission rate bound will go in effect.
Start beacon.EpochTime `json:"start,omitempty"`
// Minimum commission rate numerator. The minimum rate is this value divided by CommissionRateDenominator.
RateMin quantity.Quantity `json:"rate_min,omitempty"`
// Maximum commission rate numerator. The maximum rate is this value divided by CommissionRateDenominator.
RateMax quantity.Quantity `json:"rate_max,omitempty"`
}
// PrettyPrint writes a pretty-printed representation of CommissionRateBoundStep
// to the given writer.
func (crbs CommissionRateBoundStep) PrettyPrint(ctx context.Context, prefix string, w io.Writer) {
indexInfix, emptyInfix := PrettyPrintCommissionScheduleIndexInfixes(ctx)
fmt.Fprintf(w, "%s%sstart: epoch %d\n", prefix, indexInfix, crbs.Start)
fmt.Fprintf(w, "%s%sminimum rate: %s\n", prefix, emptyInfix, PrettyPrintCommissionRatePercentage(crbs.RateMin))
fmt.Fprintf(w, "%s%smaximum rate: %s\n", prefix, emptyInfix, PrettyPrintCommissionRatePercentage(crbs.RateMax))
}
// PrettyType returns a representation of CommissionRateBoundStep that can be
// used for pretty printing.
func (crbs CommissionRateBoundStep) PrettyType() (interface{}, error) {
return crbs, nil
}
// CommissionSchedule defines a list of commission rates and commission rate
// bounds and their starting times.
type CommissionSchedule struct {
// List of commission rates and their starting times.
Rates []CommissionRateStep `json:"rates,omitempty"`
// List of commission rate bounds and their starting times.
Bounds []CommissionRateBoundStep `json:"bounds,omitempty"`
}
// IsEmpty returns true if commission schedule is empty.
func (cs *CommissionSchedule) IsEmpty() bool {
return len(cs.Rates) == 0 && len(cs.Bounds) == 0
}
// PrettyPrint writes a pretty-printed representation of CommissionSchedule to
// the given writer.
func (cs CommissionSchedule) PrettyPrint(ctx context.Context, prefix string, w io.Writer) {
if cs.Rates == nil {
fmt.Fprintf(w, "%sRates: (none)\n", prefix)
} else {
fmt.Fprintf(w, "%sRates:\n", prefix)
for i, rate := range cs.Rates {
ctx = context.WithValue(ctx, prettyprint.ContextKeyCommissionScheduleIndex, i)
rate.PrettyPrint(ctx, prefix+" ", w)
}
}
if cs.Bounds == nil {
fmt.Fprintf(w, "%sRate Bounds: (none)\n", prefix)
} else {
fmt.Fprintf(w, "%sRate Bounds:\n", prefix)
for i, rateBound := range cs.Bounds {
ctx = context.WithValue(ctx, prettyprint.ContextKeyCommissionScheduleIndex, i)
rateBound.PrettyPrint(ctx, prefix+" ", w)
}
}
}
// PrettyType returns a representation of CommissionSchedule that can be used
// for pretty printing.
func (cs CommissionSchedule) PrettyType() (interface{}, error) {
return cs, nil
}
func (cs *CommissionSchedule) validateComplexity(rules *CommissionScheduleRules) error {
if len(cs.Rates) > int(rules.MaxRateSteps) {
return fmt.Errorf("rate schedule %d steps exceeds maximum %d", len(cs.Rates), rules.MaxRateSteps)
}
if len(cs.Bounds) > int(rules.MaxBoundSteps) {
return fmt.Errorf("bound schedule %d steps exceeds maximum %d", len(cs.Bounds), rules.MaxBoundSteps)
}
return nil
}
// validateNondegenerate detects degenerate steps.
func (cs *CommissionSchedule) validateNondegenerate(rules *CommissionScheduleRules) error {
for i, step := range cs.Rates {
if step.Start%rules.RateChangeInterval != 0 {
return fmt.Errorf("rate step %d start epoch %d not aligned with commission rate change interval %d", i, step.Start, rules.RateChangeInterval)
}
if i > 0 && step.Start <= cs.Rates[i-1].Start {
return fmt.Errorf("rate step %d start epoch %d not after previous step start epoch %d", i, step.Start, cs.Rates[i-1].Start)
}
if step.Rate.Cmp(CommissionRateDenominator) > 0 {
return fmt.Errorf("rate step %d rate %v/%v over unity", i, step.Rate, CommissionRateDenominator)
}
if step.Rate.Cmp(&rules.MinCommissionRate) < 0 {
return fmt.Errorf("rate step %d rate '%v' less than minimum allowed commission rate: '%v'", i, step.Rate, rules.MinCommissionRate)
}
}
for i, step := range cs.Bounds {
if step.Start%rules.RateChangeInterval != 0 {
return fmt.Errorf("bound step %d start epoch %d not aligned with commission rate change interval %d", i, step.Start, rules.RateChangeInterval)
}
if i > 0 && step.Start <= cs.Bounds[i-1].Start {
return fmt.Errorf("bound step %d start epoch %d not after previous step start epoch %d", i, step.Start, cs.Bounds[i-1].Start)
}
if step.RateMin.Cmp(CommissionRateDenominator) > 0 {
return fmt.Errorf("bound step %d minimum rate %v/%v over unity", i, step.RateMin, CommissionRateDenominator)
}
if step.RateMax.Cmp(CommissionRateDenominator) > 0 {
return fmt.Errorf("bound step %d maximum rate %v/%v over unity", i, step.RateMax, CommissionRateDenominator)
}
if step.RateMax.Cmp(&step.RateMin) < 0 { //nolint:gosec
return fmt.Errorf("bound step %d maximum rate %v/%v less than minimum rate %v/%v", i, step.RateMax, CommissionRateDenominator, step.RateMin, CommissionRateDenominator)
}
if step.RateMax.Cmp(&rules.MinCommissionRate) < 0 {
return fmt.Errorf("bound step %d maximum rate '%v' less than minimum allowed commission rate: '%v'", i, step.RateMax, rules.MinCommissionRate)
}
if step.RateMin.Cmp(&rules.MinCommissionRate) < 0 {
return fmt.Errorf("bound step %d minimum rate '%v' less than minimum allowed commission rate: '%v'", i, step.RateMax, rules.MinCommissionRate)
}
}
return nil
}
// validateAmendmentAcceptable apply policy for "when" changes can be made, for CommissionSchedules that are amendments.
func (cs *CommissionSchedule) validateAmendmentAcceptable(rules *CommissionScheduleRules, now beacon.EpochTime, initialSchedule bool) error {
if len(cs.Rates) != 0 {
if cs.Rates[0].Start <= now {
return fmt.Errorf("rate schedule with start epoch %d must not alter rate on or before %d", cs.Rates[0].Start, now)
}
}
if len(cs.Bounds) != 0 {
earliestAllowedChange := now + 1
if !initialSchedule {
// If this is not the initial schedule, the bounds can be amended only RateBoundLead in advance.
earliestAllowedChange += rules.RateBoundLead
}
if cs.Bounds[0].Start < earliestAllowedChange {
return fmt.Errorf("bound schedule with start epoch %d must not alter before %d", cs.Bounds[0].Start, earliestAllowedChange)
}
}
return nil
}
// Prune discards past steps that aren't in effect anymore.
func (cs *CommissionSchedule) Prune(now beacon.EpochTime) {
for len(cs.Rates) > 1 {
if cs.Rates[1].Start > now {
// Remaining steps haven't started yet, so keep them and the current active one.
break
}
cs.Rates = cs.Rates[1:]
}
for len(cs.Bounds) > 1 {
if cs.Bounds[1].Start > now {
// Remaining steps haven't started yet, so keep them and the current active one.
break
}
cs.Bounds = cs.Bounds[1:]
}
}
// amend changes the schedule to use new given steps, replacing steps that are fully covered in the amendment.
func (cs *CommissionSchedule) amend(amendment *CommissionSchedule) {
if len(amendment.Rates) != 0 {
rateSpliceIndex := 0
for ; rateSpliceIndex < len(cs.Rates); rateSpliceIndex++ {
if cs.Rates[rateSpliceIndex].Start >= amendment.Rates[0].Start {
// This and remaining steps are completely overwritten by the amendment.
break
}
}
cs.Rates = append(cs.Rates[:rateSpliceIndex], amendment.Rates...)
}
if len(amendment.Bounds) != 0 {
boundSpliceIndex := 0
for ; boundSpliceIndex < len(cs.Bounds); boundSpliceIndex++ {
if cs.Bounds[boundSpliceIndex].Start >= amendment.Bounds[0].Start {
// This and remaining steps are completely overwritten by the amendment.
break
}
}
cs.Bounds = append(cs.Bounds[:boundSpliceIndex], amendment.Bounds...)
}
}
// validateWithinBound detects rates out of bound.
func (cs *CommissionSchedule) validateWithinBound(now beacon.EpochTime) error {
if len(cs.Rates) == 0 && len(cs.Bounds) == 0 {
// Nothing to check.
return nil
}
if len(cs.Rates) == 0 {
return fmt.Errorf("rates missing")
}
currentRateIndex := 0
currentRate := &cs.Rates[currentRateIndex]
if len(cs.Bounds) == 0 {
return fmt.Errorf("bounds missing")
}
currentBoundIndex := 0
currentBound := &cs.Bounds[currentBoundIndex]
var diagnosticTime beacon.EpochTime
if currentRate.Start > now || currentBound.Start > now {
// We only care if the two schedules start simultaneously if they will start in the future.
// Steps that already started my have started at different times with older steps pruned.
if currentRate.Start != currentBound.Start {
return fmt.Errorf("rate schedule start epoch %d and bound schedule start epoch %d don't match", currentRate.Start, currentBound.Start)
}
diagnosticTime = currentRate.Start
} else {
diagnosticTime = now
}
for {
if currentRate.Rate.Cmp(¤tBound.RateMin) < 0 {
return fmt.Errorf("rate %v/%v from rate step %d less than minimum rate %v/%v from bound step %d at epoch %d",
currentRate.Rate, CommissionRateDenominator, currentRateIndex,
currentBound.RateMin, CommissionRateDenominator, currentBoundIndex,
diagnosticTime,
)
}
if currentRate.Rate.Cmp(¤tBound.RateMax) > 0 {
return fmt.Errorf("rate %v/%v from rate step %d greater than maximum rate %v/%v from bound step %d at epoch %d",
currentRate.Rate, CommissionRateDenominator, currentRateIndex,
currentBound.RateMax, CommissionRateDenominator, currentBoundIndex,
diagnosticTime,
)
}
// Determine what changes next.
nextRateIndex := currentRateIndex + 1
var nextRate *CommissionRateStep
if nextRateIndex < len(cs.Rates) {
nextRate = &cs.Rates[nextRateIndex]
} else {
nextRate = nil
}
nextBoundIndex := currentBoundIndex + 1
var nextBound *CommissionRateBoundStep
if nextBoundIndex < len(cs.Bounds) {
nextBound = &cs.Bounds[nextBoundIndex]
} else {
nextBound = nil
}
if nextRate == nil && nextBound == nil {
// Current rate and bound continue happily ever after.
break
}
if nextRate != nil {
if nextBound == nil || nextRate.Start <= nextBound.Start {
// Rate changes. Check with the new rate on next iteration.
currentRateIndex = nextRateIndex
currentRate = nextRate
diagnosticTime = nextRate.Start
}
}
if nextBound != nil {
if nextRate == nil || nextBound.Start <= nextRate.Start {
// Bound changes. Check with the new bound on the next iteration.
currentBoundIndex = nextBoundIndex
currentBound = nextBound
diagnosticTime = nextBound.Start
}
}
}
return nil
}
// PruneAndValidate validates and prunes old rules.
// Returns an error if there is a validation failure. If it does, the schedule may be pruned already.
func (cs *CommissionSchedule) PruneAndValidate(rules *CommissionScheduleRules, now beacon.EpochTime) error {
if err := cs.validateComplexity(rules); err != nil {
return err
}
if err := cs.validateNondegenerate(rules); err != nil {
return err
}
// If we, for example, import a snapshot as a genesis document, the current steps might not be cued up. So run a
// prune step too at this time.
cs.Prune(now)
if err := cs.validateWithinBound(now); err != nil {
return fmt.Errorf("after pruning: %w", err)
}
return nil
}
// AmendAndPruneAndValidate applies a proposed amendment to a valid schedule.
// Returns an error if there is a validation failure. If it does, the schedule may be amended and pruned already.
func (cs *CommissionSchedule) AmendAndPruneAndValidate(amendment *CommissionSchedule, rules *CommissionScheduleRules, now beacon.EpochTime) error {
if err := amendment.validateComplexity(rules); err != nil {
return fmt.Errorf("amendment: %w", err)
}
if err := amendment.validateNondegenerate(rules); err != nil {
return fmt.Errorf("amendment: %w", err)
}
if err := amendment.validateAmendmentAcceptable(rules, now, len(cs.Bounds) == 0); err != nil {
return fmt.Errorf("amendment: %w", err)
}
cs.Prune(now)
cs.amend(amendment)
if err := cs.validateComplexity(rules); err != nil {
return fmt.Errorf("after pruning and amending: %w", err)
}
if err := cs.validateWithinBound(now); err != nil {
return fmt.Errorf("after pruning and amending: %w", err)
}
return nil
}
// CurrentRate returns the rate at the latest rate step that has started or nil if no step has started.
func (cs *CommissionSchedule) CurrentRate(now beacon.EpochTime) *quantity.Quantity {
var latestStartedStep *CommissionRateStep
for i := range cs.Rates {
step := &cs.Rates[i]
if step.Start > now {
break
}
latestStartedStep = step
}
if latestStartedStep == nil {
return nil
}
return &latestStartedStep.Rate
}
func init() {
// Compute CommissionRateDenominator from its base-10 exponent.
CommissionRateDenominator = quantity.NewQuantity()
err := CommissionRateDenominator.FromBigInt(
new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(commissionRateDenominatorExponent)), nil),
)
if err != nil {
panic(err)
}
}