-
Notifications
You must be signed in to change notification settings - Fork 394
/
statement.go
234 lines (203 loc) · 7.15 KB
/
statement.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
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
package compensation
import (
"time"
"github.com/shopspring/decimal"
"github.com/zeebo/errs"
"storj.io/common/storj"
"storj.io/storj/private/currency"
)
var (
gb = decimal.NewFromInt(1e9)
tb = decimal.NewFromInt(1e12)
)
var (
// DefaultWithheldPercents contains the standard withholding schedule.
DefaultWithheldPercents = []int{75, 75, 75, 50, 50, 50, 25, 25, 25}
// DefaultRates contains the standard operation rates.
DefaultRates = Rates{
AtRestGBHours: RequireRateFromString("0.00000205"), // $1.50/TB at rest
GetTB: RequireRateFromString("20.00"), // $20.00/TB
PutTB: RequireRateFromString("0.00"),
GetRepairTB: RequireRateFromString("10.00"), // $10.00/TB
PutRepairTB: RequireRateFromString("0.00"),
GetAuditTB: RequireRateFromString("10.0"), // $10.00/TB
}
)
// NodeInfo contains all of the information about a node and the operations
// it performed in some period.
type NodeInfo struct {
ID storj.NodeID
CreatedAt time.Time
LastContactSuccess time.Time
Disqualified *time.Time
GracefulExit *time.Time
UsageAtRest float64
UsageGet int64
UsagePut int64
UsageGetRepair int64
UsagePutRepair int64
UsageGetAudit int64
TotalHeld currency.MicroUnit
TotalDisposed currency.MicroUnit
}
// Statement is the computed amounts and codes from a node.
type Statement struct {
NodeID storj.NodeID
Codes Codes
AtRest currency.MicroUnit
Get currency.MicroUnit
Put currency.MicroUnit
GetRepair currency.MicroUnit
PutRepair currency.MicroUnit
GetAudit currency.MicroUnit
SurgePercent int64
Owed currency.MicroUnit
Held currency.MicroUnit
Disposed currency.MicroUnit
}
// PeriodInfo contains configuration about the payment info to generate
// the statements.
type PeriodInfo struct {
// Period is the period.
Period Period
// Nodes is usage and other related information for nodes for this period.
Nodes []NodeInfo
// Rates is the compensation rates for different operations. If nil, the
// default rates are used.
Rates *Rates
// WithheldPercents is the percent to withhold from the total, after surge
// adjustments, for each month in the node's lifetime. For example, to
// withhold 75% in the first month, 50% in the second month, 0% in the third
// month and to leave withheld thereafter, set to [75,50,0]. If nil,
// DefaultWithheldPercents is used.
WithheldPercents []int
// DisposePercent is the percent to dispose to the node after it has left
// withholding. The remaining amount is kept until the node performs a graceful
// exit.
DisposePercent int
// SurgePercent is the percent to adjust final amounts owed. For example,
// to pay 150%, set to 150. Zero means no surge.
SurgePercent int64
}
// GenerateStatements generates all of the Statements for the given PeriodInfo.
func GenerateStatements(info PeriodInfo) ([]Statement, error) {
startDate := info.Period.StartDate()
endDate := info.Period.EndDateExclusive()
rates := info.Rates
if rates == nil {
rates = &DefaultRates
}
withheldPercents := info.WithheldPercents
if withheldPercents == nil {
withheldPercents = DefaultWithheldPercents
}
surgePercent := decimal.NewFromInt(info.SurgePercent)
disposePercent := decimal.NewFromInt(int64(info.DisposePercent))
// Intermediate calculations (especially at-rest related) can overflow an
// int64 so we need to use arbitrary precision fixed point math. The final
// calculations should fit comfortably into an int64. If not, it means
// we're trying to pay somebody more than 9,223,372,036,854,775,807
// micro-units (e.g. $9,223,372,036,854 dollars).
statements := make([]Statement, 0, len(info.Nodes))
for _, node := range info.Nodes {
var codes []Code
atRest := decimal.NewFromFloat(node.UsageAtRest).
Mul(decimal.Decimal(rates.AtRestGBHours)).
Div(gb)
get := decimal.NewFromInt(node.UsageGet).
Mul(decimal.Decimal(rates.GetTB)).
Div(tb)
put := decimal.NewFromInt(node.UsagePut).
Mul(decimal.Decimal(rates.PutTB)).
Div(tb)
getRepair := decimal.NewFromInt(node.UsageGetRepair).
Mul(decimal.Decimal(rates.GetRepairTB)).
Div(tb)
putRepair := decimal.NewFromInt(node.UsagePutRepair).
Mul(decimal.Decimal(rates.PutRepairTB)).
Div(tb)
getAudit := decimal.NewFromInt(node.UsageGetAudit).
Mul(decimal.Decimal(rates.GetAuditTB)).
Div(tb)
total := decimal.Sum(atRest, get, put, getRepair, putRepair, getAudit)
if info.SurgePercent > 0 {
total = PercentOf(total, surgePercent)
}
gracefullyExited := node.GracefulExit != nil && node.GracefulExit.Before(endDate)
if gracefullyExited {
codes = append(codes, GracefulExit)
}
offline := node.LastContactSuccess.Before(startDate)
if offline {
codes = append(codes, Offline)
}
withheldPercent, inWithholding := NodeWithheldPercent(withheldPercents, node.CreatedAt, endDate)
held := PercentOf(total, decimal.NewFromInt(int64(withheldPercent)))
owed := total.Sub(held)
if inWithholding {
codes = append(codes, InWithholding)
}
var disposed decimal.Decimal
if !inWithholding || gracefullyExited {
// The storage node is out of withholding. Determine how much should be
// disposed from withheld back to the storage node.
disposed = node.TotalHeld.Decimal()
if !gracefullyExited {
disposed = PercentOf(disposed, disposePercent)
} else { // if it's a graceful exit, don't withhold anything
owed = owed.Add(held)
held = decimal.Zero
}
disposed = disposed.Sub(node.TotalDisposed.Decimal())
if disposed.Sign() < 0 {
// We've disposed more than we should have according to the
// percent. Don't dispose any more.
disposed = decimal.Zero
}
owed = owed.Add(disposed)
}
// If the node is disqualified but not gracefully exited, nothing is owed/held/disposed.
if node.Disqualified != nil && node.Disqualified.Before(endDate) && !gracefullyExited {
codes = append(codes, Disqualified)
disposed = decimal.Zero
held = decimal.Zero
owed = decimal.Zero
}
// If the node is offline, nothing is owed/held/disposed.
if offline {
disposed = decimal.Zero
held = decimal.Zero
owed = decimal.Zero
}
var overflowErrs errs.Group
toMicroUnit := func(v decimal.Decimal) currency.MicroUnit {
m, err := currency.MicroUnitFromDecimal(v)
if err != nil {
overflowErrs.Add(err)
return currency.MicroUnit{}
}
return m
}
statement := Statement{
NodeID: node.ID,
Codes: codes,
AtRest: toMicroUnit(atRest),
Get: toMicroUnit(get),
Put: toMicroUnit(put),
GetRepair: toMicroUnit(getRepair),
PutRepair: toMicroUnit(putRepair),
GetAudit: toMicroUnit(getAudit),
SurgePercent: info.SurgePercent,
Owed: toMicroUnit(owed),
Held: toMicroUnit(held),
Disposed: toMicroUnit(disposed),
}
if err := overflowErrs.Err(); err != nil {
return nil, Error.New("currency overflows encountered while calculating payment for node %s", statement.NodeID.String())
}
statements = append(statements, statement)
}
return statements, nil
}