2020-03-10 20:42:11 +00:00
|
|
|
// 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 {
|
2020-04-08 21:44:27 +01:00
|
|
|
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
|
2021-01-27 18:53:16 +00:00
|
|
|
TotalPaid currency.MicroUnit
|
|
|
|
TotalDistributed currency.MicroUnit
|
2020-03-10 20:42:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
2020-03-31 23:43:17 +01:00
|
|
|
SurgePercent int64
|
2020-03-10 20:42:11 +00:00
|
|
|
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.
|
2020-03-31 23:43:17 +01:00
|
|
|
SurgePercent int64
|
2020-03-10 20:42:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// GenerateStatements generates all of the Statements for the given PeriodInfo.
|
|
|
|
func GenerateStatements(info PeriodInfo) ([]Statement, error) {
|
2020-04-08 21:44:27 +01:00
|
|
|
startDate := info.Period.StartDate()
|
2020-03-10 20:42:11 +00:00
|
|
|
endDate := info.Period.EndDateExclusive()
|
|
|
|
|
|
|
|
rates := info.Rates
|
|
|
|
if rates == nil {
|
|
|
|
rates = &DefaultRates
|
|
|
|
}
|
|
|
|
withheldPercents := info.WithheldPercents
|
|
|
|
if withheldPercents == nil {
|
|
|
|
withheldPercents = DefaultWithheldPercents
|
|
|
|
}
|
|
|
|
|
2020-03-31 23:43:17 +01:00
|
|
|
surgePercent := decimal.NewFromInt(info.SurgePercent)
|
2020-03-10 20:42:11 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2020-04-08 21:44:27 +01:00
|
|
|
offline := node.LastContactSuccess.Before(startDate)
|
|
|
|
if offline {
|
|
|
|
codes = append(codes, Offline)
|
|
|
|
}
|
|
|
|
|
2020-03-10 20:42:11 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2020-04-08 21:44:27 +01:00
|
|
|
// If the node is disqualified but not gracefully exited, nothing is owed/held/disposed.
|
2020-03-10 20:42:11 +00:00
|
|
|
if node.Disqualified != nil && node.Disqualified.Before(endDate) && !gracefullyExited {
|
|
|
|
codes = append(codes, Disqualified)
|
|
|
|
disposed = decimal.Zero
|
|
|
|
held = decimal.Zero
|
|
|
|
owed = decimal.Zero
|
|
|
|
}
|
|
|
|
|
2020-04-08 21:44:27 +01:00
|
|
|
// If the node is offline, nothing is owed/held/disposed.
|
|
|
|
if offline {
|
|
|
|
disposed = decimal.Zero
|
|
|
|
held = decimal.Zero
|
|
|
|
owed = decimal.Zero
|
|
|
|
}
|
|
|
|
|
2020-03-10 20:42:11 +00:00
|
|
|
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
|
|
|
|
}
|