Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions oracle/pkg/updater/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package updater

import "math/big"

func (u *Updater) ComputeResidualAfterDecay(startTimestamp, endTimestamp, commitTimestamp uint64) *big.Int {
return u.computeResidualAfterDecay(startTimestamp, endTimestamp, commitTimestamp)
}
68 changes: 49 additions & 19 deletions oracle/pkg/updater/updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"errors"
"fmt"
"log/slog"
"math"
"math/big"
"strings"
"sync"
Expand Down Expand Up @@ -39,8 +38,11 @@ const (
)

const (
PRECISION = 1e16
ONE_HUNDRED_PERCENT = 100 * PRECISION
PRECISION = 1e16
)

var (
BigOneHundredPercent = big.NewInt(100 * PRECISION)
)

type Winner struct {
Expand Down Expand Up @@ -583,32 +585,60 @@ func (u *Updater) getL1Txns(ctx context.Context, blockNum uint64) (map[string]Tx
// The computation does not care what format the timestamps are in, as long as they are consistent
// (e.g they could be unix or unixMili timestamps)
func (u *Updater) computeResidualAfterDecay(startTimestamp, endTimestamp, commitTimestamp uint64) *big.Int {
if startTimestamp >= endTimestamp || startTimestamp > commitTimestamp || endTimestamp <= commitTimestamp {
u.logger.Debug("timestamp out of range", "startTimestamp", startTimestamp, "endTimestamp", endTimestamp, "commitTimestamp", commitTimestamp)
if startTimestamp >= endTimestamp || endTimestamp <= commitTimestamp {
u.logger.Debug(
"timestamp out of range",
"startTimestamp", startTimestamp,
"endTimestamp", endTimestamp,
"commitTimestamp", commitTimestamp,
)
return big.NewInt(0)
}

// providers may commit before the start of the decay period
// in this case, there is no decay
if startTimestamp > commitTimestamp {
u.logger.Debug(
"commitTimestamp is before startTimestamp",
"startTimestamp", startTimestamp,
"commitTimestamp", commitTimestamp,
)
return BigOneHundredPercent
}

// Calculate the total time in seconds
totalTime := endTimestamp - startTimestamp
totalTime := new(big.Int).SetUint64(endTimestamp - startTimestamp)
// Calculate the time passed in seconds
timePassed := commitTimestamp - startTimestamp
// Calculate the decay percentage
decayPercentage := float64(timePassed) / float64(totalTime)
// Residual value
residual := 1 - decayPercentage

residualPercentageRound := math.Round(residual * ONE_HUNDRED_PERCENT)
if residualPercentageRound > ONE_HUNDRED_PERCENT {
residualPercentageRound = ONE_HUNDRED_PERCENT
timePassed := new(big.Int).SetUint64(commitTimestamp - startTimestamp)

// Calculate the residual percentage using integer arithmetic
// residual = (totalTime - timePassed) * ONE_HUNDRED_PERCENT / totalTime

// Step 1: (totalTime - timePassed)
timeRemaining := new(big.Int).Sub(totalTime, timePassed)

// Step 2: (totalTime - timePassed) * ONE_HUNDRED_PERCENT
scaledRemaining := new(big.Int).Mul(timeRemaining, BigOneHundredPercent)

// Step 3: ((totalTime - timePassed) * ONE_HUNDRED_PERCENT) / totalTime
// This gives us the residual percentage directly as an integer
residualPercentage := new(big.Int).Div(scaledRemaining, totalTime)

// Ensure residual doesn't exceed ONE_HUNDRED_PERCENT (shouldn't happen with correct inputs, but for safety)
if residualPercentage.Cmp(BigOneHundredPercent) > 0 {
residualPercentage = BigOneHundredPercent
}
u.logger.Debug("decay information",

u.logger.Debug(
"decay information",
"startTimestamp", startTimestamp,
"endTimestamp", endTimestamp,
"commitTimestamp", commitTimestamp,
"totalTime", totalTime,
"timePassed", timePassed,
"decayPercentage", decayPercentage,
"residual", residual,
"timeRemaining", timeRemaining,
"residualPercentage", residualPercentage,
)
return big.NewInt(int64(residualPercentageRound))

return residualPercentage
}
162 changes: 162 additions & 0 deletions oracle/pkg/updater/updater_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1292,6 +1292,168 @@ func TestUpdaterIgnoreCommitments(t *testing.T) {
}
}

func TestComputeResidualAfterDecay(t *testing.T) {
t.Parallel()

discardLogger := slog.New(slog.NewTextHandler(io.Discard, nil))

u, err := updater.NewUpdater(
discardLogger,
nil,
nil,
nil,
nil,
nil,
)
if err != nil {
// The current NewUpdater only returns error on cache creation failure, unlikely here.
t.Fatalf("Failed to create minimal updater instance for test: %v", err)
}

tests := []struct {
name string
start uint64
end uint64
commit uint64
want *big.Int
}{
{
name: "Commit Before Start",
start: 1000,
end: 2000,
commit: 500,
want: updater.BigOneHundredPercent,
},
{
name: "Commit At Start",
start: 1000,
end: 2000,
commit: 1000,
want: updater.BigOneHundredPercent,
},
{
name: "Commit After End",
start: 1000,
end: 2000,
commit: 2500,
want: big.NewInt(0),
},
{
name: "Commit At End",
start: 1000,
end: 2000,
commit: 2000,
want: big.NewInt(0),
},
{
name: "Invalid Range: Start Equals End",
start: 1000,
end: 1000,
commit: 1000,
want: big.NewInt(0),
},
{
name: "Invalid Range: Start Greater Than End",
start: 2000,
end: 1000,
commit: 1500,
want: big.NewInt(0),
},
{
name: "Commit Exactly Midpoint",
start: 1000,
end: 2000, // duration 1000
commit: 1500, // 500 passed
want: big.NewInt(50 * updater.PRECISION), // 1 - 500/1000 = 0.5 -> 50%
},
{
name: "Commit At 25% Time Passed",
start: 1000,
end: 2000, // duration 1000
commit: 1250, // 250 passed
want: big.NewInt(75 * updater.PRECISION), // 1 - 250/1000 = 0.75 -> 75%
},
{
name: "Commit At 75% Time Passed",
start: 1000,
end: 2000, // duration 1000
commit: 1750, // 750 passed
want: big.NewInt(25 * updater.PRECISION), // 1 - 750/1000 = 0.25 -> 25%
},
{
name: "Commit Very Close To Start",
start: 100000,
end: 200000, // duration 100000
commit: 100001, // 1 passed
// residual = 1 - 1/100000 = 0.99999
// percentage = 0.99999 * 100 = 99.999
// scaled = 99.999 * PRECISION
// Expected float: (1.0 - (1.0 / 100000.0)) * ONE_HUNDRED_PERCENT
// Expected float: 0.99999 * 100 * 1e16 = 99.999 * 1e16 = 999990000000000000
want: big.NewInt(999990000000000000),
},
{
name: "Commit Very Close To End",
start: 100000,
end: 200000, // duration 100000
commit: 199000, // 99000 passed
// residual = 1 - 99000/100000 = 1 - 0.99 = 0.01
// scaled = 0.01 * 100 * PRECISION = 1 * PRECISION
want: big.NewInt(1 * int64(updater.PRECISION)),
},
{
name: "Zero Start Time",
start: 0,
end: 1000, // duration 1000
commit: 500, // 500 passed
want: big.NewInt(50 * updater.PRECISION), // 1 - 500/1000 = 0.5 -> 50%
},
{
name: "Zero Start and Commit Time",
start: 0,
end: 1000,
commit: 0,
want: updater.BigOneHundredPercent,
},
{
name: "Large Timestamps",
start: 1700000000000, // example ms timestamps
end: 1700000012000, // 12 second duration
commit: 1700000003000, // 3 seconds passed
// residual = 1 - 3000/12000 = 1 - 0.25 = 0.75
// scaled = 0.75 * 100 * PRECISION = 75 * PRECISION
want: big.NewInt(75 * updater.PRECISION),
},
{
name: "Minimal Valid Duration",
start: 1000,
end: 1001, // duration 1
commit: 1000,
want: updater.BigOneHundredPercent,
},
{
name: "Minimal Valid Duration, Commit slightly after start",
start: 1000,
end: 1002, // duration 2
commit: 1001, // passed 1
// residual = 1 - 1/2 = 0.5
want: big.NewInt(50 * updater.PRECISION),
},
}

for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := u.ComputeResidualAfterDecay(tc.start, tc.end, tc.commit)

if got.Cmp(tc.want) != 0 {
t.Errorf("ComputeResidualAfterDecay(%d, %d, %d) = %v, want %v", tc.start, tc.end, tc.commit, got, tc.want)
}
})
}
}

type testSettlement struct {
commitmentIdx []byte
txHash string
Expand Down
Loading