From c2d4e57a676bb46d1de3b320d4b67579b3908eb3 Mon Sep 17 00:00:00 2001 From: Mikhail Wall Date: Tue, 29 Apr 2025 19:18:45 +0200 Subject: [PATCH 1/4] fix: changed decay logic --- oracle/pkg/updater/updater.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/oracle/pkg/updater/updater.go b/oracle/pkg/updater/updater.go index b2d96caef..c83a5d840 100644 --- a/oracle/pkg/updater/updater.go +++ b/oracle/pkg/updater/updater.go @@ -583,11 +583,18 @@ 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 { + 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 big.NewInt(int64(ONE_HUNDRED_PERCENT)) + } + // Calculate the total time in seconds totalTime := endTimestamp - startTimestamp // Calculate the time passed in seconds From 73ba1ae5b1afb1005ad9f632c68db25c1dbf3063 Mon Sep 17 00:00:00 2001 From: Mikhail Wall Date: Tue, 29 Apr 2025 21:42:05 +0200 Subject: [PATCH 2/4] feat: added tests for decay calculation --- oracle/pkg/updater/export_test.go | 7 ++ oracle/pkg/updater/updater_test.go | 161 +++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 oracle/pkg/updater/export_test.go diff --git a/oracle/pkg/updater/export_test.go b/oracle/pkg/updater/export_test.go new file mode 100644 index 000000000..c72014a87 --- /dev/null +++ b/oracle/pkg/updater/export_test.go @@ -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) +} diff --git a/oracle/pkg/updater/updater_test.go b/oracle/pkg/updater/updater_test.go index d74b341d6..0d80ab1f1 100644 --- a/oracle/pkg/updater/updater_test.go +++ b/oracle/pkg/updater/updater_test.go @@ -1292,6 +1292,167 @@ 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: big.NewInt(updater.ONE_HUNDRED_PERCENT), + }, + { + name: "Commit At Start", + start: 1000, + end: 2000, + commit: 1000, + want: big.NewInt(updater.ONE_HUNDRED_PERCENT), + }, + { + 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: big.NewInt(updater.ONE_HUNDRED_PERCENT), + }, + { + 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: big.NewInt(updater.ONE_HUNDRED_PERCENT), + }, + { + 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 From c1754d8992a35e39df9caa5ca1aba278733a9057 Mon Sep 17 00:00:00 2001 From: Mikhail Wall Date: Tue, 29 Apr 2025 21:59:22 +0200 Subject: [PATCH 3/4] fix: fixed flacky test --- oracle/pkg/updater/updater_test.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/oracle/pkg/updater/updater_test.go b/oracle/pkg/updater/updater_test.go index 0d80ab1f1..02ef8237e 100644 --- a/oracle/pkg/updater/updater_test.go +++ b/oracle/pkg/updater/updater_test.go @@ -1446,8 +1446,16 @@ func TestComputeResidualAfterDecay(t *testing.T) { 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) + if tc.name == "Commit Very Close To End" { + want1 := big.NewInt(10000000000000000) + want2 := big.NewInt(10000000000000008) + if got.Cmp(want1) != 0 && got.Cmp(want2) != 0 { + t.Errorf("ComputeResidualAfterDecay(%d, %d, %d) = %v, want either %v or %v", tc.start, tc.end, tc.commit, got, want1, want2) + } + } else { + if got.Cmp(tc.want) != 0 { + t.Errorf("ComputeResidualAfterDecay(%d, %d, %d) = %v, want %v", tc.start, tc.end, tc.commit, got, tc.want) + } } }) } From a76a527323f9e3f497d637f6f6a89e373ffa5746 Mon Sep 17 00:00:00 2001 From: Mikhail Wall Date: Thu, 1 May 2025 16:17:17 +0200 Subject: [PATCH 4/4] fix: rewrite decay calc from float to big int --- oracle/pkg/updater/updater.go | 63 ++++++++++++++++++++---------- oracle/pkg/updater/updater_test.go | 21 ++++------ 2 files changed, 50 insertions(+), 34 deletions(-) diff --git a/oracle/pkg/updater/updater.go b/oracle/pkg/updater/updater.go index c83a5d840..7af116382 100644 --- a/oracle/pkg/updater/updater.go +++ b/oracle/pkg/updater/updater.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "log/slog" - "math" "math/big" "strings" "sync" @@ -39,8 +38,11 @@ const ( ) const ( - PRECISION = 1e16 - ONE_HUNDRED_PERCENT = 100 * PRECISION + PRECISION = 1e16 +) + +var ( + BigOneHundredPercent = big.NewInt(100 * PRECISION) ) type Winner struct { @@ -584,38 +586,59 @@ func (u *Updater) getL1Txns(ctx context.Context, blockNum uint64) (map[string]Tx // (e.g they could be unix or unixMili timestamps) func (u *Updater) computeResidualAfterDecay(startTimestamp, endTimestamp, commitTimestamp uint64) *big.Int { if startTimestamp >= endTimestamp || endTimestamp <= commitTimestamp { - u.logger.Debug("timestamp out of range", "startTimestamp", startTimestamp, "endTimestamp", endTimestamp, "commitTimestamp", 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 big.NewInt(int64(ONE_HUNDRED_PERCENT)) + 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 } diff --git a/oracle/pkg/updater/updater_test.go b/oracle/pkg/updater/updater_test.go index 02ef8237e..cff14edfb 100644 --- a/oracle/pkg/updater/updater_test.go +++ b/oracle/pkg/updater/updater_test.go @@ -1322,14 +1322,14 @@ func TestComputeResidualAfterDecay(t *testing.T) { start: 1000, end: 2000, commit: 500, - want: big.NewInt(updater.ONE_HUNDRED_PERCENT), + want: updater.BigOneHundredPercent, }, { name: "Commit At Start", start: 1000, end: 2000, commit: 1000, - want: big.NewInt(updater.ONE_HUNDRED_PERCENT), + want: updater.BigOneHundredPercent, }, { name: "Commit After End", @@ -1413,7 +1413,7 @@ func TestComputeResidualAfterDecay(t *testing.T) { start: 0, end: 1000, commit: 0, - want: big.NewInt(updater.ONE_HUNDRED_PERCENT), + want: updater.BigOneHundredPercent, }, { name: "Large Timestamps", @@ -1429,7 +1429,7 @@ func TestComputeResidualAfterDecay(t *testing.T) { start: 1000, end: 1001, // duration 1 commit: 1000, - want: big.NewInt(updater.ONE_HUNDRED_PERCENT), + want: updater.BigOneHundredPercent, }, { name: "Minimal Valid Duration, Commit slightly after start", @@ -1446,16 +1446,9 @@ func TestComputeResidualAfterDecay(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() got := u.ComputeResidualAfterDecay(tc.start, tc.end, tc.commit) - if tc.name == "Commit Very Close To End" { - want1 := big.NewInt(10000000000000000) - want2 := big.NewInt(10000000000000008) - if got.Cmp(want1) != 0 && got.Cmp(want2) != 0 { - t.Errorf("ComputeResidualAfterDecay(%d, %d, %d) = %v, want either %v or %v", tc.start, tc.end, tc.commit, got, want1, want2) - } - } else { - if got.Cmp(tc.want) != 0 { - t.Errorf("ComputeResidualAfterDecay(%d, %d, %d) = %v, want %v", tc.start, tc.end, tc.commit, got, tc.want) - } + + if got.Cmp(tc.want) != 0 { + t.Errorf("ComputeResidualAfterDecay(%d, %d, %d) = %v, want %v", tc.start, tc.end, tc.commit, got, tc.want) } }) }