Skip to content

Commit aaa9821

Browse files
authored
fix: align effective rate calculation
1 parent cd71e49 commit aaa9821

7 files changed

Lines changed: 25 additions & 36 deletions

File tree

costs/calculate.go

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
// CalculateEffectiveRate computes the storage rate for the given total data size.
1111
// Integer division is used to match on-chain Solidity truncation.
1212
// If epochsPerMonth is zero or negative, chain.EpochsPerMonth is used as a safe default.
13-
// Nil pricePerTiBPerMonth or minMonthlyRate are treated as zero.
13+
// Nil sizeBytes, pricePerTiBPerMonth, or minMonthlyRate are treated as zero.
1414
func CalculateEffectiveRate(
1515
sizeBytes *big.Int,
1616
pricePerTiBPerMonth *big.Int,
@@ -22,6 +22,9 @@ func CalculateEffectiveRate(
2222
}
2323
epm := big.NewInt(epochsPerMonth)
2424

25+
if sizeBytes == nil {
26+
sizeBytes = new(big.Int)
27+
}
2528
if pricePerTiBPerMonth == nil {
2629
pricePerTiBPerMonth = new(big.Int)
2730
}
@@ -31,15 +34,13 @@ func CalculateEffectiveRate(
3134

3235
ratePerMonth := new(big.Int).Mul(pricePerTiBPerMonth, sizeBytes)
3336
ratePerMonth.Div(ratePerMonth, bigTiB)
34-
hitMin := ratePerMonth.Cmp(minMonthlyRate) < 0
35-
if hitMin {
37+
if ratePerMonth.Cmp(minMonthlyRate) < 0 {
3638
ratePerMonth.Set(minMonthlyRate)
3739
}
3840

39-
// ratePerEpoch is computed independently to avoid accumulating two division errors.
4041
ratePerEpoch := new(big.Int).Mul(pricePerTiBPerMonth, sizeBytes)
41-
ratePerEpoch.Div(ratePerEpoch, bigTiB)
42-
ratePerEpoch.Div(ratePerEpoch, epm)
42+
divisor := new(big.Int).Mul(bigTiB, epm)
43+
ratePerEpoch.Div(ratePerEpoch, divisor)
4344

4445
minEpochRate := new(big.Int).Div(minMonthlyRate, epm)
4546
if minEpochRate.Cmp(bigOne) < 0 {
@@ -49,14 +50,6 @@ func CalculateEffectiveRate(
4950
ratePerEpoch.Set(minEpochRate)
5051
}
5152

52-
// At the minimum floor, align ratePerMonth with the epoch-aligned rate so that
53-
// ratePerEpoch × epm == ratePerMonth exactly. Above the floor the two fields
54-
// are intentionally computed independently to avoid accumulating two division
55-
// truncation errors in the more-common non-minimum case.
56-
if hitMin {
57-
ratePerMonth.Mul(ratePerEpoch, epm)
58-
}
59-
6053
return EffectiveRate{
6154
RatePerEpoch: ratePerEpoch,
6255
RatePerMonth: ratePerMonth,

costs/calculate_test.go

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,9 @@ func TestCalculateEffectiveRate_SubTiB_HitsMinimum(t *testing.T) {
6060
chain.EpochsPerMonth,
6161
)
6262

63-
// At the minimum floor, ratePerMonth is derived from ratePerEpoch × epm so the
64-
// two fields are consistent. ratePerMonth is therefore slightly less than
65-
// MinimumPricePerMonth by at most (epm-1) attoUSDFC due to integer truncation.
66-
wantMonthly := new(big.Int).Mul(rate.RatePerEpoch, big.NewInt(chain.EpochsPerMonth))
67-
if rate.RatePerMonth.Cmp(wantMonthly) != 0 {
68-
t.Errorf("ratePerMonth should equal ratePerEpoch*epm at minimum: got %s, want %s",
69-
rate.RatePerMonth, wantMonthly)
63+
if rate.RatePerMonth.Cmp(pricing.MinimumPricePerMonth) != 0 {
64+
t.Errorf("ratePerMonth should equal MinimumPricePerMonth: got %s, want %s",
65+
rate.RatePerMonth, pricing.MinimumPricePerMonth)
7066
}
7167
if rate.RatePerEpoch.Cmp(bi(1)) < 0 {
7268
t.Errorf("ratePerEpoch should be at least 1: got %s", rate.RatePerEpoch)
@@ -99,11 +95,9 @@ func TestCalculateEffectiveRate_ZeroSize(t *testing.T) {
9995
chain.EpochsPerMonth,
10096
)
10197

102-
// Zero size hits the minimum floor; ratePerMonth must equal ratePerEpoch × epm.
103-
wantMonthly := new(big.Int).Mul(rate.RatePerEpoch, big.NewInt(chain.EpochsPerMonth))
104-
if rate.RatePerMonth.Cmp(wantMonthly) != 0 {
105-
t.Errorf("ratePerMonth should be epoch-aligned for zero size: got %s, want %s",
106-
rate.RatePerMonth, wantMonthly)
98+
if rate.RatePerMonth.Cmp(pricing.MinimumPricePerMonth) != 0 {
99+
t.Errorf("ratePerMonth should equal MinimumPricePerMonth for zero size: got %s, want %s",
100+
rate.RatePerMonth, pricing.MinimumPricePerMonth)
107101
}
108102
}
109103

@@ -673,11 +667,11 @@ func TestDepositNeeded_ZeroTotalLockup(t *testing.T) {
673667
// --- CalculateEffectiveRate edge cases ---
674668

675669
func TestCalculateEffectiveRate_NilInputs(t *testing.T) {
676-
rate := CalculateEffectiveRate(bi(chain.TiB), nil, nil, 0)
670+
rate := CalculateEffectiveRate(nil, nil, nil, 0)
677671
if rate.RatePerEpoch == nil || rate.RatePerMonth == nil {
678672
t.Fatal("nil rate fields returned")
679673
}
680-
// nil price and nil minRate both treated as 0; ratePerEpoch is clamped to bigOne=1
674+
// nil size, price, and minRate are treated as 0; ratePerEpoch is clamped to bigOne=1.
681675
if rate.RatePerEpoch.Cmp(big.NewInt(1)) != 0 {
682676
t.Errorf("RatePerEpoch = %s, want 1", rate.RatePerEpoch)
683677
}

costs/doc.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
//
1919
// Epoch — Filecoin block interval (30 seconds on mainnet and calibration).
2020
// All on-chain rates and durations are denominated in epochs; 120 epochs
21-
// equal one hour, ~86 400 equal one month. Monthly figures in this
22-
// package are derived by multiplying per-epoch rates by
23-
// [github.com/strahe/synapse-go/chain.EpochsPerMonth].
21+
// equal one hour, ~86 400 equal one month. On-chain lockup rates are
22+
// per-epoch; effective monthly rates preserve monthly pricing precision for
23+
// display and comparison.
2424
//
2525
// Basis points (bps) — one hundredth of one percent (1 bps = 0.01 %).
2626
// Commission rates returned by warmstorage are expressed in basis points

costs/multi_cost.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ type MultiContextRef struct {
4040
type MultiContextCosts struct {
4141
// RatePerEpoch is the sum of per-context effective rates (post-upload).
4242
RatePerEpoch *big.Int
43-
// RatePerMonth is RatePerEpoch * EpochsPerMonth.
43+
// RatePerMonth is the sum of per-context monthly effective rates.
4444
RatePerMonth *big.Int
4545
// DepositNeeded is the single USDFC deposit covering all contexts.
4646
DepositNeeded *big.Int

costs/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import "math/big"
44

55
// EffectiveRate is the per-epoch and per-month storage rate for a given data size.
66
// RatePerEpoch uses integer division to match on-chain Solidity truncation.
7+
// RatePerMonth preserves monthly pricing precision for display and comparison.
78
type EffectiveRate struct {
89
RatePerEpoch *big.Int
910
RatePerMonth *big.Int

spregistry/service.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"fmt"
77
"log/slog"
88
"math/big"
9-
"sort"
9+
"slices"
1010
"strings"
1111
"time"
1212

@@ -553,8 +553,8 @@ func (s *Service) SelectActivePDPProviders(ctx context.Context, f ProviderFilter
553553
offset += pageSize
554554
}
555555

556-
sort.Slice(all, func(i, j int) bool {
557-
return all[i].Info.ID.Cmp(all[j].Info.ID) < 0
556+
slices.SortFunc(all, func(a, b PDPProvider) int {
557+
return a.Info.ID.Cmp(b.Info.ID)
558558
})
559559
return all, nil
560560
}

storage/types.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,8 @@ func (r *UploadResult) PartialSuccess() bool {
240240
// direct StoreOptions, PullRequest, or CommitRequest hooks.
241241
type UploadOptions struct {
242242
// Copies is the number of provider copies to store. Zero means the resolver
243-
// default: len(DataSetIDs) or len(ProviderIDs) when those are set, otherwise 2.
243+
// default: the number of unique DataSetIDs or ProviderIDs when those are set,
244+
// otherwise 2.
244245
Copies int
245246
// PieceMetadata is stored with each piece on-chain.
246247
PieceMetadata map[string]string

0 commit comments

Comments
 (0)