storj/satellite/compensation/statement.go
Jeff Wendling 759bdd6794 satellite/compensation: add total-paid and total-distributed to invoices
Change-Id: Id4414867917cbf8aad77795f764d6381e88d9a34
2021-02-02 18:14:31 +00:00

237 lines
7.2 KiB
Go

// 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
TotalPaid currency.MicroUnit
TotalDistributed 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
}