From 538c3476305ec5258b49a75b4b9b1185e8af503c Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Thu, 12 Jun 2025 20:03:07 +0300 Subject: [PATCH 01/43] feat: implement node rewards calculation and API endpoint --- node-registrar/pkg/server/handlers.go | 33 +++++ node-registrar/pkg/server/rewards.go | 89 ++++++++++++++ node-registrar/pkg/server/rewards_test.go | 142 ++++++++++++++++++++++ node-registrar/pkg/server/routes.go | 1 + 4 files changed, 265 insertions(+) create mode 100644 node-registrar/pkg/server/rewards.go create mode 100644 node-registrar/pkg/server/rewards_test.go diff --git a/node-registrar/pkg/server/handlers.go b/node-registrar/pkg/server/handlers.go index 86b6dff..2fb5530 100644 --- a/node-registrar/pkg/server/handlers.go +++ b/node-registrar/pkg/server/handlers.go @@ -281,6 +281,39 @@ func (s Server) getNodeHandler(c *gin.Context) { c.JSON(http.StatusOK, node) } +func (s Server) getNodeRewardHandler(c *gin.Context) { + nodeID := c.Param("node_id") + + id, err := strconv.ParseUint(nodeID, 10, 64) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid node id"}) + return + } + + node, err := s.db.GetNode(id) + if err != nil { + if errors.Is(err, db.ErrRecordNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // TODO: calculate uptime percentage based on uptime reports + // uptimeReports, err := s.db.GetUptimeReports(id, time.Now().Add(-time.Hour*48), time.Now()) + // if err != nil { + // c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + // return + // } + rewards, err := CalculateMonthlyReward(node.Resources, 100) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + } + c.JSON(http.StatusOK, rewards) +} + type NodeRegistrationRequest struct { TwinID uint64 `json:"twin_id" binding:"required,min=1"` FarmID uint64 `json:"farm_id" binding:"required,min=1"` diff --git a/node-registrar/pkg/server/rewards.go b/node-registrar/pkg/server/rewards.go new file mode 100644 index 0000000..31d27ea --- /dev/null +++ b/node-registrar/pkg/server/rewards.go @@ -0,0 +1,89 @@ +package server + +import ( + "errors" + "math" + + "github.com/threefoldtech/tfgrid4-sdk-go/node-registrar/pkg/db" +) + +const ( + + // Certified capacity rewards factor + MEMORY_REWARD_PER_GB = 8.0 // INCA per GB + SSD_REWARD_PER_TB = 31.5 // INCA per TB + HDD_REWARD_PER_TB = 7.0 // INCA per TB + + // Reward distribution + FARMER_REWARD_PERCENTAGE = 0.6 // 60% of the reward goes to the node owner + TF_REWARD_PERCENTAGE = 0.2 // 20% of the reward goes to the Threefold Foundation + FP_REWARD_PERCENTAGE = 0.2 // 20% of the reward goes to the Farming Pool + + // Error messages + INVALID_UPTIME_PERCENTAGE = "invalid uptime percentage" +) + +type Reward struct { + FarmerReward float64 + TFReward float64 + FPReward float64 + Total float64 +} + +// CalculateMonthlyReward calculates the monthly reward in INCA for a given node capacity. +// +// The rewards are calculated as follows: +// +// - Certified capacity rewards factor: +// - Memory: 8.0 INCA per GB +// - SSD: 31.5 INCA per TB +// - HDD: 7.0 INCA per TB +// +// - Reward distribution: +// - Farmer: 60% of the reward +// - Threefold Foundation: 20% of the reward +// - Farming Pool: 20% of the reward +// +// CalculateMonthlyReward returns the following values: +// - FarmerReward: the reward for the node owner +// - TFReward: the reward for the Threefold Foundation +// - FPReward: the reward for the Farming Pool +// - Total: the total reward +// +// CalculateMonthlyReward takes the following parameters: +// +// - capacity: the node capacity, in form of a db.Resources Type +// - upTimePercentage: the uptime percentage of the node, as a float64 value between 0 and 100 +// +// - Note: if the uptime percentage is less than 90, the node will not reserve any rewards. + +func CalculateMonthlyReward(capacity db.Resources, upTimePercentage float64) (Reward, error) { + if upTimePercentage < 0 || upTimePercentage > 100 { + return Reward{}, errors.New(INVALID_UPTIME_PERCENTAGE) + } + if upTimePercentage < 90 { + return Reward{ + FarmerReward: 0, + TFReward: 0, + FPReward: 0, + Total: 0, + }, nil + } + + total := (bytesToGB(capacity.MRU)*MEMORY_REWARD_PER_GB + bytesToTB(capacity.SRU)*SSD_REWARD_PER_TB + bytesToTB(capacity.HRU)*HDD_REWARD_PER_TB) * (upTimePercentage / 100) + + return Reward{ + FarmerReward: total * FARMER_REWARD_PERCENTAGE, + TFReward: total * TF_REWARD_PERCENTAGE, + FPReward: total * FP_REWARD_PERCENTAGE, + Total: total, + }, nil +} + +func bytesToGB(bytes uint64) float64 { + return float64(bytes) / math.Pow(1024, 3) +} + +func bytesToTB(bytes uint64) float64 { + return float64(bytes) / math.Pow(1024, 4) +} diff --git a/node-registrar/pkg/server/rewards_test.go b/node-registrar/pkg/server/rewards_test.go new file mode 100644 index 0000000..f17b857 --- /dev/null +++ b/node-registrar/pkg/server/rewards_test.go @@ -0,0 +1,142 @@ +package server + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/threefoldtech/tfgrid4-sdk-go/node-registrar/pkg/db" +) + +func TestCalculateMonthlyReward(t *testing.T) { + // Define standard capacity for most tests + standardCapacity := db.Resources{ + CRU: 8, + MRU: 68719476736, + SRU: 4398046511104, + HRU: 17592186044416, + } + + // Define a small capacity for precision testing + preciseCapacity := db.Resources{ + CRU: 1, + MRU: 1073741824, // 1 GB exactly + SRU: 0, + HRU: 0, + } + + tests := []struct { + name string + capacity db.Resources + upTimePercentage float64 + wantError bool + expectedErrorMsg string + }{ + { + name: "negative uptime percentage", + capacity: standardCapacity, + upTimePercentage: -1, + wantError: true, + expectedErrorMsg: INVALID_UPTIME_PERCENTAGE, + }, + { + name: "valid uptime (5%)", + capacity: standardCapacity, + upTimePercentage: 5, + wantError: false, + }, + { + name: "uptime over 100%", + capacity: standardCapacity, + upTimePercentage: 101, + wantError: true, + expectedErrorMsg: INVALID_UPTIME_PERCENTAGE, + }, + { + name: "uptime at 100%", + capacity: standardCapacity, + upTimePercentage: 100, + wantError: false, + }, + { + name: "uptime below threshold (80%)", + capacity: standardCapacity, + upTimePercentage: 80, + wantError: false, + }, + { + name: "uptime at threshold (90%)", + capacity: standardCapacity, + upTimePercentage: 90, + wantError: false, + }, + { + name: "uptime at 98%", + capacity: standardCapacity, + upTimePercentage: 98, + wantError: false, + }, + { + name: "precision test with small capacity", + capacity: preciseCapacity, + upTimePercentage: 100, + wantError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := CalculateMonthlyReward(tt.capacity, tt.upTimePercentage) + + // Error check + if tt.wantError { + if err == nil { + t.Fatal("expected error, got nil") + } + assert.Equal(t, tt.expectedErrorMsg, err.Error()) + return + } + + // No error expected + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Check reward calculation + AssertMonthlyReward(t, tt.capacity, tt.upTimePercentage, got) + }) + } +} + +func AssertMonthlyReward(t testing.TB, resources db.Resources, upTimePercentage float64, got Reward) { + t.Helper() + + if upTimePercentage < 90 { + assert.Equal(t, Reward{}, got) + return + } + + // Calculate rewards + memoryReward := bytesToGB(resources.MRU) * MEMORY_REWARD_PER_GB + ssdReward := bytesToTB(resources.SRU) * SSD_REWARD_PER_TB + hddReward := bytesToTB(resources.HRU) * HDD_REWARD_PER_TB + + // Calculate total rewards + total := memoryReward + ssdReward + hddReward + + // Apply uptime percentage + total = total * (upTimePercentage / 100) + + expected := Reward{ + FarmerReward: total * FARMER_REWARD_PERCENTAGE, + TFReward: total * TF_REWARD_PERCENTAGE, + FPReward: total * FP_REWARD_PERCENTAGE, + Total: total, + } + + // Use precise floating point comparison + const delta = 1e-9 // Very small acceptable difference + assert.InDelta(t, expected.FarmerReward, got.FarmerReward, delta) + assert.InDelta(t, expected.TFReward, got.TFReward, delta) + assert.InDelta(t, expected.FPReward, got.FPReward, delta) + assert.InDelta(t, expected.Total, got.Total, delta) +} diff --git a/node-registrar/pkg/server/routes.go b/node-registrar/pkg/server/routes.go index 5aff98d..134d916 100644 --- a/node-registrar/pkg/server/routes.go +++ b/node-registrar/pkg/server/routes.go @@ -41,6 +41,7 @@ func (s *Server) registerRoutes(r *gin.RouterGroup) { publicNodeRoutes := r.Group("nodes") publicNodeRoutes.GET("", s.listNodesHandler) publicNodeRoutes.GET("/:node_id", s.getNodeHandler) + publicNodeRoutes.GET("/:node_id/reward", s.getNodeRewardHandler) // protected by node key protectedNodeRoutes := r.Group("nodes", s.AuthMiddleware()) protectedNodeRoutes.POST("", s.registerNodeHandler) From 3ed761ed7b10e271a49d742138b8a0429d756c46 Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Thu, 12 Jun 2025 20:06:18 +0300 Subject: [PATCH 02/43] test: add test case for uptime percentage with decimal value --- node-registrar/pkg/server/rewards_test.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/node-registrar/pkg/server/rewards_test.go b/node-registrar/pkg/server/rewards_test.go index f17b857..aff148e 100644 --- a/node-registrar/pkg/server/rewards_test.go +++ b/node-registrar/pkg/server/rewards_test.go @@ -69,6 +69,12 @@ func TestCalculateMonthlyReward(t *testing.T) { upTimePercentage: 90, wantError: false, }, + { + name: "uptime below threshold as float (89.1%)", + capacity: standardCapacity, + upTimePercentage: 89.1, + wantError: false, + }, { name: "uptime at 98%", capacity: standardCapacity, @@ -86,7 +92,7 @@ func TestCalculateMonthlyReward(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := CalculateMonthlyReward(tt.capacity, tt.upTimePercentage) - + // Error check if tt.wantError { if err == nil { @@ -95,12 +101,12 @@ func TestCalculateMonthlyReward(t *testing.T) { assert.Equal(t, tt.expectedErrorMsg, err.Error()) return } - + // No error expected if err != nil { t.Fatalf("unexpected error: %v", err) } - + // Check reward calculation AssertMonthlyReward(t, tt.capacity, tt.upTimePercentage, got) }) @@ -114,15 +120,15 @@ func AssertMonthlyReward(t testing.TB, resources db.Resources, upTimePercentage assert.Equal(t, Reward{}, got) return } - + // Calculate rewards memoryReward := bytesToGB(resources.MRU) * MEMORY_REWARD_PER_GB ssdReward := bytesToTB(resources.SRU) * SSD_REWARD_PER_TB hddReward := bytesToTB(resources.HRU) * HDD_REWARD_PER_TB - + // Calculate total rewards total := memoryReward + ssdReward + hddReward - + // Apply uptime percentage total = total * (upTimePercentage / 100) From 929bad715f3dfa4f1e3cff080bfcd98199053594 Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Sun, 15 Jun 2025 10:57:58 +0300 Subject: [PATCH 03/43] refactor: replace error string with proper error variable for invalid uptime percentage --- node-registrar/pkg/server/rewards.go | 7 ++++--- node-registrar/pkg/server/rewards_test.go | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/node-registrar/pkg/server/rewards.go b/node-registrar/pkg/server/rewards.go index 31d27ea..2df86e3 100644 --- a/node-registrar/pkg/server/rewards.go +++ b/node-registrar/pkg/server/rewards.go @@ -19,10 +19,11 @@ const ( TF_REWARD_PERCENTAGE = 0.2 // 20% of the reward goes to the Threefold Foundation FP_REWARD_PERCENTAGE = 0.2 // 20% of the reward goes to the Farming Pool - // Error messages - INVALID_UPTIME_PERCENTAGE = "invalid uptime percentage" ) +// Error messages +var ErrInvalidUptimePercentage = errors.New("invalid uptime percentage") + type Reward struct { FarmerReward float64 TFReward float64 @@ -59,7 +60,7 @@ type Reward struct { func CalculateMonthlyReward(capacity db.Resources, upTimePercentage float64) (Reward, error) { if upTimePercentage < 0 || upTimePercentage > 100 { - return Reward{}, errors.New(INVALID_UPTIME_PERCENTAGE) + return Reward{}, ErrInvalidUptimePercentage } if upTimePercentage < 90 { return Reward{ diff --git a/node-registrar/pkg/server/rewards_test.go b/node-registrar/pkg/server/rewards_test.go index aff148e..42a8f38 100644 --- a/node-registrar/pkg/server/rewards_test.go +++ b/node-registrar/pkg/server/rewards_test.go @@ -29,14 +29,14 @@ func TestCalculateMonthlyReward(t *testing.T) { capacity db.Resources upTimePercentage float64 wantError bool - expectedErrorMsg string + expectedError error }{ { name: "negative uptime percentage", capacity: standardCapacity, upTimePercentage: -1, wantError: true, - expectedErrorMsg: INVALID_UPTIME_PERCENTAGE, + expectedError: ErrInvalidUptimePercentage, }, { name: "valid uptime (5%)", @@ -49,7 +49,7 @@ func TestCalculateMonthlyReward(t *testing.T) { capacity: standardCapacity, upTimePercentage: 101, wantError: true, - expectedErrorMsg: INVALID_UPTIME_PERCENTAGE, + expectedError: ErrInvalidUptimePercentage, }, { name: "uptime at 100%", @@ -98,7 +98,7 @@ func TestCalculateMonthlyReward(t *testing.T) { if err == nil { t.Fatal("expected error, got nil") } - assert.Equal(t, tt.expectedErrorMsg, err.Error()) + assert.Equal(t, tt.expectedError, err) return } From bdcd279929d6a8dc3a59a8cc1ac27f65100ef189 Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Thu, 19 Jun 2025 16:24:38 +0300 Subject: [PATCH 04/43] docs: mention that the duration in seconds --- node-registrar/pkg/db/models.go | 2 +- node-registrar/pkg/server/handlers.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/node-registrar/pkg/db/models.go b/node-registrar/pkg/db/models.go index ee98059..afafe7f 100644 --- a/node-registrar/pkg/db/models.go +++ b/node-registrar/pkg/db/models.go @@ -67,7 +67,7 @@ func (n *Node) BeforeCreate(tx *gorm.DB) (err error) { type UptimeReport struct { ID uint64 `gorm:"primaryKey;autoIncrement"` NodeID uint64 `gorm:"index" json:"node_id"` - Duration time.Duration `json:"duration" swaggertype:"integer"` // Uptime duration for this period + Duration time.Duration `json:"duration" swaggertype:"integer"` // Uptime duration for this period in seconds Timestamp time.Time `json:"timestamp" gorm:"index"` WasRestart bool `json:"was_restart"` // True if this report followed a restart CreatedAt time.Time `json:"created_at"` diff --git a/node-registrar/pkg/server/handlers.go b/node-registrar/pkg/server/handlers.go index 2fb5530..c770ec2 100644 --- a/node-registrar/pkg/server/handlers.go +++ b/node-registrar/pkg/server/handlers.go @@ -455,7 +455,7 @@ func (s *Server) updateNodeHandler(c *gin.Context) { } type UptimeReportRequest struct { - Uptime uint64 `json:"uptime" binding:"required"` + Uptime uint64 `json:"uptime" binding:"required"` // in seconds Timestamp int64 `json:"timestamp" binding:"required"` } From 1dcc4319231a72c98216efd85a9fdec5c60de954 Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Thu, 19 Jun 2025 16:27:05 +0300 Subject: [PATCH 05/43] feat: implement node uptime calculation based on report history --- node-registrar/pkg/server/handlers.go | 20 ++- node-registrar/pkg/server/rewards.go | 88 ++++++++++++ node-registrar/pkg/server/rewards_test.go | 161 ++++++++++++++++++++++ 3 files changed, 262 insertions(+), 7 deletions(-) diff --git a/node-registrar/pkg/server/handlers.go b/node-registrar/pkg/server/handlers.go index c770ec2..89c8a3b 100644 --- a/node-registrar/pkg/server/handlers.go +++ b/node-registrar/pkg/server/handlers.go @@ -301,13 +301,19 @@ func (s Server) getNodeRewardHandler(c *gin.Context) { return } - // TODO: calculate uptime percentage based on uptime reports - // uptimeReports, err := s.db.GetUptimeReports(id, time.Now().Add(-time.Hour*48), time.Now()) - // if err != nil { - // c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - // return - // } - rewards, err := CalculateMonthlyReward(node.Resources, 100) + now := time.Now() + currentPeriodStart := calculateCurrentPeriodStart(now) + + reports, err := s.db.GetUptimeReports(id, currentPeriodStart, now) + + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + upTimePercentage := calculateUpTimePercentage(reports, currentPeriodStart, now) + + rewards, err := CalculateMonthlyReward(node.Resources, upTimePercentage) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } diff --git a/node-registrar/pkg/server/rewards.go b/node-registrar/pkg/server/rewards.go index 2df86e3..5276151 100644 --- a/node-registrar/pkg/server/rewards.go +++ b/node-registrar/pkg/server/rewards.go @@ -2,7 +2,9 @@ package server import ( "errors" + "log" "math" + "time" "github.com/threefoldtech/tfgrid4-sdk-go/node-registrar/pkg/db" ) @@ -19,6 +21,15 @@ const ( TF_REWARD_PERCENTAGE = 0.2 // 20% of the reward goes to the Threefold Foundation FP_REWARD_PERCENTAGE = 0.2 // 20% of the reward goes to the Farming Pool + // TODO update the start timestamp + FIRST_PERIOD_START_TIMESTAMP int64 = 1522501000 + + // uptime events are supposed to happen every 40 minutes. + // here we set this to one hour (3600 sec) to allow some room. + UPTIME_EVENTS_INTERVAL = 3600 + + // The duration of a standard period, as used by the minting payouts, in seconds. + STANDARD_PERIOD_DURATION int64 = 24 * 60 * 60 * (365*3 + 366*2) / 60 ) // Error messages @@ -88,3 +99,80 @@ func bytesToGB(bytes uint64) float64 { func bytesToTB(bytes uint64) float64 { return float64(bytes) / math.Pow(1024, 4) } + +// calculateUpTimePercentage calculates the uptime percentage for a given node within a specific period. +// +// This function takes a slice of UptimeReport, a period start time and a current time as parameters. +// It calculates the uptime percentage by comparing the expected uptime (calculated by subtracting the timestamp of the previous report from the current report) +// with the actual uptime (calculated from the duration the current report). +// If the actual uptime is less than the expected uptime, the difference is counted as downtime. +// Additionally, if there is a gap equals or larger than the @UPTIME_EVENTS_INTERVAL between the last report and now, add it to the downtime. +// The uptime percentage is then calculated by subtracting the total downtime from the total elapsed time since the period start and dividing the result by the total elapsed time. +// The result is then multiplied by 100 to get the percentage. +// +// Note: This function assumes that the reports are ordered by timestamp in ascending order. +// +// Parameters: +// - reports: a slice of UptimeReport +// - periodStart: the start of the period +// - now: the current time +// +// Returns: +// - a float64 representing the uptime percentage +func calculateUpTimePercentage(reports []db.UptimeReport, periodStart, now time.Time) float64 { + + if len(reports) == 0 { + return 0.0 + } + + //append starter point + reports = append([]db.UptimeReport{ + { + Timestamp: periodStart, + Duration: time.Duration(0), + }, + }, reports...) + + var downtime time.Duration = 0 + for i := 0; i < len(reports)-1; i++ { + + curr := reports[i] + next := reports[i+1] + + curr.Duration = time.Duration(curr.Duration * time.Second) + next.Duration = time.Duration(next.Duration * time.Second) + + //TODO should we check the order of timestamp? + + expected := next.Timestamp.Sub(curr.Timestamp).Truncate(time.Second) + actual := next.Duration.Truncate(time.Second) + if curr.Duration > next.Duration || actual < expected { + downtime += expected - actual + } + } + // if there is a gap equals or larger than th @UPTIME_EVENTS_INTERVAL between the last report and now, add it to the downtime + elapsedSinceLast := now.Sub(reports[len(reports)-1].Timestamp).Truncate(time.Second) + if elapsedSinceLast.Seconds() >= UPTIME_EVENTS_INTERVAL { + log.Println("elapsedSinceLast: ", elapsedSinceLast.Hours()) + downtime += elapsedSinceLast + } + return truncateFloat(float64(now.Sub(periodStart)-downtime)/float64(now.Sub(periodStart))*100, 2) +} + +// calculateCurrentPeriodStart returns the start of the current period. +// +// The function uses the unix timestamp of the first period start (FIRST_PERIOD_START_TIMESTAMP) and the standard period duration (STANDARD_PERIOD_DURATION) to calculate the start of the current period. +// +// Parameter: +// - now: the reference time used to calculate the current period start +func calculateCurrentPeriodStart(now time.Time) time.Time { + secondsSinceFirstPeriod := now.Unix() - FIRST_PERIOD_START_TIMESTAMP + periodOffset := secondsSinceFirstPeriod % STANDARD_PERIOD_DURATION + currentPeriodStart := now.Unix() - periodOffset + return time.Unix(currentPeriodStart, 0) +} + +func truncateFloat(num float64, precision int) float64 { + pow := math.Pow(10, float64(precision)) + return math.Trunc(num*pow) / pow +} diff --git a/node-registrar/pkg/server/rewards_test.go b/node-registrar/pkg/server/rewards_test.go index 42a8f38..1b6b8f4 100644 --- a/node-registrar/pkg/server/rewards_test.go +++ b/node-registrar/pkg/server/rewards_test.go @@ -1,7 +1,9 @@ package server import ( + "math" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/threefoldtech/tfgrid4-sdk-go/node-registrar/pkg/db" @@ -146,3 +148,162 @@ func AssertMonthlyReward(t testing.TB, resources db.Resources, upTimePercentage assert.InDelta(t, expected.FPReward, got.FPReward, delta) assert.InDelta(t, expected.Total, got.Total, delta) } + +// TestCalculateCurrentPeriodStart tests the calculateCurrentPeriodStart function with different inputs +func TestCalculateCurrentPeriodStart(t *testing.T) { + tests := []struct { + name string + inputTime time.Time + expectedTime time.Time + }{ + { + name: "First period start timestamp", + inputTime: time.Unix(FIRST_PERIOD_START_TIMESTAMP, 0), + expectedTime: time.Unix(FIRST_PERIOD_START_TIMESTAMP, 0), + }, + { + name: "Exactly at second period start", + inputTime: time.Unix(FIRST_PERIOD_START_TIMESTAMP+STANDARD_PERIOD_DURATION, 0), + expectedTime: time.Unix(FIRST_PERIOD_START_TIMESTAMP+STANDARD_PERIOD_DURATION, 0), + }, + { + name: "Middle of a period", + inputTime: time.Unix(FIRST_PERIOD_START_TIMESTAMP+STANDARD_PERIOD_DURATION/2, 0), + expectedTime: time.Unix(FIRST_PERIOD_START_TIMESTAMP, 0), + }, + { + name: "Near end of a period", + inputTime: time.Unix(FIRST_PERIOD_START_TIMESTAMP+STANDARD_PERIOD_DURATION-1, 0), + expectedTime: time.Unix(FIRST_PERIOD_START_TIMESTAMP, 0), + }, + { + name: "Multiple periods later", + inputTime: time.Unix(FIRST_PERIOD_START_TIMESTAMP+3*STANDARD_PERIOD_DURATION+STANDARD_PERIOD_DURATION/3, 0), + expectedTime: time.Unix(FIRST_PERIOD_START_TIMESTAMP+3*STANDARD_PERIOD_DURATION, 0), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Call the actual function with our test time + result := calculateCurrentPeriodStart(tt.inputTime) + + // Check that the result equals our expected time + assert.Equal(t, tt.expectedTime.Unix(), result.Unix()) + + // Additionally, verify that we can manually calculate the same result + // secondsSinceCurrentPeriodStart := (tt.inputTime.Unix() - FIRST_PERIOD_START_TIMESTAMP) % STANDARD_PERIOD_DURATION + // manualCalculation := time.Unix(FIRST_PERIOD_START_TIMESTAMP+secondsSinceCurrentPeriodStart, 0) + // assert.Equal(t, manualCalculation.Unix(), result.Unix()) + }) + } +} + +func TestCalculateUpTimePercentage(t *testing.T) { + type args struct { + reports []db.UptimeReport + periodStart time.Time + } + now := time.Now().Truncate(time.Second) + tests := []struct { + name string + args args + expected float64 + }{ + { + name: "All uptime, no downtime (40 min gaps)", + args: args{ + periodStart: now.Add(-160 * time.Minute), // Start 160 min ago (for 4 reports) + reports: []db.UptimeReport{ + {Timestamp: now.Add(-120 * time.Minute), Duration: 2400}, // 40 min (2400 seconds) + {Timestamp: now.Add(-80 * time.Minute), Duration: 2400}, // 40 min (2400 seconds) + {Timestamp: now.Add(-40 * time.Minute), Duration: 2400}, // 40 min (2400 seconds) + {Timestamp: now, Duration: 2400}, // 40 min (2400 seconds) + }, + }, + expected: 100.0, + }, + { + name: "50% uptime — only half the reports received", + args: args{ + periodStart: now.Add(-160 * time.Minute), // full 160 mins = 9600s + reports: []db.UptimeReport{ + {Timestamp: now.Add(-120 * time.Minute), Duration: 2400}, // 40 min (2400 seconds) + {Timestamp: now.Add(-80 * time.Minute), Duration: 2400}, // 40 min (2400 seconds) + }, + }, + expected: 50.0, + }, + { + name: "0% uptime — no reports received", + args: args{ + periodStart: now.Add(-160 * time.Minute), // full 160 mins = 9600s + reports: []db.UptimeReport{}, + }, + expected: 0.0, + }, + { + name: "allowance for only one report received, after 1hour", + args: args{ + periodStart: now.Add(-60 * time.Minute), // full 60 mins + reports: []db.UptimeReport{ + {Timestamp: now.Add(-40 * time.Minute), Duration: 2400}, // 40 min (2400 seconds) + }, + }, + expected: 100.0, + }, + { + name: "one report received after 3 report intervals, with full duration of 120 min", + args: args{ + periodStart: now.Add(-130 * time.Minute), // full 130 mins = 7800s + reports: []db.UptimeReport{ + {Timestamp: now.Add(-10 * time.Minute), Duration: 7200}, // 120 min (7200 seconds) + }, + }, + expected: 100.0, + }, + { + name: "one report after single report interval with 30min uptime", + args: args{ + periodStart: now.Add(-40 * time.Minute), + reports: []db.UptimeReport{ + {Timestamp: now, Duration: 1200}, // 20 min (1200 seconds) + }, + }, + expected: 50.0, + }, + { + name: "Duration decreases with time", + args: args{ + periodStart: now.Add(-120 * time.Minute), + reports: []db.UptimeReport{ + {Timestamp: now.Add(-80 * time.Minute), Duration: 2400}, // 40 min (2400 seconds) + {Timestamp: now.Add(-40 * time.Minute), Duration: 1800}, // 30 min (1800 seconds) + {Timestamp: now, Duration: 1200}, // 20 min (1200 seconds) + }, + }, + expected: 75.0, + }, + { + name: "Duration increases with time", + args: args{ + periodStart: now.Add(-120 * time.Minute), + reports: []db.UptimeReport{ + {Timestamp: now.Add(-80 * time.Minute), Duration: 1200}, // 20 min (1200 seconds) + {Timestamp: now.Add(-40 * time.Minute), Duration: 1800}, // 30 min (1800 seconds) + {Timestamp: now, Duration: 2400}, // 40 min (2400 seconds) + }, + }, + expected: 75.0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := calculateUpTimePercentage(tt.args.reports, tt.args.periodStart, now) + if math.Abs(got-tt.expected) > 0.01 { + t.Errorf("calculateUpTimePercentage() = %v, want %v", got, tt.expected) + } + }) + } +} From 66b2a66386fcba3c7c41e79177a8507ac1b11cfb Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Thu, 19 Jun 2025 18:22:23 +0300 Subject: [PATCH 06/43] feat: add uptime percentage to rewards struct --- node-registrar/pkg/server/rewards.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/node-registrar/pkg/server/rewards.go b/node-registrar/pkg/server/rewards.go index 5276151..062f4f2 100644 --- a/node-registrar/pkg/server/rewards.go +++ b/node-registrar/pkg/server/rewards.go @@ -2,7 +2,6 @@ package server import ( "errors" - "log" "math" "time" @@ -36,10 +35,11 @@ const ( var ErrInvalidUptimePercentage = errors.New("invalid uptime percentage") type Reward struct { - FarmerReward float64 - TFReward float64 - FPReward float64 - Total float64 + FarmerReward float64 + TFReward float64 + FPReward float64 + Total float64 + UpTimePercentage float64 } // CalculateMonthlyReward calculates the monthly reward in INCA for a given node capacity. @@ -85,10 +85,11 @@ func CalculateMonthlyReward(capacity db.Resources, upTimePercentage float64) (Re total := (bytesToGB(capacity.MRU)*MEMORY_REWARD_PER_GB + bytesToTB(capacity.SRU)*SSD_REWARD_PER_TB + bytesToTB(capacity.HRU)*HDD_REWARD_PER_TB) * (upTimePercentage / 100) return Reward{ - FarmerReward: total * FARMER_REWARD_PERCENTAGE, - TFReward: total * TF_REWARD_PERCENTAGE, - FPReward: total * FP_REWARD_PERCENTAGE, - Total: total, + FarmerReward: total * FARMER_REWARD_PERCENTAGE, + TFReward: total * TF_REWARD_PERCENTAGE, + FPReward: total * FP_REWARD_PERCENTAGE, + Total: total, + UpTimePercentage: upTimePercentage, }, nil } @@ -153,7 +154,6 @@ func calculateUpTimePercentage(reports []db.UptimeReport, periodStart, now time. // if there is a gap equals or larger than th @UPTIME_EVENTS_INTERVAL between the last report and now, add it to the downtime elapsedSinceLast := now.Sub(reports[len(reports)-1].Timestamp).Truncate(time.Second) if elapsedSinceLast.Seconds() >= UPTIME_EVENTS_INTERVAL { - log.Println("elapsedSinceLast: ", elapsedSinceLast.Hours()) downtime += elapsedSinceLast } return truncateFloat(float64(now.Sub(periodStart)-downtime)/float64(now.Sub(periodStart))*100, 2) From 0ea416e43e4445a4fc635019d55a7302e8b4f946 Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Sun, 22 Jun 2025 13:03:19 +0300 Subject: [PATCH 07/43] docs: add swagger docs for the rewards endpoint --- node-registrar/docs/docs.go | 46 +++++++++++++++++++++++++++ node-registrar/docs/swagger.json | 46 +++++++++++++++++++++++++++ node-registrar/docs/swagger.yaml | 32 +++++++++++++++++++ node-registrar/pkg/server/handlers.go | 10 ++++++ 4 files changed, 134 insertions(+) diff --git a/node-registrar/docs/docs.go b/node-registrar/docs/docs.go index b570127..21f1725 100644 --- a/node-registrar/docs/docs.go +++ b/node-registrar/docs/docs.go @@ -694,6 +694,52 @@ const docTemplate = `{ } } }, + "/nodes/{node_id}/reward": { + "get": { + "description": "Retrieves reward calculation for a specific node based on resources and uptime", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "nodes" + ], + "summary": "Get node monthly reward information", + "parameters": [ + { + "type": "integer", + "description": "Node ID", + "name": "node_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Rewards details with the node uptime percentage", + "schema": { + "$ref": "#/definitions/server.Reward" + } + }, + "400": { + "description": "Invalid node ID", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Node not found", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/nodes/{node_id}/uptime": { "post": { "description": "Submit uptime report for a node", diff --git a/node-registrar/docs/swagger.json b/node-registrar/docs/swagger.json index f666144..c5ae313 100644 --- a/node-registrar/docs/swagger.json +++ b/node-registrar/docs/swagger.json @@ -687,6 +687,52 @@ } } }, + "/nodes/{node_id}/reward": { + "get": { + "description": "Retrieves reward calculation for a specific node based on resources and uptime", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "nodes" + ], + "summary": "Get node monthly reward information", + "parameters": [ + { + "type": "integer", + "description": "Node ID", + "name": "node_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Rewards details with the node uptime percentage", + "schema": { + "$ref": "#/definitions/server.Reward" + } + }, + "400": { + "description": "Invalid node ID", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Node not found", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/nodes/{node_id}/uptime": { "post": { "description": "Submit uptime report for a node", diff --git a/node-registrar/docs/swagger.yaml b/node-registrar/docs/swagger.yaml index b4f3e59..016eb05 100644 --- a/node-registrar/docs/swagger.yaml +++ b/node-registrar/docs/swagger.yaml @@ -697,6 +697,38 @@ paths: summary: Update node tags: - nodes + /nodes/{node_id}/reward: + get: + consumes: + - application/json + description: Retrieves reward calculation for a specific node based on resources + and uptime + parameters: + - description: Node ID + in: path + name: node_id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Rewards details with the node uptime percentage + schema: + $ref: '#/definitions/server.Reward' + "400": + description: Invalid node ID + schema: + additionalProperties: true + type: object + "404": + description: Node not found + schema: + additionalProperties: true + type: object + summary: Get node monthly reward information + tags: + - nodes /nodes/{node_id}/uptime: post: consumes: diff --git a/node-registrar/pkg/server/handlers.go b/node-registrar/pkg/server/handlers.go index 89c8a3b..88da75f 100644 --- a/node-registrar/pkg/server/handlers.go +++ b/node-registrar/pkg/server/handlers.go @@ -281,6 +281,16 @@ func (s Server) getNodeHandler(c *gin.Context) { c.JSON(http.StatusOK, node) } +// @Summary Get node monthly reward information +// @Description Retrieves reward calculation for a specific node based on resources and uptime +// @Tags nodes +// @Accept json +// @Produce json +// @Param node_id path int true "Node ID" +// @Success 200 {object} Reward "Rewards details with the node uptime percentage" +// @Failure 400 {object} map[string]any "Invalid node ID" +// @Failure 404 {object} map[string]any "Node not found" +// @Router /nodes/{node_id}/reward [get] func (s Server) getNodeRewardHandler(c *gin.Context) { nodeID := c.Param("node_id") From f60aa13ee2ae529d997a74ffcb8b588f181f104d Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Sun, 22 Jun 2025 13:04:14 +0300 Subject: [PATCH 08/43] feat: show uptime percentage in the response of no rewards --- node-registrar/pkg/server/rewards.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/node-registrar/pkg/server/rewards.go b/node-registrar/pkg/server/rewards.go index 062f4f2..d6b0f06 100644 --- a/node-registrar/pkg/server/rewards.go +++ b/node-registrar/pkg/server/rewards.go @@ -74,12 +74,7 @@ func CalculateMonthlyReward(capacity db.Resources, upTimePercentage float64) (Re return Reward{}, ErrInvalidUptimePercentage } if upTimePercentage < 90 { - return Reward{ - FarmerReward: 0, - TFReward: 0, - FPReward: 0, - Total: 0, - }, nil + return Reward{UpTimePercentage: upTimePercentage}, nil } total := (bytesToGB(capacity.MRU)*MEMORY_REWARD_PER_GB + bytesToTB(capacity.SRU)*SSD_REWARD_PER_TB + bytesToTB(capacity.HRU)*HDD_REWARD_PER_TB) * (upTimePercentage / 100) From 38e8082508e5005b3edf70de2ab3c9391a3a9d2b Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Sun, 22 Jun 2025 13:11:00 +0300 Subject: [PATCH 09/43] fix: rename reward endpoint to rewards --- node-registrar/docs/swagger.json | 24 ++++++++++++++++++++++-- node-registrar/docs/swagger.yaml | 15 ++++++++++++++- node-registrar/pkg/server/handlers.go | 3 ++- node-registrar/pkg/server/routes.go | 2 +- 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/node-registrar/docs/swagger.json b/node-registrar/docs/swagger.json index c5ae313..c13c1a1 100644 --- a/node-registrar/docs/swagger.json +++ b/node-registrar/docs/swagger.json @@ -687,7 +687,7 @@ } } }, - "/nodes/{node_id}/reward": { + "/nodes/{node_id}/rewards": { "get": { "description": "Retrieves reward calculation for a specific node based on resources and uptime", "consumes": [ @@ -1150,7 +1150,27 @@ } } }, - "server.UpdateAccountRequest": { + "server.Reward": { + "type": "object", + "properties": { + "farmerReward": { + "type": "number" + }, + "fpreward": { + "type": "number" + }, + "tfreward": { + "type": "number" + }, + "total": { + "type": "number" + }, + "upTimePercentage": { + "type": "number" + } + } + }, + "pkg_server.UpdateAccountRequest": { "type": "object", "properties": { "relays": { diff --git a/node-registrar/docs/swagger.yaml b/node-registrar/docs/swagger.yaml index 016eb05..c245d32 100644 --- a/node-registrar/docs/swagger.yaml +++ b/node-registrar/docs/swagger.yaml @@ -170,6 +170,19 @@ definitions: - serial_number - twin_id type: object + server.Reward: + properties: + farmerReward: + type: number + fpreward: + type: number + tfreward: + type: number + total: + type: number + upTimePercentage: + type: number + type: object server.UpdateAccountRequest: properties: relays: @@ -697,7 +710,7 @@ paths: summary: Update node tags: - nodes - /nodes/{node_id}/reward: + /nodes/{node_id}/rewards: get: consumes: - application/json diff --git a/node-registrar/pkg/server/handlers.go b/node-registrar/pkg/server/handlers.go index 88da75f..e524d0f 100644 --- a/node-registrar/pkg/server/handlers.go +++ b/node-registrar/pkg/server/handlers.go @@ -290,7 +290,7 @@ func (s Server) getNodeHandler(c *gin.Context) { // @Success 200 {object} Reward "Rewards details with the node uptime percentage" // @Failure 400 {object} map[string]any "Invalid node ID" // @Failure 404 {object} map[string]any "Node not found" -// @Router /nodes/{node_id}/reward [get] +// @Router /nodes/{node_id}/rewards [get] func (s Server) getNodeRewardHandler(c *gin.Context) { nodeID := c.Param("node_id") @@ -326,6 +326,7 @@ func (s Server) getNodeRewardHandler(c *gin.Context) { rewards, err := CalculateMonthlyReward(node.Resources, upTimePercentage) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return } c.JSON(http.StatusOK, rewards) } diff --git a/node-registrar/pkg/server/routes.go b/node-registrar/pkg/server/routes.go index 134d916..4dea896 100644 --- a/node-registrar/pkg/server/routes.go +++ b/node-registrar/pkg/server/routes.go @@ -41,7 +41,7 @@ func (s *Server) registerRoutes(r *gin.RouterGroup) { publicNodeRoutes := r.Group("nodes") publicNodeRoutes.GET("", s.listNodesHandler) publicNodeRoutes.GET("/:node_id", s.getNodeHandler) - publicNodeRoutes.GET("/:node_id/reward", s.getNodeRewardHandler) + publicNodeRoutes.GET("/:node_id/rewards", s.getNodeRewardHandler) // protected by node key protectedNodeRoutes := r.Group("nodes", s.AuthMiddleware()) protectedNodeRoutes.POST("", s.registerNodeHandler) From eb4aecab6e21d5c3889ecba3944fdfdded267606 Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Sun, 22 Jun 2025 14:26:31 +0300 Subject: [PATCH 10/43] feat: truncate reward values to 3 decimal places --- node-registrar/pkg/server/rewards.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/node-registrar/pkg/server/rewards.go b/node-registrar/pkg/server/rewards.go index d6b0f06..be0ece1 100644 --- a/node-registrar/pkg/server/rewards.go +++ b/node-registrar/pkg/server/rewards.go @@ -80,10 +80,10 @@ func CalculateMonthlyReward(capacity db.Resources, upTimePercentage float64) (Re total := (bytesToGB(capacity.MRU)*MEMORY_REWARD_PER_GB + bytesToTB(capacity.SRU)*SSD_REWARD_PER_TB + bytesToTB(capacity.HRU)*HDD_REWARD_PER_TB) * (upTimePercentage / 100) return Reward{ - FarmerReward: total * FARMER_REWARD_PERCENTAGE, - TFReward: total * TF_REWARD_PERCENTAGE, - FPReward: total * FP_REWARD_PERCENTAGE, - Total: total, + FarmerReward: truncateFloat(total*FARMER_REWARD_PERCENTAGE, 3), + TFReward: truncateFloat(total*TF_REWARD_PERCENTAGE, 3), + FPReward: truncateFloat(total*FP_REWARD_PERCENTAGE, 3), + Total: truncateFloat(total, 3), UpTimePercentage: upTimePercentage, }, nil } @@ -138,8 +138,6 @@ func calculateUpTimePercentage(reports []db.UptimeReport, periodStart, now time. curr.Duration = time.Duration(curr.Duration * time.Second) next.Duration = time.Duration(next.Duration * time.Second) - //TODO should we check the order of timestamp? - expected := next.Timestamp.Sub(curr.Timestamp).Truncate(time.Second) actual := next.Duration.Truncate(time.Second) if curr.Duration > next.Duration || actual < expected { From b39f7f7ce031d9aaab779f1ce87b80e66d77fe36 Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Mon, 23 Jun 2025 12:03:13 +0300 Subject: [PATCH 11/43] feat: add uptime percentage to reward calculation in tests --- node-registrar/pkg/server/rewards_test.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/node-registrar/pkg/server/rewards_test.go b/node-registrar/pkg/server/rewards_test.go index 1b6b8f4..68b0eba 100644 --- a/node-registrar/pkg/server/rewards_test.go +++ b/node-registrar/pkg/server/rewards_test.go @@ -119,7 +119,9 @@ func AssertMonthlyReward(t testing.TB, resources db.Resources, upTimePercentage t.Helper() if upTimePercentage < 90 { - assert.Equal(t, Reward{}, got) + assert.Equal(t, Reward{ + UpTimePercentage: upTimePercentage, + }, got) return } @@ -135,10 +137,11 @@ func AssertMonthlyReward(t testing.TB, resources db.Resources, upTimePercentage total = total * (upTimePercentage / 100) expected := Reward{ - FarmerReward: total * FARMER_REWARD_PERCENTAGE, - TFReward: total * TF_REWARD_PERCENTAGE, - FPReward: total * FP_REWARD_PERCENTAGE, - Total: total, + FarmerReward: total * FARMER_REWARD_PERCENTAGE, + TFReward: total * TF_REWARD_PERCENTAGE, + FPReward: total * FP_REWARD_PERCENTAGE, + Total: total, + UpTimePercentage: upTimePercentage, } // Use precise floating point comparison @@ -147,6 +150,7 @@ func AssertMonthlyReward(t testing.TB, resources db.Resources, upTimePercentage assert.InDelta(t, expected.TFReward, got.TFReward, delta) assert.InDelta(t, expected.FPReward, got.FPReward, delta) assert.InDelta(t, expected.Total, got.Total, delta) + assert.InDelta(t, expected.UpTimePercentage, got.UpTimePercentage, delta) } // TestCalculateCurrentPeriodStart tests the calculateCurrentPeriodStart function with different inputs From 41c2c6491ec53df6690e49f5fa3a90866022cde8 Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Mon, 23 Jun 2025 12:49:23 +0300 Subject: [PATCH 12/43] refactor: rename constants and functions to follow Go naming conventions --- node-registrar/pkg/server/handlers.go | 12 ++-- node-registrar/pkg/server/rewards.go | 86 ++++++++++++----------- node-registrar/pkg/server/rewards_test.go | 86 +++++++++++++++-------- 3 files changed, 110 insertions(+), 74 deletions(-) diff --git a/node-registrar/pkg/server/handlers.go b/node-registrar/pkg/server/handlers.go index e524d0f..08e1b95 100644 --- a/node-registrar/pkg/server/handlers.go +++ b/node-registrar/pkg/server/handlers.go @@ -98,7 +98,7 @@ func (s Server) getFarmHandler(c *gin.Context) { // @Produce json // @Param X-Auth header string true "Authentication format: Base64(:):Base64(signature)" // @Param farm body db.Farm true "Farm creation data" -// @Success 201 {object} map[string]uint64 "'farm_id': farmID"] +// @Success 201 {object} map[string]uint64 "'farm_id': farmID" // @Failure 400 {object} map[string]any "Invalid request" // @Failure 401 {object} map[string]any "Unauthorized" // @Failure 409 {object} map[string]any "Farm already exists" @@ -312,16 +312,20 @@ func (s Server) getNodeRewardHandler(c *gin.Context) { } now := time.Now() - currentPeriodStart := calculateCurrentPeriodStart(now) + periodStart := calculatePeriodStart(now) - reports, err := s.db.GetUptimeReports(id, currentPeriodStart, now) + reports, err := s.db.GetUptimeReports(id, periodStart, now) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - upTimePercentage := calculateUpTimePercentage(reports, currentPeriodStart, now) + upTimePercentage, err := calculateUpTimePercentage(reports, periodStart, now) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } rewards, err := CalculateMonthlyReward(node.Resources, upTimePercentage) if err != nil { diff --git a/node-registrar/pkg/server/rewards.go b/node-registrar/pkg/server/rewards.go index be0ece1..150088d 100644 --- a/node-registrar/pkg/server/rewards.go +++ b/node-registrar/pkg/server/rewards.go @@ -9,37 +9,38 @@ import ( ) const ( + MemoryRewardPerGB float64 = 8.0 // Certified capacity rewards factor Per GB + SsdRewardPerTB float64 = 31.5 // Certified capacity rewards factor Per TB + HddRewardPerTB float64 = 7.0 // Certified capacity rewards factor Per TB - // Certified capacity rewards factor - MEMORY_REWARD_PER_GB = 8.0 // INCA per GB - SSD_REWARD_PER_TB = 31.5 // INCA per TB - HDD_REWARD_PER_TB = 7.0 // INCA per TB - - // Reward distribution - FARMER_REWARD_PERCENTAGE = 0.6 // 60% of the reward goes to the node owner - TF_REWARD_PERCENTAGE = 0.2 // 20% of the reward goes to the Threefold Foundation - FP_REWARD_PERCENTAGE = 0.2 // 20% of the reward goes to the Farming Pool + FarmerRewardPercentage float64 = 0.6 // FarmerRewardPercentage is the percentage of the reward that goes to the node owner (60%) + TfRewardPercentage float64 = 0.2 // TfRewardPercentage is the percentage of the reward that goes to the Threefold (20%) + FpRewardPercentage float64 = 0.2 // FpRewardPercentage is the percentage of the reward that goes to the Farming Pool (20%) +) +// Time related constants +const ( // TODO update the start timestamp - FIRST_PERIOD_START_TIMESTAMP int64 = 1522501000 + FirstPeriodStartTimestamp int64 = 1522501000 - // uptime events are supposed to happen every 40 minutes. - // here we set this to one hour (3600 sec) to allow some room. - UPTIME_EVENTS_INTERVAL = 3600 + // Uptime events are supposed to happen every 40 minutes. + // Here we set this to one hour (3600 sec) to allow some room. + UptimeEventsInterval = 3600 // The duration of a standard period, as used by the minting payouts, in seconds. - STANDARD_PERIOD_DURATION int64 = 24 * 60 * 60 * (365*3 + 366*2) / 60 + // Calculated as: 24 hours * 60 minutes * 60 seconds * (365*3 + 366*2) / 60 periods + StandardPeriodDuration int64 = 24 * 60 * 60 * (365*3 + 366*2) / 60 ) // Error messages var ErrInvalidUptimePercentage = errors.New("invalid uptime percentage") type Reward struct { - FarmerReward float64 - TFReward float64 - FPReward float64 - Total float64 - UpTimePercentage float64 + FarmerReward float64 //FarmerReward: the reward for the node owner + TfReward float64 //TfReward: the reward for the Threefold Foundation + FpReward float64 //FpReward: the reward for the Farming Pool + Total float64 //Total: the total reward + UpTimePercentage float64 //UpTimePercentage: the uptime percentage of the node } // CalculateMonthlyReward calculates the monthly reward in INCA for a given node capacity. @@ -56,11 +57,7 @@ type Reward struct { // - Threefold Foundation: 20% of the reward // - Farming Pool: 20% of the reward // -// CalculateMonthlyReward returns the following values: -// - FarmerReward: the reward for the node owner -// - TFReward: the reward for the Threefold Foundation -// - FPReward: the reward for the Farming Pool -// - Total: the total reward +// CalculateMonthlyReward returns @Reward struct // // CalculateMonthlyReward takes the following parameters: // @@ -77,12 +74,12 @@ func CalculateMonthlyReward(capacity db.Resources, upTimePercentage float64) (Re return Reward{UpTimePercentage: upTimePercentage}, nil } - total := (bytesToGB(capacity.MRU)*MEMORY_REWARD_PER_GB + bytesToTB(capacity.SRU)*SSD_REWARD_PER_TB + bytesToTB(capacity.HRU)*HDD_REWARD_PER_TB) * (upTimePercentage / 100) + total := (bytesToGB(capacity.MRU)*MemoryRewardPerGB + bytesToTB(capacity.SRU)*SsdRewardPerTB + bytesToTB(capacity.HRU)*HddRewardPerTB) * (upTimePercentage / 100) return Reward{ - FarmerReward: truncateFloat(total*FARMER_REWARD_PERCENTAGE, 3), - TFReward: truncateFloat(total*TF_REWARD_PERCENTAGE, 3), - FPReward: truncateFloat(total*FP_REWARD_PERCENTAGE, 3), + FarmerReward: truncateFloat(total*FarmerRewardPercentage, 3), + TfReward: truncateFloat(total*TfRewardPercentage, 3), + FpReward: truncateFloat(total*FpRewardPercentage, 3), Total: truncateFloat(total, 3), UpTimePercentage: upTimePercentage, }, nil @@ -115,10 +112,16 @@ func bytesToTB(bytes uint64) float64 { // // Returns: // - a float64 representing the uptime percentage -func calculateUpTimePercentage(reports []db.UptimeReport, periodStart, now time.Time) float64 { +func calculateUpTimePercentage(reports []db.UptimeReport, periodStart, now time.Time) (float64, error) { if len(reports) == 0 { - return 0.0 + return 0.0, nil + } + + for i := 0; i < len(reports)-1; i++ { + if reports[i].Timestamp.After(reports[i+1].Timestamp) { + return 0.0, errors.New("timestamps are not ordered correctly") + } } //append starter point @@ -144,25 +147,26 @@ func calculateUpTimePercentage(reports []db.UptimeReport, periodStart, now time. downtime += expected - actual } } - // if there is a gap equals or larger than th @UPTIME_EVENTS_INTERVAL between the last report and now, add it to the downtime + // if there is a gap equal + // s or larger than th @UPTIME_EVENTS_INTERVAL between the last report and now, add it to the downtime elapsedSinceLast := now.Sub(reports[len(reports)-1].Timestamp).Truncate(time.Second) - if elapsedSinceLast.Seconds() >= UPTIME_EVENTS_INTERVAL { + if elapsedSinceLast.Seconds() >= UptimeEventsInterval { downtime += elapsedSinceLast } - return truncateFloat(float64(now.Sub(periodStart)-downtime)/float64(now.Sub(periodStart))*100, 2) + return truncateFloat(float64(now.Sub(periodStart)-downtime)/float64(now.Sub(periodStart))*100, 2), nil } -// calculateCurrentPeriodStart returns the start of the current period. +// calculatePeriodStart returns the start of the period that contains the reference time. // -// The function uses the unix timestamp of the first period start (FIRST_PERIOD_START_TIMESTAMP) and the standard period duration (STANDARD_PERIOD_DURATION) to calculate the start of the current period. +// The function uses the unix timestamp of the first period start (FirstPeriodStartTimestamp) and the standard period duration (StandardPeriodDuration) to calculate the start of the period. // // Parameter: -// - now: the reference time used to calculate the current period start -func calculateCurrentPeriodStart(now time.Time) time.Time { - secondsSinceFirstPeriod := now.Unix() - FIRST_PERIOD_START_TIMESTAMP - periodOffset := secondsSinceFirstPeriod % STANDARD_PERIOD_DURATION - currentPeriodStart := now.Unix() - periodOffset - return time.Unix(currentPeriodStart, 0) +// - referenceTime: the reference time used to calculate its period start time +func calculatePeriodStart(referenceTime time.Time) time.Time { + secondsSinceFirstPeriod := referenceTime.Unix() - FirstPeriodStartTimestamp + periodOffset := secondsSinceFirstPeriod % StandardPeriodDuration + periodStart := referenceTime.Unix() - periodOffset + return time.Unix(periodStart, 0) } func truncateFloat(num float64, precision int) float64 { diff --git a/node-registrar/pkg/server/rewards_test.go b/node-registrar/pkg/server/rewards_test.go index 68b0eba..52fd73c 100644 --- a/node-registrar/pkg/server/rewards_test.go +++ b/node-registrar/pkg/server/rewards_test.go @@ -126,9 +126,9 @@ func AssertMonthlyReward(t testing.TB, resources db.Resources, upTimePercentage } // Calculate rewards - memoryReward := bytesToGB(resources.MRU) * MEMORY_REWARD_PER_GB - ssdReward := bytesToTB(resources.SRU) * SSD_REWARD_PER_TB - hddReward := bytesToTB(resources.HRU) * HDD_REWARD_PER_TB + memoryReward := bytesToGB(resources.MRU) * MemoryRewardPerGB + ssdReward := bytesToTB(resources.SRU) * SsdRewardPerTB + hddReward := bytesToTB(resources.HRU) * HddRewardPerTB // Calculate total rewards total := memoryReward + ssdReward + hddReward @@ -137,9 +137,9 @@ func AssertMonthlyReward(t testing.TB, resources db.Resources, upTimePercentage total = total * (upTimePercentage / 100) expected := Reward{ - FarmerReward: total * FARMER_REWARD_PERCENTAGE, - TFReward: total * TF_REWARD_PERCENTAGE, - FPReward: total * FP_REWARD_PERCENTAGE, + FarmerReward: total * FarmerRewardPercentage, + TfReward: total * TfRewardPercentage, + FpReward: total * FpRewardPercentage, Total: total, UpTimePercentage: upTimePercentage, } @@ -147,14 +147,14 @@ func AssertMonthlyReward(t testing.TB, resources db.Resources, upTimePercentage // Use precise floating point comparison const delta = 1e-9 // Very small acceptable difference assert.InDelta(t, expected.FarmerReward, got.FarmerReward, delta) - assert.InDelta(t, expected.TFReward, got.TFReward, delta) - assert.InDelta(t, expected.FPReward, got.FPReward, delta) + assert.InDelta(t, expected.TfReward, got.TfReward, delta) + assert.InDelta(t, expected.FpReward, got.FpReward, delta) assert.InDelta(t, expected.Total, got.Total, delta) assert.InDelta(t, expected.UpTimePercentage, got.UpTimePercentage, delta) } -// TestCalculateCurrentPeriodStart tests the calculateCurrentPeriodStart function with different inputs -func TestCalculateCurrentPeriodStart(t *testing.T) { +// TestCalculatePeriodStart tests the calculatePeriodStart function with different inputs +func TestCalculatePeriodStart(t *testing.T) { tests := []struct { name string inputTime time.Time @@ -162,42 +162,42 @@ func TestCalculateCurrentPeriodStart(t *testing.T) { }{ { name: "First period start timestamp", - inputTime: time.Unix(FIRST_PERIOD_START_TIMESTAMP, 0), - expectedTime: time.Unix(FIRST_PERIOD_START_TIMESTAMP, 0), + inputTime: time.Unix(FirstPeriodStartTimestamp, 0), + expectedTime: time.Unix(FirstPeriodStartTimestamp, 0), }, { name: "Exactly at second period start", - inputTime: time.Unix(FIRST_PERIOD_START_TIMESTAMP+STANDARD_PERIOD_DURATION, 0), - expectedTime: time.Unix(FIRST_PERIOD_START_TIMESTAMP+STANDARD_PERIOD_DURATION, 0), + inputTime: time.Unix(FirstPeriodStartTimestamp+StandardPeriodDuration, 0), + expectedTime: time.Unix(FirstPeriodStartTimestamp+StandardPeriodDuration, 0), }, { name: "Middle of a period", - inputTime: time.Unix(FIRST_PERIOD_START_TIMESTAMP+STANDARD_PERIOD_DURATION/2, 0), - expectedTime: time.Unix(FIRST_PERIOD_START_TIMESTAMP, 0), + inputTime: time.Unix(FirstPeriodStartTimestamp+StandardPeriodDuration/2, 0), + expectedTime: time.Unix(FirstPeriodStartTimestamp, 0), }, { name: "Near end of a period", - inputTime: time.Unix(FIRST_PERIOD_START_TIMESTAMP+STANDARD_PERIOD_DURATION-1, 0), - expectedTime: time.Unix(FIRST_PERIOD_START_TIMESTAMP, 0), + inputTime: time.Unix(FirstPeriodStartTimestamp+StandardPeriodDuration-1, 0), + expectedTime: time.Unix(FirstPeriodStartTimestamp, 0), }, { name: "Multiple periods later", - inputTime: time.Unix(FIRST_PERIOD_START_TIMESTAMP+3*STANDARD_PERIOD_DURATION+STANDARD_PERIOD_DURATION/3, 0), - expectedTime: time.Unix(FIRST_PERIOD_START_TIMESTAMP+3*STANDARD_PERIOD_DURATION, 0), + inputTime: time.Unix(FirstPeriodStartTimestamp+3*StandardPeriodDuration+StandardPeriodDuration/3, 0), + expectedTime: time.Unix(FirstPeriodStartTimestamp+3*StandardPeriodDuration, 0), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Call the actual function with our test time - result := calculateCurrentPeriodStart(tt.inputTime) + result := calculatePeriodStart(tt.inputTime) // Check that the result equals our expected time assert.Equal(t, tt.expectedTime.Unix(), result.Unix()) // Additionally, verify that we can manually calculate the same result - // secondsSinceCurrentPeriodStart := (tt.inputTime.Unix() - FIRST_PERIOD_START_TIMESTAMP) % STANDARD_PERIOD_DURATION - // manualCalculation := time.Unix(FIRST_PERIOD_START_TIMESTAMP+secondsSinceCurrentPeriodStart, 0) + // secondsSinceCurrentPeriodStart := (tt.inputTime.Unix() - FirstPeriodStartTimestamp) % StandardPeriodDuration + // manualCalculation := time.Unix(FirstPeriodStartTimestamp+secondsSinceCurrentPeriodStart, 0) // assert.Equal(t, manualCalculation.Unix(), result.Unix()) }) } @@ -210,9 +210,10 @@ func TestCalculateUpTimePercentage(t *testing.T) { } now := time.Now().Truncate(time.Second) tests := []struct { - name string - args args - expected float64 + name string + args args + expected float64 + wantError bool }{ { name: "All uptime, no downtime (40 min gaps)", @@ -283,7 +284,7 @@ func TestCalculateUpTimePercentage(t *testing.T) { reports: []db.UptimeReport{ {Timestamp: now.Add(-80 * time.Minute), Duration: 2400}, // 40 min (2400 seconds) {Timestamp: now.Add(-40 * time.Minute), Duration: 1800}, // 30 min (1800 seconds) - {Timestamp: now, Duration: 1200}, // 20 min (1200 seconds) + {Timestamp: now, Duration: 1200}, // 20 min (1200 seconds) }, }, expected: 75.0, @@ -295,16 +296,43 @@ func TestCalculateUpTimePercentage(t *testing.T) { reports: []db.UptimeReport{ {Timestamp: now.Add(-80 * time.Minute), Duration: 1200}, // 20 min (1200 seconds) {Timestamp: now.Add(-40 * time.Minute), Duration: 1800}, // 30 min (1800 seconds) - {Timestamp: now, Duration: 2400}, // 40 min (2400 seconds) + {Timestamp: now, Duration: 2400}, // 40 min (2400 seconds) }, }, expected: 75.0, }, + { + name: "Unordered reports - timestamps not in ascending order", + args: args{ + periodStart: now.Add(-120 * time.Minute), + reports: []db.UptimeReport{ + {Timestamp: now.Add(-40 * time.Minute), Duration: 2400}, // Out of order (should be before the one below) + {Timestamp: now.Add(-80 * time.Minute), Duration: 2400}, + {Timestamp: now, Duration: 2400}, + }, + }, + expected: 0.0, + wantError: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := calculateUpTimePercentage(tt.args.reports, tt.args.periodStart, now) + got, err := calculateUpTimePercentage(tt.args.reports, tt.args.periodStart, now) + + // Check for expected error + if tt.wantError { + if err == nil { + t.Errorf("calculateUpTimePercentage() expected error, got nil") + } + return + } + + // No error expected + if err != nil { + t.Errorf("calculateUpTimePercentage() unexpected error: %v", err) + return + } if math.Abs(got-tt.expected) > 0.01 { t.Errorf("calculateUpTimePercentage() = %v, want %v", got, tt.expected) } From 6c5993cc903b61d4e907d17327c0d476ae13ae4e Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Mon, 23 Jun 2025 15:21:50 +0300 Subject: [PATCH 13/43] Fix tests: pass duration as time.duration --- node-registrar/pkg/server/rewards_test.go | 36 +++++++++++------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/node-registrar/pkg/server/rewards_test.go b/node-registrar/pkg/server/rewards_test.go index 52fd73c..e43031f 100644 --- a/node-registrar/pkg/server/rewards_test.go +++ b/node-registrar/pkg/server/rewards_test.go @@ -220,10 +220,10 @@ func TestCalculateUpTimePercentage(t *testing.T) { args: args{ periodStart: now.Add(-160 * time.Minute), // Start 160 min ago (for 4 reports) reports: []db.UptimeReport{ - {Timestamp: now.Add(-120 * time.Minute), Duration: 2400}, // 40 min (2400 seconds) - {Timestamp: now.Add(-80 * time.Minute), Duration: 2400}, // 40 min (2400 seconds) - {Timestamp: now.Add(-40 * time.Minute), Duration: 2400}, // 40 min (2400 seconds) - {Timestamp: now, Duration: 2400}, // 40 min (2400 seconds) + {Timestamp: now.Add(-120 * time.Minute), Duration: 40 * time.Minute}, // 40 min + {Timestamp: now.Add(-80 * time.Minute), Duration: 40 * time.Minute}, // 40 min + {Timestamp: now.Add(-40 * time.Minute), Duration: 40 * time.Minute}, // 40 min + {Timestamp: now, Duration: 40 * time.Minute}, // 40 min }, }, expected: 100.0, @@ -233,8 +233,8 @@ func TestCalculateUpTimePercentage(t *testing.T) { args: args{ periodStart: now.Add(-160 * time.Minute), // full 160 mins = 9600s reports: []db.UptimeReport{ - {Timestamp: now.Add(-120 * time.Minute), Duration: 2400}, // 40 min (2400 seconds) - {Timestamp: now.Add(-80 * time.Minute), Duration: 2400}, // 40 min (2400 seconds) + {Timestamp: now.Add(-120 * time.Minute), Duration: 40 * time.Minute}, // 40 min + {Timestamp: now.Add(-80 * time.Minute), Duration: 40 * time.Minute}, // 40 min }, }, expected: 50.0, @@ -252,7 +252,7 @@ func TestCalculateUpTimePercentage(t *testing.T) { args: args{ periodStart: now.Add(-60 * time.Minute), // full 60 mins reports: []db.UptimeReport{ - {Timestamp: now.Add(-40 * time.Minute), Duration: 2400}, // 40 min (2400 seconds) + {Timestamp: now.Add(-40 * time.Minute), Duration: 40 * time.Minute}, // 40 min }, }, expected: 100.0, @@ -262,7 +262,7 @@ func TestCalculateUpTimePercentage(t *testing.T) { args: args{ periodStart: now.Add(-130 * time.Minute), // full 130 mins = 7800s reports: []db.UptimeReport{ - {Timestamp: now.Add(-10 * time.Minute), Duration: 7200}, // 120 min (7200 seconds) + {Timestamp: now.Add(-10 * time.Minute), Duration: 120 * time.Minute}, // 120 min }, }, expected: 100.0, @@ -272,7 +272,7 @@ func TestCalculateUpTimePercentage(t *testing.T) { args: args{ periodStart: now.Add(-40 * time.Minute), reports: []db.UptimeReport{ - {Timestamp: now, Duration: 1200}, // 20 min (1200 seconds) + {Timestamp: now, Duration: 20 * time.Minute}, // 20 min }, }, expected: 50.0, @@ -282,9 +282,9 @@ func TestCalculateUpTimePercentage(t *testing.T) { args: args{ periodStart: now.Add(-120 * time.Minute), reports: []db.UptimeReport{ - {Timestamp: now.Add(-80 * time.Minute), Duration: 2400}, // 40 min (2400 seconds) - {Timestamp: now.Add(-40 * time.Minute), Duration: 1800}, // 30 min (1800 seconds) - {Timestamp: now, Duration: 1200}, // 20 min (1200 seconds) + {Timestamp: now.Add(-80 * time.Minute), Duration: 40 * time.Minute}, // 40 min + {Timestamp: now.Add(-40 * time.Minute), Duration: 30 * time.Minute}, // 30 min + {Timestamp: now, Duration: 20 * time.Minute}, // 20 min }, }, expected: 75.0, @@ -294,9 +294,9 @@ func TestCalculateUpTimePercentage(t *testing.T) { args: args{ periodStart: now.Add(-120 * time.Minute), reports: []db.UptimeReport{ - {Timestamp: now.Add(-80 * time.Minute), Duration: 1200}, // 20 min (1200 seconds) - {Timestamp: now.Add(-40 * time.Minute), Duration: 1800}, // 30 min (1800 seconds) - {Timestamp: now, Duration: 2400}, // 40 min (2400 seconds) + {Timestamp: now.Add(-80 * time.Minute), Duration: 20 * time.Minute}, // 20 min + {Timestamp: now.Add(-40 * time.Minute), Duration: 30 * time.Minute}, // 30 min + {Timestamp: now, Duration: 40 * time.Minute}, // 40 min }, }, expected: 75.0, @@ -306,9 +306,9 @@ func TestCalculateUpTimePercentage(t *testing.T) { args: args{ periodStart: now.Add(-120 * time.Minute), reports: []db.UptimeReport{ - {Timestamp: now.Add(-40 * time.Minute), Duration: 2400}, // Out of order (should be before the one below) - {Timestamp: now.Add(-80 * time.Minute), Duration: 2400}, - {Timestamp: now, Duration: 2400}, + {Timestamp: now.Add(-40 * time.Minute), Duration: 40 * time.Minute}, // Out of order (should be before the one below) + {Timestamp: now.Add(-80 * time.Minute), Duration: 40 * time.Minute}, + {Timestamp: now, Duration: 40 * time.Minute}, }, }, expected: 0.0, From d80cbe0469a72039e88098ce2ba2f87de7530b7a Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Mon, 23 Jun 2025 17:33:49 +0300 Subject: [PATCH 14/43] refactor: rename and split reward calculation functions for better maintainability --- node-registrar/pkg/server/handlers.go | 2 +- node-registrar/pkg/server/rewards.go | 160 +++++++++++++--------- node-registrar/pkg/server/rewards_test.go | 19 +-- 3 files changed, 108 insertions(+), 73 deletions(-) diff --git a/node-registrar/pkg/server/handlers.go b/node-registrar/pkg/server/handlers.go index 08e1b95..39fe13c 100644 --- a/node-registrar/pkg/server/handlers.go +++ b/node-registrar/pkg/server/handlers.go @@ -327,7 +327,7 @@ func (s Server) getNodeRewardHandler(c *gin.Context) { return } - rewards, err := CalculateMonthlyReward(node.Resources, upTimePercentage) + rewards, err := CalculateCapacityReward(node.Resources, upTimePercentage) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return diff --git a/node-registrar/pkg/server/rewards.go b/node-registrar/pkg/server/rewards.go index 150088d..a4b6c33 100644 --- a/node-registrar/pkg/server/rewards.go +++ b/node-registrar/pkg/server/rewards.go @@ -16,6 +16,12 @@ const ( FarmerRewardPercentage float64 = 0.6 // FarmerRewardPercentage is the percentage of the reward that goes to the node owner (60%) TfRewardPercentage float64 = 0.2 // TfRewardPercentage is the percentage of the reward that goes to the Threefold (20%) FpRewardPercentage float64 = 0.2 // FpRewardPercentage is the percentage of the reward that goes to the Farming Pool (20%) + + // defines the number of decimal places to keep in reward calculations + RewardPrecisionDecimalPlaces int = 3 + + // MinUptimePercentageForReward is the minimum uptime percentage required for a node to receive rewards + MinUptimePercentageForReward float64 = 90.0 ) // Time related constants @@ -43,7 +49,7 @@ type Reward struct { UpTimePercentage float64 //UpTimePercentage: the uptime percentage of the node } -// CalculateMonthlyReward calculates the monthly reward in INCA for a given node capacity. +// CalculateCapacityReward calculates the reward in INCA for a given node capacity. // // The rewards are calculated as follows: // @@ -57,42 +63,116 @@ type Reward struct { // - Threefold Foundation: 20% of the reward // - Farming Pool: 20% of the reward // -// CalculateMonthlyReward returns @Reward struct -// -// CalculateMonthlyReward takes the following parameters: +// Parameters: // // - capacity: the node capacity, in form of a db.Resources Type // - upTimePercentage: the uptime percentage of the node, as a float64 value between 0 and 100 // -// - Note: if the uptime percentage is less than 90, the node will not reserve any rewards. +// Returns: a Reward struct containing the distributed rewards and uptime percentage +// +// - Note: if the uptime percentage is less than MinUptimePercentageForReward, the node will not receive any rewards. -func CalculateMonthlyReward(capacity db.Resources, upTimePercentage float64) (Reward, error) { +func CalculateCapacityReward(capacity db.Resources, upTimePercentage float64) (Reward, error) { if upTimePercentage < 0 || upTimePercentage > 100 { return Reward{}, ErrInvalidUptimePercentage } - if upTimePercentage < 90 { + if upTimePercentage < MinUptimePercentageForReward { return Reward{UpTimePercentage: upTimePercentage}, nil } total := (bytesToGB(capacity.MRU)*MemoryRewardPerGB + bytesToTB(capacity.SRU)*SsdRewardPerTB + bytesToTB(capacity.HRU)*HddRewardPerTB) * (upTimePercentage / 100) return Reward{ - FarmerReward: truncateFloat(total*FarmerRewardPercentage, 3), - TfReward: truncateFloat(total*TfRewardPercentage, 3), - FpReward: truncateFloat(total*FpRewardPercentage, 3), - Total: truncateFloat(total, 3), + FarmerReward: truncateFloat(total*FarmerRewardPercentage, RewardPrecisionDecimalPlaces), + TfReward: truncateFloat(total*TfRewardPercentage, RewardPrecisionDecimalPlaces), + FpReward: truncateFloat(total*FpRewardPercentage, RewardPrecisionDecimalPlaces), + Total: truncateFloat(total, RewardPrecisionDecimalPlaces), UpTimePercentage: upTimePercentage, }, nil } +// bytesToGB converts bytes to gigabytes. func bytesToGB(bytes uint64) float64 { return float64(bytes) / math.Pow(1024, 3) } +// bytesToTB converts bytes to terabytes. func bytesToTB(bytes uint64) float64 { return float64(bytes) / math.Pow(1024, 4) } +// calculatePeriodStart returns the start of the period that contains the reference time. +// +// The function uses the unix timestamp of the first period start (FirstPeriodStartTimestamp) and the standard period duration (StandardPeriodDuration) to calculate the start of the period. +func calculatePeriodStart(referenceTime time.Time) time.Time { + secondsSinceFirstPeriod := referenceTime.Unix() - FirstPeriodStartTimestamp + periodOffset := secondsSinceFirstPeriod % StandardPeriodDuration + periodStart := referenceTime.Unix() - periodOffset + return time.Unix(periodStart, 0) +} + +// truncateFloat truncates a floating point number to the specified precision. +func truncateFloat(num float64, precision int) float64 { + pow := math.Pow(10, float64(precision)) + return math.Trunc(num*pow) / pow +} + +// calculateDowntimeFromReports calculates the downtime from a sequence of uptime reports. +// +// This function iterates through the reports and calculates downtime by comparing the +// gap between consecutive timestamps with the reported duration. If the duration is less +// than the gap or if the current duration is greater than the next duration, the difference +// is counted as downtime. +func calculateDowntimeFromReports(reports []db.UptimeReport) time.Duration { + var downtime time.Duration + for i := 0; i < len(reports)-1; i++ { + curr := reports[i] + next := reports[i+1] + gapBetweenTimeStamps := next.Timestamp.Sub(curr.Timestamp) + duration := next.Duration + if curr.Duration > next.Duration || duration < gapBetweenTimeStamps { + downtime += gapBetweenTimeStamps - duration + } + } + return downtime +} + +// areReportsOrderedCorrectly verifies that uptime reports are ordered chronologically by timestamp. +func areReportsOrderedCorrectly(reports []db.UptimeReport) bool { + for i := 0; i < len(reports)-1; i++ { + if reports[i].Timestamp.After(reports[i+1].Timestamp) { + return false + } + } + return true +} + +// calculateUptimePercentage calculates the percentage of uptime based on total period duration and downtime. +func calculateUptimePercentage(totalPeriod, downtime time.Duration) float64 { + // Calculate actual uptime by subtracting downtime from total period + actualUptime := totalPeriod - downtime + + // Calculate the percentage (actual uptime / total period * 100) + uptimeRatio := float64(actualUptime) / float64(totalPeriod) + uptimePercentage := uptimeRatio * 100 + + // Truncate to 2 decimal places + return truncateFloat(uptimePercentage, 2) +} + +// downtimeSinceLastReportTimestamp calculates the downtime since the last uptime report timestamp. +// +// This function determines if there has been a significant gap (exceeding UptimeEventsInterval) +// since the last report. If such a gap exists, it's considered downtime. +func downtimeSinceLastReportTimestamp(lastReportTimestamp time.Time, currentTime time.Time) time.Duration { + elapsedSinceLast := currentTime.Sub(lastReportTimestamp).Truncate(time.Second) + + if elapsedSinceLast.Seconds() >= UptimeEventsInterval { + return elapsedSinceLast + } + return 0 +} + // calculateUpTimePercentage calculates the uptime percentage for a given node within a specific period. // // This function takes a slice of UptimeReport, a period start time and a current time as parameters. @@ -102,26 +182,14 @@ func bytesToTB(bytes uint64) float64 { // Additionally, if there is a gap equals or larger than the @UPTIME_EVENTS_INTERVAL between the last report and now, add it to the downtime. // The uptime percentage is then calculated by subtracting the total downtime from the total elapsed time since the period start and dividing the result by the total elapsed time. // The result is then multiplied by 100 to get the percentage. -// -// Note: This function assumes that the reports are ordered by timestamp in ascending order. -// -// Parameters: -// - reports: a slice of UptimeReport -// - periodStart: the start of the period -// - now: the current time -// -// Returns: -// - a float64 representing the uptime percentage func calculateUpTimePercentage(reports []db.UptimeReport, periodStart, now time.Time) (float64, error) { if len(reports) == 0 { return 0.0, nil } - for i := 0; i < len(reports)-1; i++ { - if reports[i].Timestamp.After(reports[i+1].Timestamp) { - return 0.0, errors.New("timestamps are not ordered correctly") - } + if !areReportsOrderedCorrectly(reports) { + return 0.0, errors.New("timestamps are not ordered correctly") } //append starter point @@ -133,43 +201,9 @@ func calculateUpTimePercentage(reports []db.UptimeReport, periodStart, now time. }, reports...) var downtime time.Duration = 0 - for i := 0; i < len(reports)-1; i++ { - - curr := reports[i] - next := reports[i+1] - - curr.Duration = time.Duration(curr.Duration * time.Second) - next.Duration = time.Duration(next.Duration * time.Second) - - expected := next.Timestamp.Sub(curr.Timestamp).Truncate(time.Second) - actual := next.Duration.Truncate(time.Second) - if curr.Duration > next.Duration || actual < expected { - downtime += expected - actual - } - } - // if there is a gap equal - // s or larger than th @UPTIME_EVENTS_INTERVAL between the last report and now, add it to the downtime - elapsedSinceLast := now.Sub(reports[len(reports)-1].Timestamp).Truncate(time.Second) - if elapsedSinceLast.Seconds() >= UptimeEventsInterval { - downtime += elapsedSinceLast - } - return truncateFloat(float64(now.Sub(periodStart)-downtime)/float64(now.Sub(periodStart))*100, 2), nil -} + downtime += calculateDowntimeFromReports(reports) + downtime += downtimeSinceLastReportTimestamp(reports[len(reports)-1].Timestamp, now) -// calculatePeriodStart returns the start of the period that contains the reference time. -// -// The function uses the unix timestamp of the first period start (FirstPeriodStartTimestamp) and the standard period duration (StandardPeriodDuration) to calculate the start of the period. -// -// Parameter: -// - referenceTime: the reference time used to calculate its period start time -func calculatePeriodStart(referenceTime time.Time) time.Time { - secondsSinceFirstPeriod := referenceTime.Unix() - FirstPeriodStartTimestamp - periodOffset := secondsSinceFirstPeriod % StandardPeriodDuration - periodStart := referenceTime.Unix() - periodOffset - return time.Unix(periodStart, 0) -} - -func truncateFloat(num float64, precision int) float64 { - pow := math.Pow(10, float64(precision)) - return math.Trunc(num*pow) / pow + totalPeriod := now.Sub(periodStart) + return calculateUptimePercentage(totalPeriod, downtime), nil } diff --git a/node-registrar/pkg/server/rewards_test.go b/node-registrar/pkg/server/rewards_test.go index e43031f..c92ece9 100644 --- a/node-registrar/pkg/server/rewards_test.go +++ b/node-registrar/pkg/server/rewards_test.go @@ -1,6 +1,7 @@ package server import ( + "fmt" "math" "testing" "time" @@ -9,7 +10,7 @@ import ( "github.com/threefoldtech/tfgrid4-sdk-go/node-registrar/pkg/db" ) -func TestCalculateMonthlyReward(t *testing.T) { +func TestCalculateCapacityReward(t *testing.T) { // Define standard capacity for most tests standardCapacity := db.Resources{ CRU: 8, @@ -66,9 +67,9 @@ func TestCalculateMonthlyReward(t *testing.T) { wantError: false, }, { - name: "uptime at threshold (90%)", + name: fmt.Sprintf("uptime at threshold (%.0f%%)", MinUptimePercentageForReward), capacity: standardCapacity, - upTimePercentage: 90, + upTimePercentage: MinUptimePercentageForReward, wantError: false, }, { @@ -93,7 +94,7 @@ func TestCalculateMonthlyReward(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := CalculateMonthlyReward(tt.capacity, tt.upTimePercentage) + got, err := CalculateCapacityReward(tt.capacity, tt.upTimePercentage) // Error check if tt.wantError { @@ -110,15 +111,15 @@ func TestCalculateMonthlyReward(t *testing.T) { } // Check reward calculation - AssertMonthlyReward(t, tt.capacity, tt.upTimePercentage, got) + AssertCapacityReward(t, tt.capacity, tt.upTimePercentage, got) }) } } -func AssertMonthlyReward(t testing.TB, resources db.Resources, upTimePercentage float64, got Reward) { +func AssertCapacityReward(t testing.TB, resources db.Resources, upTimePercentage float64, got Reward) { t.Helper() - if upTimePercentage < 90 { + if upTimePercentage < MinUptimePercentageForReward { assert.Equal(t, Reward{ UpTimePercentage: upTimePercentage, }, got) @@ -307,11 +308,11 @@ func TestCalculateUpTimePercentage(t *testing.T) { periodStart: now.Add(-120 * time.Minute), reports: []db.UptimeReport{ {Timestamp: now.Add(-40 * time.Minute), Duration: 40 * time.Minute}, // Out of order (should be before the one below) - {Timestamp: now.Add(-80 * time.Minute), Duration: 40 * time.Minute}, + {Timestamp: now.Add(-80 * time.Minute), Duration: 40 * time.Minute}, {Timestamp: now, Duration: 40 * time.Minute}, }, }, - expected: 0.0, + expected: 0.0, wantError: true, }, } From 0c6094d0be39bad7cf6d6909c24215a659ef74a0 Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Mon, 23 Jun 2025 18:03:00 +0300 Subject: [PATCH 15/43] feat: improve error handling for node uptime reports and add test cases --- node-registrar/pkg/server/handlers.go | 9 ++++++ node-registrar/pkg/server/rewards.go | 16 +++++++---- node-registrar/pkg/server/rewards_test.go | 34 +++++++++++++++++------ 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/node-registrar/pkg/server/handlers.go b/node-registrar/pkg/server/handlers.go index 39fe13c..620cdbf 100644 --- a/node-registrar/pkg/server/handlers.go +++ b/node-registrar/pkg/server/handlers.go @@ -290,6 +290,7 @@ func (s Server) getNodeHandler(c *gin.Context) { // @Success 200 {object} Reward "Rewards details with the node uptime percentage" // @Failure 400 {object} map[string]any "Invalid node ID" // @Failure 404 {object} map[string]any "Node not found" +// @Failure 422 {object} map[string]any "No uptime reports available for this node" // @Router /nodes/{node_id}/rewards [get] func (s Server) getNodeRewardHandler(c *gin.Context) { nodeID := c.Param("node_id") @@ -323,6 +324,14 @@ func (s Server) getNodeRewardHandler(c *gin.Context) { upTimePercentage, err := calculateUpTimePercentage(reports, periodStart, now) if err != nil { + if err == ErrReportsNotInAscOrder { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if err == ErrNoReportsAvailable { + c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) + return + } c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } diff --git a/node-registrar/pkg/server/rewards.go b/node-registrar/pkg/server/rewards.go index a4b6c33..3d27533 100644 --- a/node-registrar/pkg/server/rewards.go +++ b/node-registrar/pkg/server/rewards.go @@ -39,7 +39,11 @@ const ( ) // Error messages -var ErrInvalidUptimePercentage = errors.New("invalid uptime percentage") +var ( + ErrInvalidUptimePercentage = errors.New("invalid uptime percentage") + ErrReportsNotInAscOrder = errors.New("timestamps are not ordered correctly") + ErrNoReportsAvailable = errors.New("no reports available For this node") +) type Reward struct { FarmerReward float64 //FarmerReward: the reward for the node owner @@ -147,8 +151,8 @@ func areReportsOrderedCorrectly(reports []db.UptimeReport) bool { return true } -// calculateUptimePercentage calculates the percentage of uptime based on total period duration and downtime. -func calculateUptimePercentage(totalPeriod, downtime time.Duration) float64 { +// calculatePercentage calculates the percentage of uptime based on total period duration and downtime. +func calculatePercentage(totalPeriod, downtime time.Duration) float64 { // Calculate actual uptime by subtracting downtime from total period actualUptime := totalPeriod - downtime @@ -185,11 +189,11 @@ func downtimeSinceLastReportTimestamp(lastReportTimestamp time.Time, currentTime func calculateUpTimePercentage(reports []db.UptimeReport, periodStart, now time.Time) (float64, error) { if len(reports) == 0 { - return 0.0, nil + return 0.0, ErrNoReportsAvailable } if !areReportsOrderedCorrectly(reports) { - return 0.0, errors.New("timestamps are not ordered correctly") + return 0.0, ErrReportsNotInAscOrder } //append starter point @@ -205,5 +209,5 @@ func calculateUpTimePercentage(reports []db.UptimeReport, periodStart, now time. downtime += downtimeSinceLastReportTimestamp(reports[len(reports)-1].Timestamp, now) totalPeriod := now.Sub(periodStart) - return calculateUptimePercentage(totalPeriod, downtime), nil + return calculatePercentage(totalPeriod, downtime), nil } diff --git a/node-registrar/pkg/server/rewards_test.go b/node-registrar/pkg/server/rewards_test.go index c92ece9..5428dca 100644 --- a/node-registrar/pkg/server/rewards_test.go +++ b/node-registrar/pkg/server/rewards_test.go @@ -211,10 +211,11 @@ func TestCalculateUpTimePercentage(t *testing.T) { } now := time.Now().Truncate(time.Second) tests := []struct { - name string - args args - expected float64 - wantError bool + name string + args args + expected float64 + wantError bool + expectedError error }{ { name: "All uptime, no downtime (40 min gaps)", @@ -241,12 +242,14 @@ func TestCalculateUpTimePercentage(t *testing.T) { expected: 50.0, }, { - name: "0% uptime — no reports received", + name: "Empty reports — should return error", args: args{ periodStart: now.Add(-160 * time.Minute), // full 160 mins = 9600s reports: []db.UptimeReport{}, }, - expected: 0.0, + expected: 0.0, + wantError: true, + expectedError: ErrNoReportsAvailable, }, { name: "allowance for only one report received, after 1hour", @@ -312,8 +315,19 @@ func TestCalculateUpTimePercentage(t *testing.T) { {Timestamp: now, Duration: 40 * time.Minute}, }, }, - expected: 0.0, - wantError: true, + expected: 0.0, + wantError: true, + expectedError: ErrReportsNotInAscOrder, + }, + { + name: "No reports available", + args: args{ + periodStart: now.Add(-120 * time.Minute), + reports: []db.UptimeReport{}, // Empty reports + }, + expected: 0.0, + wantError: true, + expectedError: ErrNoReportsAvailable, }, } @@ -326,6 +340,10 @@ func TestCalculateUpTimePercentage(t *testing.T) { if err == nil { t.Errorf("calculateUpTimePercentage() expected error, got nil") } + // Also check for specific error type + if tt.expectedError != nil { + assert.Equal(t, tt.expectedError, err, "Expected specific error type") + } return } From 04e5a9a0808f439538a8e7cdbde5d9030047ed77 Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Mon, 23 Jun 2025 18:09:33 +0300 Subject: [PATCH 16/43] refactor: enhance loops --- node-registrar/pkg/server/rewards.go | 30 +++++----------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/node-registrar/pkg/server/rewards.go b/node-registrar/pkg/server/rewards.go index 3d27533..d60f020 100644 --- a/node-registrar/pkg/server/rewards.go +++ b/node-registrar/pkg/server/rewards.go @@ -55,27 +55,7 @@ type Reward struct { // CalculateCapacityReward calculates the reward in INCA for a given node capacity. // -// The rewards are calculated as follows: -// -// - Certified capacity rewards factor: -// - Memory: 8.0 INCA per GB -// - SSD: 31.5 INCA per TB -// - HDD: 7.0 INCA per TB -// -// - Reward distribution: -// - Farmer: 60% of the reward -// - Threefold Foundation: 20% of the reward -// - Farming Pool: 20% of the reward -// -// Parameters: -// -// - capacity: the node capacity, in form of a db.Resources Type -// - upTimePercentage: the uptime percentage of the node, as a float64 value between 0 and 100 -// -// Returns: a Reward struct containing the distributed rewards and uptime percentage -// // - Note: if the uptime percentage is less than MinUptimePercentageForReward, the node will not receive any rewards. - func CalculateCapacityReward(capacity db.Resources, upTimePercentage float64) (Reward, error) { if upTimePercentage < 0 || upTimePercentage > 100 { return Reward{}, ErrInvalidUptimePercentage @@ -124,12 +104,12 @@ func truncateFloat(num float64, precision int) float64 { // calculateDowntimeFromReports calculates the downtime from a sequence of uptime reports. // // This function iterates through the reports and calculates downtime by comparing the -// gap between consecutive timestamps with the reported duration. If the duration is less -// than the gap or if the current duration is greater than the next duration, the difference -// is counted as downtime. +// gap between consecutive timestamps with the reported duration. +// If the duration is less than the gap or if the current duration is greater than the next duration, +// the difference is counted as downtime. func calculateDowntimeFromReports(reports []db.UptimeReport) time.Duration { var downtime time.Duration - for i := 0; i < len(reports)-1; i++ { + for i := range len(reports) - 1 { curr := reports[i] next := reports[i+1] gapBetweenTimeStamps := next.Timestamp.Sub(curr.Timestamp) @@ -143,7 +123,7 @@ func calculateDowntimeFromReports(reports []db.UptimeReport) time.Duration { // areReportsOrderedCorrectly verifies that uptime reports are ordered chronologically by timestamp. func areReportsOrderedCorrectly(reports []db.UptimeReport) bool { - for i := 0; i < len(reports)-1; i++ { + for i := range len(reports) - 1 { if reports[i].Timestamp.After(reports[i+1].Timestamp) { return false } From ceff122cd4d7d83d7f09fb2491ff49dab10c5f71 Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Mon, 23 Jun 2025 18:22:13 +0300 Subject: [PATCH 17/43] refactor: extract reward calculation logic into separate functions and add test suite --- node-registrar/pkg/server/rewards.go | 18 +- node-registrar/test/main.go | 294 +++++++++++++++++++++++++++ 2 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 node-registrar/test/main.go diff --git a/node-registrar/pkg/server/rewards.go b/node-registrar/pkg/server/rewards.go index d60f020..042f037 100644 --- a/node-registrar/pkg/server/rewards.go +++ b/node-registrar/pkg/server/rewards.go @@ -53,6 +53,22 @@ type Reward struct { UpTimePercentage float64 //UpTimePercentage: the uptime percentage of the node } +// calculateBaseCapacityReward calculates the base reward from node capacity without applying uptime. +func calculateBaseCapacityReward(capacity db.Resources) float64 { + mruReward := bytesToGB(capacity.MRU) * MemoryRewardPerGB + sruReward := bytesToTB(capacity.SRU) * SsdRewardPerTB + hruReward := bytesToTB(capacity.HRU) * HddRewardPerTB + + return mruReward + sruReward + hruReward +} + +// calculateTotalReward calculates the total reward based on node capacity and uptime percentage. +// It first calculates the base capacity reward and then applies the uptime percentage. +func calculateTotalReward(capacity db.Resources, upTimePercentage float64) float64 { + baseReward := calculateBaseCapacityReward(capacity) + return baseReward * (upTimePercentage / 100) +} + // CalculateCapacityReward calculates the reward in INCA for a given node capacity. // // - Note: if the uptime percentage is less than MinUptimePercentageForReward, the node will not receive any rewards. @@ -64,7 +80,7 @@ func CalculateCapacityReward(capacity db.Resources, upTimePercentage float64) (R return Reward{UpTimePercentage: upTimePercentage}, nil } - total := (bytesToGB(capacity.MRU)*MemoryRewardPerGB + bytesToTB(capacity.SRU)*SsdRewardPerTB + bytesToTB(capacity.HRU)*HddRewardPerTB) * (upTimePercentage / 100) + total := calculateTotalReward(capacity, upTimePercentage) return Reward{ FarmerReward: truncateFloat(total*FarmerRewardPercentage, RewardPrecisionDecimalPlaces), diff --git a/node-registrar/test/main.go b/node-registrar/test/main.go new file mode 100644 index 0000000..95c606c --- /dev/null +++ b/node-registrar/test/main.go @@ -0,0 +1,294 @@ +package main + +import ( + "crypto/ed25519" + "os" + "time" + + subkeyEd25519 "github.com/vedhavyas/go-subkey/v2/ed25519" + + "github.com/pkg/errors" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/threefoldtech/tfgrid4-sdk-go/node-registrar/client" + "github.com/vedhavyas/go-subkey/v2" +) + +var ( + twinID uint64 = 4 + farmID uint64 = 1 + nodeID uint64 = 2 + farmName = "freeFarm" + net = "" +) + +const ( + localHexKey = "acoustic foot tomorrow brown candy cash reject hurt wood roof blossom sausage" + dev4HexKey = "" + qa4HexKey = "" + test4HexKey = "" + prod4hexKey = "" +) + +type network struct { + url string + key string +} + +var networks = map[string]network{ + "local": {url: "http://localhost:8080/api/v1", key: localHexKey}, + // "dev": {url: "https://registrar.dev4.grid.tf/api/v1", key: dev4HexKey}, + // "qa": {url: "https://registrar.qa4.grid.tf/api/v1", key: qa4HexKey}, + // "test": {url: "https://registrar.test4.grid.tf/api/v1", key: test4HexKey}, + // "prod": {url: "https://registrar.prod4.grid.tf/api/v1", key: prod4hexKey}, +} + +func main() { + log.Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).Level(zerolog.DebugLevel).With().Timestamp().Logger() + if _, ok := networks[net]; !ok { + net = "local" + } + + registrarURL := networks[net].url + // seedOrMnemonic := networks[net].key + + // publicKey, err := parseSeed(seedOrMnemonic) + // if err != nil { + // log.Fatal().Err(err).Send() + // } + + c, err := client.NewRegistrarClient(registrarURL, "tool mirror high clay quit cube affair dirt rely hire joy text") + if err != nil { + log.Fatal().Err(err).Msg("failed to create new registrar client") + } + + // manageAccount(&c, publicKey) + + // manageFarm(&c) + + manageNode(&c) + + // manageVersion(&c) +} + +// The flow: +// - we need to create an account +// - try to get the account by id and by pk +// - try to update the account +// - try to ensure account +func manageAccount(c *client.RegistrarClient, publicKey ed25519.PublicKey) { + relays := []string{} + rmbEncKey := "" + + log.Info().Msg("***************************************************************************") + log.Info().Msg("manage account create/update/get/ensure") + + createAccount(c, relays, rmbEncKey) + getAccountByPK(c, publicKey) + relays = []string{"relay1", "relay2"} + rmbEncKey = "key1&2" + // updateAccount(c, relays, rmbEncKey) + enusreAccount(c, relays, rmbEncKey) + getAccount(c, twinID) +} + +// The flow: +// - we need to create a farm with the created twin +// - try to get the farm +// - try to update the farm +func manageFarm(c *client.RegistrarClient) { + log.Info().Msg("***************************************************************************") + log.Info().Msg("manage farm create/update/get") + // createFarm(c, farmName) + // getFarm(c, farmID) + // updateFarm(c, "notFreeFarm101") + // getFarm(c, farmID) +} + +// The flow: +// - we need to create an account and use it to register a node in the created farm +// - try to update the node +// - try to get the node +// - try to send the node up time report +// - try to the node by twin id +func manageNode(c *client.RegistrarClient) { + log.Info().Msg("***************************************************************************") + log.Info().Msg("manage node register/update/get/list/send uptime report") + // Try with a nil IPs field to see if that helps + interface1 := client.Interface{ + Name: "zos", + Mac: "mac", + IPs: []string{"1.1.1.1"}, + + // Not explicitly setting IPs field at all + } + + registerNode(c, farmID, twinID, []client.Interface{interface1}, client.Location{City: "somewhere"}, client.Resources{CRU: 8, SRU: 5497558138880, + MRU: 34359738368, + HRU: 17592186044416}, "serialNumber", false, false) + node := getNode(c, nodeID) + // updateNode(c, client.Location{City: "somewhere"}) + sendUptimeReport(c, client.UptimeReport{Uptime: 40 * 60, Timestamp: time.Now().Unix()}) + getNodeWithTwinID(c, node.TwinID) +} + +func manageVersion(c *client.RegistrarClient) { + log.Info().Msg("***************************************************************************") + log.Info().Msg("manage version set/get") + err := c.SetZosVersion("v0.1.8", true) + if err != nil { + log.Fatal().Err(err).Msg("failed to set registrar version") + } + + version, err := c.GetZosVersion() + if err != nil { + log.Fatal().Err(err).Msg("failed to set registrar version") + } + + log.Info().Msg("version is updated successfully") + log.Info().Msgf("%s version is: %+v", net, version) +} + +func createAccount(c *client.RegistrarClient, relays []string, rmbEncKey string) { + log.Info().Msg("create account") + account, mnemonic, err := c.CreateAccount(relays, rmbEncKey) + if err != nil { + log.Fatal().Err(err).Msg("failed to create new account on registrar") + } + + log.Info().Uint64("twinID", account.TwinID).Str("mnemonic", mnemonic).Msg("account created successfully") + twinID = account.TwinID +} + +func getAccountByPK(c *client.RegistrarClient, pk []byte) { + log.Info().Msg("get account by public key") + account, err := c.GetAccountByPK(pk) + if err != nil { + log.Fatal().Err(err).Msg("failed to get account from registrar") + } + log.Info().Any("account", account).Send() +} + +func getAccount(c *client.RegistrarClient, id uint64) { + log.Info().Msg("get account by twinID") + account, err := c.GetAccount(id) + log.Info().Uint64("twinID", id).Send() + if err != nil { + log.Fatal().Err(err).Msg("failed to get account from registrar") + } + log.Info().Any("account", account).Send() +} + +func updateAccount(c *client.RegistrarClient, relays []string, rmbEncKey string) { + log.Info().Msg("update account") + err := c.UpdateAccount(client.UpdateAccountWithRelays(relays), client.UpdateAccountWithRMBEncKey(rmbEncKey)) + if err != nil { + log.Fatal().Err(err).Msg("failed to get account from registrar") + } + log.Info().Msg("account updated successfully") +} + +func enusreAccount(c *client.RegistrarClient, relays []string, rmbEncKey string) { + log.Info().Msg("ensure account") + account, err := c.EnsureAccount(relays, rmbEncKey) + if err != nil { + log.Fatal().Err(err).Msg("failed to ensure account account from registrar") + } + log.Info().Any("account", account).Send() +} + +func createFarm(c *client.RegistrarClient, farmName string) { + log.Info().Msg("create farm") + id, err := c.CreateFarm(farmName, "GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGJJJJJJGGGGGGGGGG", false) + if err != nil { + log.Fatal().Err(err).Msg("failed to create new farm on registrar") + } + + log.Info().Uint64("farmID", id).Msg("farm created successfully") + farmID = id +} + +func getFarm(c *client.RegistrarClient, id uint64) { + log.Info().Msg("get farm by id") + farm, err := c.GetFarm(id) + if err != nil { + log.Fatal().Err(err).Msg("failed to get farm from registrar") + } + log.Info().Any("farm", farm).Send() +} + +func updateFarm(c *client.RegistrarClient, name string) { + log.Info().Msg("update farm") + addr := "GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGJJJJJJGGGGGGGGGG" + err := c.UpdateFarm(2, client.FarmUpdate{FarmName: &name, StellarAddress: &addr}) + if err != nil { + log.Fatal().Err(err).Msg("failed to update farm from registrar") + } + log.Info().Msg("farm updated successfully") +} + +func registerNode(c *client.RegistrarClient, farmID uint64, twinID uint64, interfaces []client.Interface, location client.Location, resources client.Resources, serialNumber string, secureBoot, virtualized bool) { + log.Info().Msg("register node") + id, err := c.RegisterNode(client.Node{ + FarmID: farmID, + TwinID: twinID, + Interfaces: interfaces, + Location: location, + Resources: resources, + SerialNumber: serialNumber, + SecureBoot: secureBoot, + Virtualized: virtualized, + }) + if err != nil { + log.Fatal().Err(err).Msg("failed to register a new node on registrar") + } + + log.Info().Uint64("nodeID", id).Msg("node registered successfully") + nodeID = id +} + +func getNode(c *client.RegistrarClient, id uint64) (node client.Node) { + log.Info().Msg("get node with node id") + node, err := c.GetNode(id) + if err != nil { + log.Fatal().Err(err).Msg("failed to get node from registrar") + } + log.Info().Any("node", node).Send() + return node +} + +func getNodeWithTwinID(c *client.RegistrarClient, id uint64) { + log.Info().Msg("get node with twin id") + node, err := c.GetNodeByTwinID(id) + if err != nil { + log.Fatal().Err(err).Msg("failed to get node from registrar") + } + log.Info().Any("node", node).Send() +} + +func updateNode(c *client.RegistrarClient, location client.Location) { + log.Info().Msg("update node, update node location") + err := c.UpdateNode(client.NodeUpdate{Resources: &client.Resources{CRU: 8, MRU: 68719476736, SRU: 4398046511104, HRU: 17592186044416}}) + if err != nil { + log.Fatal().Err(err).Msg("failed to update node location on the registrar") + } + log.Info().Msg("node updated successfully") +} + +func sendUptimeReport(c *client.RegistrarClient, report client.UptimeReport) { + log.Info().Msg("send uptime report") + err := c.ReportUptime(report) + if err != nil { + log.Fatal().Err(err).Msg("failed to update node uptime in the registrar") + } + log.Info().Msg("node uptime is updated successfully") +} + +func parseSeed(mnemonicOrSeed string) (publicKey ed25519.PublicKey, err error) { + keypair, err := subkey.DeriveKeyPair(subkeyEd25519.Scheme{}, mnemonicOrSeed) + if err != nil { + return publicKey, errors.Wrapf(err, "Failed to derive key pair from seed %s", mnemonicOrSeed) + } + + return keypair.Public(), err +} From 5e9f390584e06020ed4575e9e760897e708f4e12 Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Mon, 23 Jun 2025 18:40:12 +0300 Subject: [PATCH 18/43] test: add comprehensive test suite for rewards calculation and uptime tracking --- node-registrar/pkg/server/rewards_test.go | 474 ++++++++++++++++++++++ 1 file changed, 474 insertions(+) diff --git a/node-registrar/pkg/server/rewards_test.go b/node-registrar/pkg/server/rewards_test.go index 5428dca..251e546 100644 --- a/node-registrar/pkg/server/rewards_test.go +++ b/node-registrar/pkg/server/rewards_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/threefoldtech/tfgrid4-sdk-go/node-registrar/pkg/db" ) @@ -204,6 +205,479 @@ func TestCalculatePeriodStart(t *testing.T) { } } +func TestCalculateTotalReward(t *testing.T) { + tests := []struct { + name string + capacity db.Resources + upTimePercentage float64 + expected float64 + }{ + { + name: "standard capacity with 100% uptime", + capacity: db.Resources{ + CRU: 8, + MRU: 68719476736, // 64 GB + SRU: 1099511627776, // 1 TB + HRU: 10995116277760, // 10 TB + }, + upTimePercentage: 100, + expected: 512 + 31.5 + 70, // (64 * 8) + (1 * 31.5) + (10 * 7) + }, + { + name: "standard capacity with 50% uptime", + capacity: db.Resources{ + CRU: 8, + MRU: 68719476736, // 64 GB + SRU: 1099511627776, // 1 TB + HRU: 10995116277760, // 10 TB + }, + upTimePercentage: 50, + expected: (512 + 31.5 + 70) * 0.5, // 50% of total + }, + { + name: "zero capacity with 100% uptime", + capacity: db.Resources{ + CRU: 0, + MRU: 0, + SRU: 0, + HRU: 0, + }, + upTimePercentage: 100, + expected: 0, + }, + { + name: "small memory only capacity with 95% uptime", + capacity: db.Resources{ + CRU: 0, + MRU: 1073741824, // 1 GB + SRU: 0, + HRU: 0, + }, + upTimePercentage: 95, + expected: 8 * 0.95, // (1 * 8) * 0.95 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := calculateTotalReward(tt.capacity, tt.upTimePercentage) + + // Use a small delta for floating point comparison + assert.InDelta(t, tt.expected, result, 0.001, "Total reward calculation incorrect") + }) + } +} + +// TestCalculateBaseCapacityReward tests the calculateBaseCapacityReward function +func TestCalculateBaseCapacityReward(t *testing.T) { + tests := []struct { + name string + capacity db.Resources + expected float64 + }{ + { + name: "standard capacity", + capacity: db.Resources{ + CRU: 8, + MRU: 68719476736, // 64 GB + SRU: 1099511627776, // 1 TB + HRU: 10995116277760, // 10 TB + }, + expected: 512 + 31.5 + 70, // (64 * 8) + (1 * 31.5) + (10 * 7) + }, + { + name: "zero capacity", + capacity: db.Resources{ + CRU: 0, + MRU: 0, + SRU: 0, + HRU: 0, + }, + expected: 0, + }, + { + name: "memory only", + capacity: db.Resources{ + CRU: 0, + MRU: 1073741824, // 1 GB + SRU: 0, + HRU: 0, + }, + expected: 8, // 1 * 8 + }, + { + name: "SSD only", + capacity: db.Resources{ + CRU: 0, + MRU: 0, + SRU: 1099511627776, // 1 TB + HRU: 0, + }, + expected: 31.5, // 1 * 31.5 + }, + { + name: "HDD only", + capacity: db.Resources{ + CRU: 0, + MRU: 0, + SRU: 0, + HRU: 1099511627776, // 1 TB + }, + expected: 7, // 1 * 7 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := calculateBaseCapacityReward(tt.capacity) + + // Use a small delta for floating point comparison + assert.InDelta(t, tt.expected, result, 0.001, "Base capacity reward calculation incorrect") + }) + } +} + +// TestRewardFloatingPointPrecision tests that the floating point calculations in CalculateCapacityReward +// are handled correctly and the percentages are applied as expected +func TestRewardFloatingPointPrecision(t *testing.T) { + // Create a resource with a specific memory size to test floating point precision + memoryOnlyCapacity := db.Resources{ + CRU: 0, + MRU: 1073741824, // 1 GB exactly + SRU: 0, + HRU: 0, + } + + // Calculate expected values based on our constants + expectedBaseReward := bytesToGB(memoryOnlyCapacity.MRU) * MemoryRewardPerGB // Should be 8.0 + expectedTotal := expectedBaseReward // 100% uptime + + // Expected distribution based on percentages + expectedFarmerReward := expectedTotal * FarmerRewardPercentage // 8.0 * 0.6 = 4.8 + expectedTfReward := expectedTotal * TfRewardPercentage // 8.0 * 0.2 = 1.6 + expectedFpReward := expectedTotal * FpRewardPercentage // 8.0 * 0.2 = 1.6 + + // Get the actual reward calculation + reward, err := CalculateCapacityReward(memoryOnlyCapacity, 100) + require.NoError(t, err) + + // Test precision and distribution + t.Run("base reward calculation", func(t *testing.T) { + assert.InDelta(t, expectedBaseReward, calculateBaseCapacityReward(memoryOnlyCapacity), 0.001) + }) + + t.Run("total reward", func(t *testing.T) { + assert.InDelta(t, expectedTotal, reward.Total, 0.001) + }) + + // Test the distribution percentages + t.Run("farmer reward percentage", func(t *testing.T) { + assert.InDelta(t, expectedFarmerReward, reward.FarmerReward, 0.001) + assert.InDelta(t, 0.6, reward.FarmerReward/reward.Total, 0.001, "Farmer reward should be 60% of total") + }) + + t.Run("tf reward percentage", func(t *testing.T) { + assert.InDelta(t, expectedTfReward, reward.TfReward, 0.001) + assert.InDelta(t, 0.2, reward.TfReward/reward.Total, 0.001, "TF reward should be 20% of total") + }) + + t.Run("fp reward percentage", func(t *testing.T) { + assert.InDelta(t, expectedFpReward, reward.FpReward, 0.001) + assert.InDelta(t, 0.2, reward.FpReward/reward.Total, 0.001, "FP reward should be 20% of total") + }) + + // Test that sum of portions equals total (within rounding error) + t.Run("reward portions sum to total", func(t *testing.T) { + actualSum := reward.FarmerReward + reward.TfReward + reward.FpReward + assert.InDelta(t, reward.Total, actualSum, 0.001, "Sum of reward portions should equal total reward") + }) +} + +// TestAreReportsOrderedCorrectly tests the areReportsOrderedCorrectly function +func TestAreReportsOrderedCorrectly(t *testing.T) { + now := time.Now() + tests := []struct { + name string + reports []db.UptimeReport + expected bool + }{ + { + name: "empty reports", + reports: []db.UptimeReport{}, + expected: true, // empty reports are considered properly ordered + }, + { + name: "single report", + reports: []db.UptimeReport{ + {Timestamp: now}, + }, + expected: true, // single report is always ordered + }, + { + name: "ordered reports", + reports: []db.UptimeReport{ + {Timestamp: now.Add(-2 * time.Hour)}, + {Timestamp: now.Add(-1 * time.Hour)}, + {Timestamp: now}, + }, + expected: true, + }, + { + name: "unordered reports", + reports: []db.UptimeReport{ + {Timestamp: now.Add(-1 * time.Hour)}, + {Timestamp: now.Add(-2 * time.Hour)}, // out of order + {Timestamp: now}, + }, + expected: false, + }, + { + name: "same timestamps", + reports: []db.UptimeReport{ + {Timestamp: now}, + {Timestamp: now}, // same timestamp + }, + expected: true, // equal timestamps are considered ordered + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := areReportsOrderedCorrectly(tt.reports) + assert.Equal(t, tt.expected, result) + }) + } +} + +// TestCalculateDowntimeFromReports tests the calculateDowntimeFromReports function +func TestCalculateDowntimeFromReports(t *testing.T) { + now := time.Now().Truncate(time.Second) + tests := []struct { + name string + reports []db.UptimeReport + expected time.Duration + }{ + { + name: "empty reports", + reports: []db.UptimeReport{}, + expected: 0, + }, + { + name: "single report", + reports: []db.UptimeReport{ + {Timestamp: now, Duration: time.Hour}, + }, + expected: 0, // single report means no gaps to calculate + }, + { + name: "no downtime - perfect reports", + reports: []db.UptimeReport{ + {Timestamp: now.Add(-2 * time.Hour), Duration: time.Hour}, + {Timestamp: now.Add(-1 * time.Hour), Duration: time.Hour}, + }, + expected: 0, // no downtime + }, + { + name: "partial downtime - gap larger than duration", + reports: []db.UptimeReport{ + {Timestamp: now.Add(-3 * time.Hour), Duration: time.Hour}, + {Timestamp: now.Add(-1 * time.Hour), Duration: time.Hour}, // 2 hour gap, 1 hour reported + }, + expected: time.Hour, // 1 hour of downtime + }, + { + name: "decreasing duration but no downtime due to equal gap and duration", + reports: []db.UptimeReport{ + {Timestamp: now.Add(-2 * time.Hour), Duration: 2 * time.Hour}, + {Timestamp: now.Add(-1 * time.Hour), Duration: time.Hour}, // duration decreased but gap == duration + }, + expected: 0, // No downtime since gap == duration + }, + { + name: "decreasing duration with actual downtime", + reports: []db.UptimeReport{ + {Timestamp: now.Add(-3 * time.Hour), Duration: 2 * time.Hour}, + {Timestamp: now.Add(-1 * time.Hour), Duration: time.Hour}, // duration decreased and gap > duration + }, + expected: time.Hour, // 1 hour of downtime (2 hour gap - 1 hour duration) + }, + { + name: "multiple downtime periods", + reports: []db.UptimeReport{ + {Timestamp: now.Add(-4 * time.Hour), Duration: time.Hour}, + {Timestamp: now.Add(-3 * time.Hour), Duration: 30 * time.Minute}, // 30 min downtime + {Timestamp: now.Add(-1 * time.Hour), Duration: 30 * time.Minute}, // 90 min downtime (2h - 30min) + }, + expected: 30*time.Minute + 90*time.Minute, // total 2 hours of downtime + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := calculateDowntimeFromReports(tt.reports) + assert.Equal(t, tt.expected, result) + }) + } +} + +// TestCalculatePercentage tests the calculatePercentage function +func TestCalculatePercentage(t *testing.T) { + tests := []struct { + name string + totalPeriod time.Duration + downtime time.Duration + expected float64 + }{ + { + name: "no downtime", + totalPeriod: 24 * time.Hour, + downtime: 0, + expected: 100.0, + }, + { + name: "50% downtime", + totalPeriod: 24 * time.Hour, + downtime: 12 * time.Hour, + expected: 50.0, + }, + { + name: "total downtime", + totalPeriod: 24 * time.Hour, + downtime: 24 * time.Hour, + expected: 0.0, + }, + { + name: "25% downtime", + totalPeriod: 24 * time.Hour, + downtime: 6 * time.Hour, + expected: 75.0, + }, + { + name: "small percentage downtime", + totalPeriod: 1000 * time.Hour, + downtime: 1 * time.Hour, + expected: 99.9, // truncated to 2 decimal places + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := calculatePercentage(tt.totalPeriod, tt.downtime) + assert.Equal(t, tt.expected, result) + }) + } +} + +// TestDowntimeSinceLastReportTimestamp tests the downtimeSinceLastReportTimestamp function +func TestDowntimeSinceLastReportTimestamp(t *testing.T) { + now := time.Now().Truncate(time.Second) + tests := []struct { + name string + lastReportTimestamp time.Time + currentTime time.Time + expected time.Duration + }{ + { + name: "recent report, no downtime", + lastReportTimestamp: now.Add(-time.Duration(UptimeEventsInterval-100) * time.Second), + currentTime: now, + expected: 0, + }, + { + name: "exactly at threshold", + lastReportTimestamp: now.Add(-time.Duration(UptimeEventsInterval) * time.Second), + currentTime: now, + expected: time.Duration(UptimeEventsInterval) * time.Second, + }, + { + name: "over threshold", + lastReportTimestamp: now.Add(-2 * time.Duration(UptimeEventsInterval) * time.Second), + currentTime: now, + expected: 2 * time.Duration(UptimeEventsInterval) * time.Second, + }, + { + name: "future timestamp, no downtime", + lastReportTimestamp: now.Add(time.Hour), + currentTime: now, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := downtimeSinceLastReportTimestamp(tt.lastReportTimestamp, tt.currentTime) + assert.Equal(t, tt.expected, result) + }) + } +} + +// TestHelper functions tests the various helper functions used in reward calculations +func TestHelperFunctions(t *testing.T) { + // Test bytesToGB + t.Run("bytesToGB conversion", func(t *testing.T) { + t.Run("zero bytes", func(t *testing.T) { + result := bytesToGB(0) + assert.Equal(t, 0.0, result, "0 bytes should convert to 0 GB") + }) + + t.Run("1 GB", func(t *testing.T) { + result := bytesToGB(1073741824) // 1 GB in bytes (2^30) + assert.InDelta(t, 1.0, result, 0.001, "1073741824 bytes should convert to 1 GB") + }) + + t.Run("1.5 GB", func(t *testing.T) { + result := bytesToGB(1610612736) // 1.5 GB in bytes + assert.InDelta(t, 1.5, result, 0.001, "1610612736 bytes should convert to 1.5 GB") + }) + }) + + // Test bytesToTB + t.Run("bytesToTB conversion", func(t *testing.T) { + t.Run("zero bytes", func(t *testing.T) { + result := bytesToTB(0) + assert.Equal(t, 0.0, result, "0 bytes should convert to 0 TB") + }) + + t.Run("1 TB", func(t *testing.T) { + result := bytesToTB(1099511627776) // 1 TB in bytes (2^40) + assert.InDelta(t, 1.0, result, 0.001, "1099511627776 bytes should convert to 1 TB") + }) + + t.Run("2.5 TB", func(t *testing.T) { + result := bytesToTB(2748779069440) // 2.5 TB in bytes + assert.InDelta(t, 2.5, result, 0.001, "2748779069440 bytes should convert to 2.5 TB") + }) + }) + + // Test truncateFloat + t.Run("truncateFloat", func(t *testing.T) { + t.Run("truncate to 2 places", func(t *testing.T) { + result := truncateFloat(123.456789, 2) + assert.Equal(t, 123.45, result, "123.456789 truncated to 2 decimal places should be 123.45") + }) + + t.Run("truncate 124", func(t *testing.T) { + result := truncateFloat(124, 0) + assert.Equal(t, 124.0, result, "124 truncated to 0 decimal places should be 124.0") + }) + + t.Run("truncate to 0 places", func(t *testing.T) { + result := truncateFloat(123.456789, 0) + assert.Equal(t, 123.0, result, "123.456789 truncated to 0 decimal places should be 123.0") + }) + + t.Run("truncate to 3 places", func(t *testing.T) { + result := truncateFloat(123.456789, 3) + assert.Equal(t, 123.456, result, "123.456789 truncated to 3 decimal places should be 123.456") + }) + + t.Run("truncate negative number", func(t *testing.T) { + result := truncateFloat(-123.456789, 2) + assert.Equal(t, -123.45, result, "-123.456789 truncated to 2 decimal places should be -123.45") + }) + }) +} + func TestCalculateUpTimePercentage(t *testing.T) { type args struct { reports []db.UptimeReport From f5382957397a58ea088f4cc06966eb524ab03a2e Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Mon, 23 Jun 2025 18:40:37 +0300 Subject: [PATCH 19/43] revert adding main.go --- node-registrar/test/main.go | 294 ------------------------------------ 1 file changed, 294 deletions(-) delete mode 100644 node-registrar/test/main.go diff --git a/node-registrar/test/main.go b/node-registrar/test/main.go deleted file mode 100644 index 95c606c..0000000 --- a/node-registrar/test/main.go +++ /dev/null @@ -1,294 +0,0 @@ -package main - -import ( - "crypto/ed25519" - "os" - "time" - - subkeyEd25519 "github.com/vedhavyas/go-subkey/v2/ed25519" - - "github.com/pkg/errors" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - "github.com/threefoldtech/tfgrid4-sdk-go/node-registrar/client" - "github.com/vedhavyas/go-subkey/v2" -) - -var ( - twinID uint64 = 4 - farmID uint64 = 1 - nodeID uint64 = 2 - farmName = "freeFarm" - net = "" -) - -const ( - localHexKey = "acoustic foot tomorrow brown candy cash reject hurt wood roof blossom sausage" - dev4HexKey = "" - qa4HexKey = "" - test4HexKey = "" - prod4hexKey = "" -) - -type network struct { - url string - key string -} - -var networks = map[string]network{ - "local": {url: "http://localhost:8080/api/v1", key: localHexKey}, - // "dev": {url: "https://registrar.dev4.grid.tf/api/v1", key: dev4HexKey}, - // "qa": {url: "https://registrar.qa4.grid.tf/api/v1", key: qa4HexKey}, - // "test": {url: "https://registrar.test4.grid.tf/api/v1", key: test4HexKey}, - // "prod": {url: "https://registrar.prod4.grid.tf/api/v1", key: prod4hexKey}, -} - -func main() { - log.Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).Level(zerolog.DebugLevel).With().Timestamp().Logger() - if _, ok := networks[net]; !ok { - net = "local" - } - - registrarURL := networks[net].url - // seedOrMnemonic := networks[net].key - - // publicKey, err := parseSeed(seedOrMnemonic) - // if err != nil { - // log.Fatal().Err(err).Send() - // } - - c, err := client.NewRegistrarClient(registrarURL, "tool mirror high clay quit cube affair dirt rely hire joy text") - if err != nil { - log.Fatal().Err(err).Msg("failed to create new registrar client") - } - - // manageAccount(&c, publicKey) - - // manageFarm(&c) - - manageNode(&c) - - // manageVersion(&c) -} - -// The flow: -// - we need to create an account -// - try to get the account by id and by pk -// - try to update the account -// - try to ensure account -func manageAccount(c *client.RegistrarClient, publicKey ed25519.PublicKey) { - relays := []string{} - rmbEncKey := "" - - log.Info().Msg("***************************************************************************") - log.Info().Msg("manage account create/update/get/ensure") - - createAccount(c, relays, rmbEncKey) - getAccountByPK(c, publicKey) - relays = []string{"relay1", "relay2"} - rmbEncKey = "key1&2" - // updateAccount(c, relays, rmbEncKey) - enusreAccount(c, relays, rmbEncKey) - getAccount(c, twinID) -} - -// The flow: -// - we need to create a farm with the created twin -// - try to get the farm -// - try to update the farm -func manageFarm(c *client.RegistrarClient) { - log.Info().Msg("***************************************************************************") - log.Info().Msg("manage farm create/update/get") - // createFarm(c, farmName) - // getFarm(c, farmID) - // updateFarm(c, "notFreeFarm101") - // getFarm(c, farmID) -} - -// The flow: -// - we need to create an account and use it to register a node in the created farm -// - try to update the node -// - try to get the node -// - try to send the node up time report -// - try to the node by twin id -func manageNode(c *client.RegistrarClient) { - log.Info().Msg("***************************************************************************") - log.Info().Msg("manage node register/update/get/list/send uptime report") - // Try with a nil IPs field to see if that helps - interface1 := client.Interface{ - Name: "zos", - Mac: "mac", - IPs: []string{"1.1.1.1"}, - - // Not explicitly setting IPs field at all - } - - registerNode(c, farmID, twinID, []client.Interface{interface1}, client.Location{City: "somewhere"}, client.Resources{CRU: 8, SRU: 5497558138880, - MRU: 34359738368, - HRU: 17592186044416}, "serialNumber", false, false) - node := getNode(c, nodeID) - // updateNode(c, client.Location{City: "somewhere"}) - sendUptimeReport(c, client.UptimeReport{Uptime: 40 * 60, Timestamp: time.Now().Unix()}) - getNodeWithTwinID(c, node.TwinID) -} - -func manageVersion(c *client.RegistrarClient) { - log.Info().Msg("***************************************************************************") - log.Info().Msg("manage version set/get") - err := c.SetZosVersion("v0.1.8", true) - if err != nil { - log.Fatal().Err(err).Msg("failed to set registrar version") - } - - version, err := c.GetZosVersion() - if err != nil { - log.Fatal().Err(err).Msg("failed to set registrar version") - } - - log.Info().Msg("version is updated successfully") - log.Info().Msgf("%s version is: %+v", net, version) -} - -func createAccount(c *client.RegistrarClient, relays []string, rmbEncKey string) { - log.Info().Msg("create account") - account, mnemonic, err := c.CreateAccount(relays, rmbEncKey) - if err != nil { - log.Fatal().Err(err).Msg("failed to create new account on registrar") - } - - log.Info().Uint64("twinID", account.TwinID).Str("mnemonic", mnemonic).Msg("account created successfully") - twinID = account.TwinID -} - -func getAccountByPK(c *client.RegistrarClient, pk []byte) { - log.Info().Msg("get account by public key") - account, err := c.GetAccountByPK(pk) - if err != nil { - log.Fatal().Err(err).Msg("failed to get account from registrar") - } - log.Info().Any("account", account).Send() -} - -func getAccount(c *client.RegistrarClient, id uint64) { - log.Info().Msg("get account by twinID") - account, err := c.GetAccount(id) - log.Info().Uint64("twinID", id).Send() - if err != nil { - log.Fatal().Err(err).Msg("failed to get account from registrar") - } - log.Info().Any("account", account).Send() -} - -func updateAccount(c *client.RegistrarClient, relays []string, rmbEncKey string) { - log.Info().Msg("update account") - err := c.UpdateAccount(client.UpdateAccountWithRelays(relays), client.UpdateAccountWithRMBEncKey(rmbEncKey)) - if err != nil { - log.Fatal().Err(err).Msg("failed to get account from registrar") - } - log.Info().Msg("account updated successfully") -} - -func enusreAccount(c *client.RegistrarClient, relays []string, rmbEncKey string) { - log.Info().Msg("ensure account") - account, err := c.EnsureAccount(relays, rmbEncKey) - if err != nil { - log.Fatal().Err(err).Msg("failed to ensure account account from registrar") - } - log.Info().Any("account", account).Send() -} - -func createFarm(c *client.RegistrarClient, farmName string) { - log.Info().Msg("create farm") - id, err := c.CreateFarm(farmName, "GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGJJJJJJGGGGGGGGGG", false) - if err != nil { - log.Fatal().Err(err).Msg("failed to create new farm on registrar") - } - - log.Info().Uint64("farmID", id).Msg("farm created successfully") - farmID = id -} - -func getFarm(c *client.RegistrarClient, id uint64) { - log.Info().Msg("get farm by id") - farm, err := c.GetFarm(id) - if err != nil { - log.Fatal().Err(err).Msg("failed to get farm from registrar") - } - log.Info().Any("farm", farm).Send() -} - -func updateFarm(c *client.RegistrarClient, name string) { - log.Info().Msg("update farm") - addr := "GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGJJJJJJGGGGGGGGGG" - err := c.UpdateFarm(2, client.FarmUpdate{FarmName: &name, StellarAddress: &addr}) - if err != nil { - log.Fatal().Err(err).Msg("failed to update farm from registrar") - } - log.Info().Msg("farm updated successfully") -} - -func registerNode(c *client.RegistrarClient, farmID uint64, twinID uint64, interfaces []client.Interface, location client.Location, resources client.Resources, serialNumber string, secureBoot, virtualized bool) { - log.Info().Msg("register node") - id, err := c.RegisterNode(client.Node{ - FarmID: farmID, - TwinID: twinID, - Interfaces: interfaces, - Location: location, - Resources: resources, - SerialNumber: serialNumber, - SecureBoot: secureBoot, - Virtualized: virtualized, - }) - if err != nil { - log.Fatal().Err(err).Msg("failed to register a new node on registrar") - } - - log.Info().Uint64("nodeID", id).Msg("node registered successfully") - nodeID = id -} - -func getNode(c *client.RegistrarClient, id uint64) (node client.Node) { - log.Info().Msg("get node with node id") - node, err := c.GetNode(id) - if err != nil { - log.Fatal().Err(err).Msg("failed to get node from registrar") - } - log.Info().Any("node", node).Send() - return node -} - -func getNodeWithTwinID(c *client.RegistrarClient, id uint64) { - log.Info().Msg("get node with twin id") - node, err := c.GetNodeByTwinID(id) - if err != nil { - log.Fatal().Err(err).Msg("failed to get node from registrar") - } - log.Info().Any("node", node).Send() -} - -func updateNode(c *client.RegistrarClient, location client.Location) { - log.Info().Msg("update node, update node location") - err := c.UpdateNode(client.NodeUpdate{Resources: &client.Resources{CRU: 8, MRU: 68719476736, SRU: 4398046511104, HRU: 17592186044416}}) - if err != nil { - log.Fatal().Err(err).Msg("failed to update node location on the registrar") - } - log.Info().Msg("node updated successfully") -} - -func sendUptimeReport(c *client.RegistrarClient, report client.UptimeReport) { - log.Info().Msg("send uptime report") - err := c.ReportUptime(report) - if err != nil { - log.Fatal().Err(err).Msg("failed to update node uptime in the registrar") - } - log.Info().Msg("node uptime is updated successfully") -} - -func parseSeed(mnemonicOrSeed string) (publicKey ed25519.PublicKey, err error) { - keypair, err := subkey.DeriveKeyPair(subkeyEd25519.Scheme{}, mnemonicOrSeed) - if err != nil { - return publicKey, errors.Wrapf(err, "Failed to derive key pair from seed %s", mnemonicOrSeed) - } - - return keypair.Public(), err -} From 81906f978b7471aa8a3240dd2d41136b76cae95c Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Mon, 23 Jun 2025 18:46:06 +0300 Subject: [PATCH 20/43] docs: simplify uptime percentage calculation documentation with concise explanation --- node-registrar/pkg/server/rewards.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/node-registrar/pkg/server/rewards.go b/node-registrar/pkg/server/rewards.go index 042f037..e2b4fc9 100644 --- a/node-registrar/pkg/server/rewards.go +++ b/node-registrar/pkg/server/rewards.go @@ -175,13 +175,9 @@ func downtimeSinceLastReportTimestamp(lastReportTimestamp time.Time, currentTime // calculateUpTimePercentage calculates the uptime percentage for a given node within a specific period. // -// This function takes a slice of UptimeReport, a period start time and a current time as parameters. -// It calculates the uptime percentage by comparing the expected uptime (calculated by subtracting the timestamp of the previous report from the current report) -// with the actual uptime (calculated from the duration the current report). -// If the actual uptime is less than the expected uptime, the difference is counted as downtime. -// Additionally, if there is a gap equals or larger than the @UPTIME_EVENTS_INTERVAL between the last report and now, add it to the downtime. -// The uptime percentage is then calculated by subtracting the total downtime from the total elapsed time since the period start and dividing the result by the total elapsed time. -// The result is then multiplied by 100 to get the percentage. +// This function calculates the node's uptime percentage based on a series of UptimeReports. +// It computes downtime by analyzing gaps between reports and by checking if there's been significant downtime since the last report. +// The uptime percentage is calculated as: (totalPeriod - downtime) / totalPeriod * 100. func calculateUpTimePercentage(reports []db.UptimeReport, periodStart, now time.Time) (float64, error) { if len(reports) == 0 { From 27c3fe8b3b2b82d59f6cb5f211ed5f071e321ff9 Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Mon, 23 Jun 2025 19:39:20 +0300 Subject: [PATCH 21/43] feat: add node capacity rewards calculation endpoint and types --- node-registrar/client/node.go | 37 ++++++++++++++++++++++++++++++++++ node-registrar/client/types.go | 8 ++++++++ 2 files changed, 45 insertions(+) diff --git a/node-registrar/client/node.go b/node-registrar/client/node.go index 5d5eade..83e158b 100644 --- a/node-registrar/client/node.go +++ b/node-registrar/client/node.go @@ -40,6 +40,11 @@ func (c *RegistrarClient) GetNodeByTwinID(id uint64) (node Node, err error) { return c.getNodeByTwinID(id) } +// GetNodeCapacityRewards calculates the reward for a node based on its capacity and uptime. +func (c *RegistrarClient) GetNodeCapacityRewards(nodeID uint64) (reward NodeCapacityReward, err error) { + return c.getNodeCapacityRewards(nodeID) +} + // ListNodes lists registered nodes details using (nodeID, twinID, farmID). func (c *RegistrarClient) ListNodes(opts NodeFilter) (nodes []Node, err error) { return c.listNodesWithFilter(opts) @@ -320,6 +325,38 @@ func (c *RegistrarClient) getNodeByTwinID(id uint64) (node Node, err error) { return nodes[0], nil } +func (c *RegistrarClient) getNodeCapacityRewards(nodeID uint64) (reward NodeCapacityReward, err error) { + + url, err := url.JoinPath(c.baseURL, "nodes", fmt.Sprint(nodeID), "rewards") + if err != nil { + return reward, errors.Wrap(err, "failed to construct rewards url") + } + + resp, err := c.httpClient.Get(url) + if err != nil { + return reward, errors.Wrap(err, "failed to send request to rewards endpoint") + } + defer resp.Body.Close() + if resp.StatusCode == http.StatusNotFound { + return reward, ErrorNodeNotFound + } + + if resp.StatusCode == http.StatusUnprocessableEntity { + return reward, parseResponseError(resp.Body) + } + + if resp.StatusCode != http.StatusOK { + err = parseResponseError(resp.Body) + return reward, errors.Wrap(err, "failed to get node rewards") + } + + if err = json.NewDecoder(resp.Body).Decode(&reward); err != nil { + return reward, errors.Wrap(err, "failed to decode rewards response") + } + + return +} + // NodeFilter represents filtering options for listing nodes type NodeFilter struct { NodeID *uint64 diff --git a/node-registrar/client/types.go b/node-registrar/client/types.go index 5c150d4..037b363 100644 --- a/node-registrar/client/types.go +++ b/node-registrar/client/types.go @@ -64,3 +64,11 @@ type Location struct { Longitude string `json:"longitude"` Latitude string `json:"latitude"` } + +type NodeCapacityReward struct { + FarmerReward float64 `json:"farmerReward"` // Reward amount for the node owner (60%) + TfReward float64 `json:"tfReward"` // Reward amount for Threefold Foundation (20%) + FpReward float64 `json:"fpReward"` // Reward amount for the Farming Pool (20%) + Total float64 `json:"total"` // Total reward amount + UpTimePercentage float64 `json:"uptimePercentage"` // Node's uptime percentage +} From b69397d15a0b8f47f1a3d5760dcacdd5934d7290 Mon Sep 17 00:00:00 2001 From: Eslam-Nawara Date: Tue, 24 Jun 2025 18:11:28 +0300 Subject: [PATCH 22/43] fix node tests in client --- node-registrar/client/node_test.go | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/node-registrar/client/node_test.go b/node-registrar/client/node_test.go index b59d356..f72fdb1 100644 --- a/node-registrar/client/node_test.go +++ b/node-registrar/client/node_test.go @@ -1,6 +1,7 @@ package client import ( + "encoding/base64" "net/http" "net/http/httptest" "net/url" @@ -20,6 +21,10 @@ func TestRegistarNode(t *testing.T) { FarmID: farmID, } + keyPair, err := parseKeysFromMnemonicOrSeed(testMnemonic) + require.NoError(err) + account.PublicKey = base64.StdEncoding.EncodeToString(keyPair.Public()) + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { statusCode, body := serverHandler(r, request, count, require) w.WriteHeader(statusCode) @@ -73,10 +78,10 @@ func TestUpdateNode(t *testing.T) { var count int require := require.New(t) - // publicKey, privateKey, err := aliceKeys() - // require.NoError(err) - // account.PublicKey = base64.StdEncoding.EncodeToString(publicKey) - // + keyPair, err := parseKeysFromMnemonicOrSeed(testMnemonic) + require.NoError(err) + account.PublicKey = base64.StdEncoding.EncodeToString(keyPair.Public()) + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { statusCode, body := serverHandler(r, request, count, require) w.WriteHeader(statusCode) @@ -118,9 +123,9 @@ func TestGetNode(t *testing.T) { var count int require := require.New(t) - // publicKey, privateKey, err := aliceKeys() - // require.NoError(err) - // account.PublicKey = base64.StdEncoding.EncodeToString(publicKey) + keyPair, err := parseKeysFromMnemonicOrSeed(testMnemonic) + require.NoError(err) + account.PublicKey = base64.StdEncoding.EncodeToString(keyPair.Public()) testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { statusCode, body := serverHandler(r, request, count, require) @@ -139,12 +144,14 @@ func TestGetNode(t *testing.T) { require.NoError(err) t.Run("test get node status not found", func(t *testing.T) { + count = 0 request = getNodeWithIDStatusNotFound _, err = c.GetNode(nodeID) require.Error(err) }) t.Run("test get node, status ok", func(t *testing.T) { + count = 0 request = getNodeWithIDStatusOK result, err := c.GetNode(nodeID) require.NoError(err) @@ -152,6 +159,7 @@ func TestGetNode(t *testing.T) { }) t.Run("test get node with twin id", func(t *testing.T) { + count = 0 request = getNodeWithTwinID result, err := c.GetNodeByTwinID(twinID) require.NoError(err) @@ -159,6 +167,7 @@ func TestGetNode(t *testing.T) { }) t.Run("test list nodes of specific farm", func(t *testing.T) { + count = 0 request = listNodesInFarm id := farmID result, err := c.ListNodes(NodeFilter{FarmID: &id}) From 395a4fc6dfe4d082669392f77eadbfc95847e266 Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Tue, 24 Jun 2025 18:19:37 +0300 Subject: [PATCH 23/43] WIP: add rewards tests in client --- node-registrar/client/node_test.go | 45 +++++++++++++++++++++++++++++ node-registrar/client/utils_test.go | 25 ++++++++++++++-- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/node-registrar/client/node_test.go b/node-registrar/client/node_test.go index f72fdb1..19c2df5 100644 --- a/node-registrar/client/node_test.go +++ b/node-registrar/client/node_test.go @@ -2,6 +2,7 @@ package client import ( "encoding/base64" + "fmt" "net/http" "net/http/httptest" "net/url" @@ -162,6 +163,7 @@ func TestGetNode(t *testing.T) { count = 0 request = getNodeWithTwinID result, err := c.GetNodeByTwinID(twinID) + fmt.Println(result) require.NoError(err) require.Equal(node, result) }) @@ -175,3 +177,46 @@ func TestGetNode(t *testing.T) { require.Equal([]Node{node}, result) }) } + +func TestGetNodeCapacityRewards(t *testing.T) { + var request int + var count int + require := require.New(t) + + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + statusCode, body := serverHandler(r, request, count, require) + w.WriteHeader(statusCode) + _, err := w.Write(body) + require.NoError(err) + count++ + })) + defer testServer.Close() + + baseURL, err := url.JoinPath(testServer.URL, "v1") + require.NoError(err) + request = newClientWithNoAccount + c, err := NewRegistrarClient(baseURL) + require.NoError(err) + + t.Run("test get node capacity rewards, status ok", func(t *testing.T) { + request = getNodeCapacityRewardsWithStatusOK + resp, err := c.GetNodeCapacityRewards(nodeID) + require.NoError(err) + require.Equal(resp, NodeCapacityReward{}) + }) + + t.Run("get node rewards for non-existing node", func(t *testing.T) { + request = getNodeCapacityRewardsWithStatusNotFound + _, err := c.GetNodeCapacityRewards(nodeID) + require.Error(err) + }) + + t.Run("No reports available, status UnprocessableEntity", func(t *testing.T) { + request = getNodeCapacityRewardsWithStatusUnprocessableEntity + res, err := c.GetNodeCapacityRewards(1) + fmt.Println(res) + fmt.Println(err.Error()) + require.Error(err) + require.Equal(res, NodeCapacityReward{TfReward: 323}) + }) +} diff --git a/node-registrar/client/utils_test.go b/node-registrar/client/utils_test.go index 80b5d1a..170e345 100644 --- a/node-registrar/client/utils_test.go +++ b/node-registrar/client/utils_test.go @@ -11,7 +11,7 @@ import ( var ( account = Account{TwinID: 1, Relays: []string{}, RMBEncKey: ""} farm = Farm{FarmID: 1, FarmName: "freeFarm", TwinID: 1} - node = Node{NodeID: 1, FarmID: farmID, TwinID: twinID} + node = Node{NodeID: 1, FarmID: farmID, TwinID: twinID, Resources: Resources{CRU: 2342}} ) const ( @@ -44,6 +44,10 @@ const ( getNodeWithTwinID listNodesInFarm + getNodeCapacityRewardsWithStatusOK + getNodeCapacityRewardsWithStatusNotFound + getNodeCapacityRewardsWithStatusUnprocessableEntity + testMnemonic = "bottom drive obey lake curtain smoke basket hold race lonely fit walk" farmID uint64 = 1 @@ -220,6 +224,24 @@ func serverHandler(r *http.Request, request, count int, require *require.Asserti require.NoError(err) return http.StatusOK, resp + case getNodeCapacityRewardsWithStatusOK: + require.Equal("/v1/nodes/1/rewards", r.URL.Path) + require.Equal(http.MethodGet, r.Method) + resp, err := json.Marshal(NodeCapacityReward{}) + require.NoError(err) + return http.StatusOK, resp + + case getNodeCapacityRewardsWithStatusNotFound: + require.Equal("/v1/nodes/1/rewards", r.URL.Path) + require.Equal(http.MethodGet, r.Method) + return http.StatusNotFound, nil + + case getNodeCapacityRewardsWithStatusUnprocessableEntity: + require.Equal("/v1/nodes/90/rewards", r.URL.Path) + require.Equal(http.MethodGet, r.Method) + resp, err := json.Marshal(NodeCapacityReward{TfReward: 239843}) + require.NoError(err) + return http.StatusUnprocessableEntity, resp // unauthorized requests case newClientWithNoAccount, getAccountWithPKStatusNotFount, @@ -230,7 +252,6 @@ func serverHandler(r *http.Request, request, count int, require *require.Asserti require.Equal(account.PublicKey, r.URL.Query().Get("public_key")) require.Equal(http.MethodGet, r.Method) return http.StatusNotFound, nil - } return http.StatusNotAcceptable, nil From 55e7ea8bc0bc4592755e9eb2114e20fdc1f8b343 Mon Sep 17 00:00:00 2001 From: Eslam-Nawara Date: Tue, 24 Jun 2025 18:33:04 +0300 Subject: [PATCH 24/43] remove extra println --- node-registrar/client/node.go | 1 - node-registrar/client/node_test.go | 9 +++------ node-registrar/client/utils_test.go | 3 ++- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/node-registrar/client/node.go b/node-registrar/client/node.go index 83e158b..c52f565 100644 --- a/node-registrar/client/node.go +++ b/node-registrar/client/node.go @@ -326,7 +326,6 @@ func (c *RegistrarClient) getNodeByTwinID(id uint64) (node Node, err error) { } func (c *RegistrarClient) getNodeCapacityRewards(nodeID uint64) (reward NodeCapacityReward, err error) { - url, err := url.JoinPath(c.baseURL, "nodes", fmt.Sprint(nodeID), "rewards") if err != nil { return reward, errors.Wrap(err, "failed to construct rewards url") diff --git a/node-registrar/client/node_test.go b/node-registrar/client/node_test.go index 19c2df5..554f232 100644 --- a/node-registrar/client/node_test.go +++ b/node-registrar/client/node_test.go @@ -2,7 +2,6 @@ package client import ( "encoding/base64" - "fmt" "net/http" "net/http/httptest" "net/url" @@ -163,7 +162,6 @@ func TestGetNode(t *testing.T) { count = 0 request = getNodeWithTwinID result, err := c.GetNodeByTwinID(twinID) - fmt.Println(result) require.NoError(err) require.Equal(node, result) }) @@ -194,6 +192,7 @@ func TestGetNodeCapacityRewards(t *testing.T) { baseURL, err := url.JoinPath(testServer.URL, "v1") require.NoError(err) + request = newClientWithNoAccount c, err := NewRegistrarClient(baseURL) require.NoError(err) @@ -211,12 +210,10 @@ func TestGetNodeCapacityRewards(t *testing.T) { require.Error(err) }) - t.Run("No reports available, status UnprocessableEntity", func(t *testing.T) { + t.Run("no reports available, status UnprocessableEntity", func(t *testing.T) { request = getNodeCapacityRewardsWithStatusUnprocessableEntity res, err := c.GetNodeCapacityRewards(1) - fmt.Println(res) - fmt.Println(err.Error()) require.Error(err) - require.Equal(res, NodeCapacityReward{TfReward: 323}) + require.Equal(res, NodeCapacityReward{}) }) } diff --git a/node-registrar/client/utils_test.go b/node-registrar/client/utils_test.go index 170e345..9ec1866 100644 --- a/node-registrar/client/utils_test.go +++ b/node-registrar/client/utils_test.go @@ -237,11 +237,12 @@ func serverHandler(r *http.Request, request, count int, require *require.Asserti return http.StatusNotFound, nil case getNodeCapacityRewardsWithStatusUnprocessableEntity: - require.Equal("/v1/nodes/90/rewards", r.URL.Path) + require.Equal("/v1/nodes/1/rewards", r.URL.Path) require.Equal(http.MethodGet, r.Method) resp, err := json.Marshal(NodeCapacityReward{TfReward: 239843}) require.NoError(err) return http.StatusUnprocessableEntity, resp + // unauthorized requests case newClientWithNoAccount, getAccountWithPKStatusNotFount, From c4f1136c97c1a07f740efdde75c223a25ec1f3e7 Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Tue, 24 Jun 2025 19:42:17 +0300 Subject: [PATCH 25/43] tests: add more test cases for the rewards --- node-registrar/client/node_test.go | 39 ++++++++++++++++++++++++++--- node-registrar/client/utils_test.go | 29 +++++++++++++++++++-- node-registrar/test/main.go | 31 +++++++++++++++++++++++ 3 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 node-registrar/test/main.go diff --git a/node-registrar/client/node_test.go b/node-registrar/client/node_test.go index 554f232..5b10e61 100644 --- a/node-registrar/client/node_test.go +++ b/node-registrar/client/node_test.go @@ -201,7 +201,7 @@ func TestGetNodeCapacityRewards(t *testing.T) { request = getNodeCapacityRewardsWithStatusOK resp, err := c.GetNodeCapacityRewards(nodeID) require.NoError(err) - require.Equal(resp, NodeCapacityReward{}) + require.Equal(NodeCapacityReward{}, resp) }) t.Run("get node rewards for non-existing node", func(t *testing.T) { @@ -212,8 +212,41 @@ func TestGetNodeCapacityRewards(t *testing.T) { t.Run("no reports available, status UnprocessableEntity", func(t *testing.T) { request = getNodeCapacityRewardsWithStatusUnprocessableEntity - res, err := c.GetNodeCapacityRewards(1) + res, err := c.GetNodeCapacityRewards(nodeID) require.Error(err) - require.Equal(res, NodeCapacityReward{}) + require.Equal(NodeCapacityReward{}, res) + + }) + + t.Run("node with partial uptime rewards calculation", func(t *testing.T) { + request = getNodeCapacityRewardsWithPartialUptime + res, err := c.GetNodeCapacityRewards(nodeID) + require.NoError(err) + expected := NodeCapacityReward{ + FarmerReward: 60.0, + TfReward: 20.0, + FpReward: 20.0, + Total: 100.0, + UpTimePercentage: 75.0, + } + require.Equal(expected, res) + // Verify reward distribution percentages are correct + require.InDelta(0.6, res.FarmerReward/res.Total, 0.001) + require.InDelta(0.2, res.TfReward/res.Total, 0.001) + require.InDelta(0.2, res.FpReward/res.Total, 0.001) + }) + + t.Run("bad request due to invalid node ID format", func(t *testing.T) { + request = getNodeCapacityRewardsWithBadRequest + res, err := c.GetNodeCapacityRewards(nodeID) + require.Error(err) + require.Equal(NodeCapacityReward{}, res) + }) + + t.Run("internal server error when calculating rewards", func(t *testing.T) { + request = getNodeCapacityRewardsWithServerError + res, err := c.GetNodeCapacityRewards(nodeID) + require.Error(err) + require.Equal(NodeCapacityReward{}, res) }) } diff --git a/node-registrar/client/utils_test.go b/node-registrar/client/utils_test.go index 9ec1866..376c005 100644 --- a/node-registrar/client/utils_test.go +++ b/node-registrar/client/utils_test.go @@ -47,6 +47,9 @@ const ( getNodeCapacityRewardsWithStatusOK getNodeCapacityRewardsWithStatusNotFound getNodeCapacityRewardsWithStatusUnprocessableEntity + getNodeCapacityRewardsWithPartialUptime + getNodeCapacityRewardsWithBadRequest + getNodeCapacityRewardsWithServerError testMnemonic = "bottom drive obey lake curtain smoke basket hold race lonely fit walk" @@ -239,9 +242,31 @@ func serverHandler(r *http.Request, request, count int, require *require.Asserti case getNodeCapacityRewardsWithStatusUnprocessableEntity: require.Equal("/v1/nodes/1/rewards", r.URL.Path) require.Equal(http.MethodGet, r.Method) - resp, err := json.Marshal(NodeCapacityReward{TfReward: 239843}) + return http.StatusUnprocessableEntity, nil + + case getNodeCapacityRewardsWithPartialUptime: + require.Equal("/v1/nodes/1/rewards", r.URL.Path) + require.Equal(http.MethodGet, r.Method) + resp, err := json.Marshal(NodeCapacityReward{ + FarmerReward: 60.0, + TfReward: 20.0, + FpReward: 20.0, + Total: 100.0, + UpTimePercentage: 75.0, + }) require.NoError(err) - return http.StatusUnprocessableEntity, resp + return http.StatusOK, resp + + case getNodeCapacityRewardsWithBadRequest: + require.Equal("/v1/nodes/1/rewards", r.URL.Path) + require.Equal(http.MethodGet, r.Method) + + return http.StatusBadRequest, nil + + case getNodeCapacityRewardsWithServerError: + require.Equal("/v1/nodes/1/rewards", r.URL.Path) + require.Equal(http.MethodGet, r.Method) + return http.StatusInternalServerError, nil // unauthorized requests case newClientWithNoAccount, diff --git a/node-registrar/test/main.go b/node-registrar/test/main.go new file mode 100644 index 0000000..fa024b6 --- /dev/null +++ b/node-registrar/test/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "fmt" + "log" + + "github.com/threefoldtech/tfgrid4-sdk-go/node-registrar/client" +) + +func main() { + // Create a new client with the registrar server URL + registrarClient, err := client.NewRegistrarClient("http://localhost:8080/api/v1") + if err != nil { + log.Fatalf("Failed to create client: %v", err) + } + + // Get node capacity rewards by node ID + nodeID := uint64(34) // Replace with actual node ID + rewards, err := registrarClient.GetNodeCapacityRewards(nodeID) + if err != nil { + log.Fatalf("Failed to get node capacity rewards: %v", err) + } + + // Print the rewards information + fmt.Printf("Node %d Rewards:\n", nodeID) + fmt.Printf("Total: %f\n", rewards.Total) + fmt.Printf("Farmer Reward: %f\n", rewards.FarmerReward) + fmt.Printf("TF Reward: %f\n", rewards.TfReward) + fmt.Printf("FP Reward: %f\n", rewards.FpReward) + fmt.Printf("Uptime Percentage: %f%%\n", rewards.UpTimePercentage) +} From 10c4a79fc42104a5417e375f8f0bad7dc1326d46 Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Tue, 24 Jun 2025 19:44:47 +0300 Subject: [PATCH 26/43] refactor: standardize reward field names and JSON tags in NodeCapacityReward struct --- node-registrar/client/node_test.go | 8 ++++---- node-registrar/client/types.go | 10 +++++----- node-registrar/client/utils_test.go | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/node-registrar/client/node_test.go b/node-registrar/client/node_test.go index 5b10e61..4b49883 100644 --- a/node-registrar/client/node_test.go +++ b/node-registrar/client/node_test.go @@ -224,16 +224,16 @@ func TestGetNodeCapacityRewards(t *testing.T) { require.NoError(err) expected := NodeCapacityReward{ FarmerReward: 60.0, - TfReward: 20.0, - FpReward: 20.0, + TFReward: 20.0, + FPReward: 20.0, Total: 100.0, UpTimePercentage: 75.0, } require.Equal(expected, res) // Verify reward distribution percentages are correct require.InDelta(0.6, res.FarmerReward/res.Total, 0.001) - require.InDelta(0.2, res.TfReward/res.Total, 0.001) - require.InDelta(0.2, res.FpReward/res.Total, 0.001) + require.InDelta(0.2, res.TFReward/res.Total, 0.001) + require.InDelta(0.2, res.FPReward/res.Total, 0.001) }) t.Run("bad request due to invalid node ID format", func(t *testing.T) { diff --git a/node-registrar/client/types.go b/node-registrar/client/types.go index 037b363..7717f6c 100644 --- a/node-registrar/client/types.go +++ b/node-registrar/client/types.go @@ -66,9 +66,9 @@ type Location struct { } type NodeCapacityReward struct { - FarmerReward float64 `json:"farmerReward"` // Reward amount for the node owner (60%) - TfReward float64 `json:"tfReward"` // Reward amount for Threefold Foundation (20%) - FpReward float64 `json:"fpReward"` // Reward amount for the Farming Pool (20%) - Total float64 `json:"total"` // Total reward amount - UpTimePercentage float64 `json:"uptimePercentage"` // Node's uptime percentage + FarmerReward float64 `json:"FarmerReward"` // Reward amount for the node owner (60%) + TFReward float64 `json:"TFReward"` // Reward amount for Threefold Foundation (20%) + FPReward float64 `json:"FPReward"` // Reward amount for the Farming Pool (20%) + Total float64 `json:"Total"` // Total reward amount + UpTimePercentage float64 `json:"UptimePercentage"` // Node's uptime percentage } diff --git a/node-registrar/client/utils_test.go b/node-registrar/client/utils_test.go index 376c005..b1898ab 100644 --- a/node-registrar/client/utils_test.go +++ b/node-registrar/client/utils_test.go @@ -249,8 +249,8 @@ func serverHandler(r *http.Request, request, count int, require *require.Asserti require.Equal(http.MethodGet, r.Method) resp, err := json.Marshal(NodeCapacityReward{ FarmerReward: 60.0, - TfReward: 20.0, - FpReward: 20.0, + TFReward: 20.0, + FPReward: 20.0, Total: 100.0, UpTimePercentage: 75.0, }) From e5e481ca887d8bdf526f0348719aaeda7372714e Mon Sep 17 00:00:00 2001 From: Eslam-Nawara Date: Tue, 24 Jun 2025 18:11:28 +0300 Subject: [PATCH 27/43] fix node tests in client --- node-registrar/client/node_test.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/node-registrar/client/node_test.go b/node-registrar/client/node_test.go index b59d356..15c8d44 100644 --- a/node-registrar/client/node_test.go +++ b/node-registrar/client/node_test.go @@ -1,6 +1,7 @@ package client import ( + "encoding/base64" "net/http" "net/http/httptest" "net/url" @@ -20,6 +21,10 @@ func TestRegistarNode(t *testing.T) { FarmID: farmID, } + keyPair, err := parseKeysFromMnemonicOrSeed(testMnemonic) + require.NoError(err) + account.PublicKey = base64.StdEncoding.EncodeToString(keyPair.Public()) + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { statusCode, body := serverHandler(r, request, count, require) w.WriteHeader(statusCode) @@ -73,10 +78,10 @@ func TestUpdateNode(t *testing.T) { var count int require := require.New(t) - // publicKey, privateKey, err := aliceKeys() - // require.NoError(err) - // account.PublicKey = base64.StdEncoding.EncodeToString(publicKey) - // + keyPair, err := parseKeysFromMnemonicOrSeed(testMnemonic) + require.NoError(err) + account.PublicKey = base64.StdEncoding.EncodeToString(keyPair.Public()) + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { statusCode, body := serverHandler(r, request, count, require) w.WriteHeader(statusCode) @@ -118,9 +123,9 @@ func TestGetNode(t *testing.T) { var count int require := require.New(t) - // publicKey, privateKey, err := aliceKeys() - // require.NoError(err) - // account.PublicKey = base64.StdEncoding.EncodeToString(publicKey) + keyPair, err := parseKeysFromMnemonicOrSeed(testMnemonic) + require.NoError(err) + account.PublicKey = base64.StdEncoding.EncodeToString(keyPair.Public()) testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { statusCode, body := serverHandler(r, request, count, require) From 2465abcb1b5b2e88aec52e918ce1d962b4cde36c Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Tue, 24 Jun 2025 18:19:37 +0300 Subject: [PATCH 28/43] WIP: add rewards tests in client --- node-registrar/client/node_test.go | 45 +++++++++++++++++++++++++++++ node-registrar/client/utils_test.go | 25 ++++++++++++++-- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/node-registrar/client/node_test.go b/node-registrar/client/node_test.go index 15c8d44..ac8d663 100644 --- a/node-registrar/client/node_test.go +++ b/node-registrar/client/node_test.go @@ -2,6 +2,7 @@ package client import ( "encoding/base64" + "fmt" "net/http" "net/http/httptest" "net/url" @@ -159,6 +160,7 @@ func TestGetNode(t *testing.T) { t.Run("test get node with twin id", func(t *testing.T) { request = getNodeWithTwinID result, err := c.GetNodeByTwinID(twinID) + fmt.Println(result) require.NoError(err) require.Equal(node, result) }) @@ -171,3 +173,46 @@ func TestGetNode(t *testing.T) { require.Equal([]Node{node}, result) }) } + +func TestGetNodeCapacityRewards(t *testing.T) { + var request int + var count int + require := require.New(t) + + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + statusCode, body := serverHandler(r, request, count, require) + w.WriteHeader(statusCode) + _, err := w.Write(body) + require.NoError(err) + count++ + })) + defer testServer.Close() + + baseURL, err := url.JoinPath(testServer.URL, "v1") + require.NoError(err) + request = newClientWithNoAccount + c, err := NewRegistrarClient(baseURL) + require.NoError(err) + + t.Run("test get node capacity rewards, status ok", func(t *testing.T) { + request = getNodeCapacityRewardsWithStatusOK + resp, err := c.GetNodeCapacityRewards(nodeID) + require.NoError(err) + require.Equal(resp, NodeCapacityReward{}) + }) + + t.Run("get node rewards for non-existing node", func(t *testing.T) { + request = getNodeCapacityRewardsWithStatusNotFound + _, err := c.GetNodeCapacityRewards(nodeID) + require.Error(err) + }) + + t.Run("No reports available, status UnprocessableEntity", func(t *testing.T) { + request = getNodeCapacityRewardsWithStatusUnprocessableEntity + res, err := c.GetNodeCapacityRewards(1) + fmt.Println(res) + fmt.Println(err.Error()) + require.Error(err) + require.Equal(res, NodeCapacityReward{TfReward: 323}) + }) +} diff --git a/node-registrar/client/utils_test.go b/node-registrar/client/utils_test.go index 80b5d1a..170e345 100644 --- a/node-registrar/client/utils_test.go +++ b/node-registrar/client/utils_test.go @@ -11,7 +11,7 @@ import ( var ( account = Account{TwinID: 1, Relays: []string{}, RMBEncKey: ""} farm = Farm{FarmID: 1, FarmName: "freeFarm", TwinID: 1} - node = Node{NodeID: 1, FarmID: farmID, TwinID: twinID} + node = Node{NodeID: 1, FarmID: farmID, TwinID: twinID, Resources: Resources{CRU: 2342}} ) const ( @@ -44,6 +44,10 @@ const ( getNodeWithTwinID listNodesInFarm + getNodeCapacityRewardsWithStatusOK + getNodeCapacityRewardsWithStatusNotFound + getNodeCapacityRewardsWithStatusUnprocessableEntity + testMnemonic = "bottom drive obey lake curtain smoke basket hold race lonely fit walk" farmID uint64 = 1 @@ -220,6 +224,24 @@ func serverHandler(r *http.Request, request, count int, require *require.Asserti require.NoError(err) return http.StatusOK, resp + case getNodeCapacityRewardsWithStatusOK: + require.Equal("/v1/nodes/1/rewards", r.URL.Path) + require.Equal(http.MethodGet, r.Method) + resp, err := json.Marshal(NodeCapacityReward{}) + require.NoError(err) + return http.StatusOK, resp + + case getNodeCapacityRewardsWithStatusNotFound: + require.Equal("/v1/nodes/1/rewards", r.URL.Path) + require.Equal(http.MethodGet, r.Method) + return http.StatusNotFound, nil + + case getNodeCapacityRewardsWithStatusUnprocessableEntity: + require.Equal("/v1/nodes/90/rewards", r.URL.Path) + require.Equal(http.MethodGet, r.Method) + resp, err := json.Marshal(NodeCapacityReward{TfReward: 239843}) + require.NoError(err) + return http.StatusUnprocessableEntity, resp // unauthorized requests case newClientWithNoAccount, getAccountWithPKStatusNotFount, @@ -230,7 +252,6 @@ func serverHandler(r *http.Request, request, count int, require *require.Asserti require.Equal(account.PublicKey, r.URL.Query().Get("public_key")) require.Equal(http.MethodGet, r.Method) return http.StatusNotFound, nil - } return http.StatusNotAcceptable, nil From 2b5f9645f0dc29c4fb2d56a895b33a07bc3f9eba Mon Sep 17 00:00:00 2001 From: Eslam-Nawara Date: Tue, 24 Jun 2025 18:33:04 +0300 Subject: [PATCH 29/43] remove extra println --- node-registrar/client/node.go | 1 - node-registrar/client/node_test.go | 9 +++------ node-registrar/client/utils_test.go | 3 ++- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/node-registrar/client/node.go b/node-registrar/client/node.go index 83e158b..c52f565 100644 --- a/node-registrar/client/node.go +++ b/node-registrar/client/node.go @@ -326,7 +326,6 @@ func (c *RegistrarClient) getNodeByTwinID(id uint64) (node Node, err error) { } func (c *RegistrarClient) getNodeCapacityRewards(nodeID uint64) (reward NodeCapacityReward, err error) { - url, err := url.JoinPath(c.baseURL, "nodes", fmt.Sprint(nodeID), "rewards") if err != nil { return reward, errors.Wrap(err, "failed to construct rewards url") diff --git a/node-registrar/client/node_test.go b/node-registrar/client/node_test.go index ac8d663..b1ad3c6 100644 --- a/node-registrar/client/node_test.go +++ b/node-registrar/client/node_test.go @@ -2,7 +2,6 @@ package client import ( "encoding/base64" - "fmt" "net/http" "net/http/httptest" "net/url" @@ -160,7 +159,6 @@ func TestGetNode(t *testing.T) { t.Run("test get node with twin id", func(t *testing.T) { request = getNodeWithTwinID result, err := c.GetNodeByTwinID(twinID) - fmt.Println(result) require.NoError(err) require.Equal(node, result) }) @@ -190,6 +188,7 @@ func TestGetNodeCapacityRewards(t *testing.T) { baseURL, err := url.JoinPath(testServer.URL, "v1") require.NoError(err) + request = newClientWithNoAccount c, err := NewRegistrarClient(baseURL) require.NoError(err) @@ -207,12 +206,10 @@ func TestGetNodeCapacityRewards(t *testing.T) { require.Error(err) }) - t.Run("No reports available, status UnprocessableEntity", func(t *testing.T) { + t.Run("no reports available, status UnprocessableEntity", func(t *testing.T) { request = getNodeCapacityRewardsWithStatusUnprocessableEntity res, err := c.GetNodeCapacityRewards(1) - fmt.Println(res) - fmt.Println(err.Error()) require.Error(err) - require.Equal(res, NodeCapacityReward{TfReward: 323}) + require.Equal(res, NodeCapacityReward{}) }) } diff --git a/node-registrar/client/utils_test.go b/node-registrar/client/utils_test.go index 170e345..9ec1866 100644 --- a/node-registrar/client/utils_test.go +++ b/node-registrar/client/utils_test.go @@ -237,11 +237,12 @@ func serverHandler(r *http.Request, request, count int, require *require.Asserti return http.StatusNotFound, nil case getNodeCapacityRewardsWithStatusUnprocessableEntity: - require.Equal("/v1/nodes/90/rewards", r.URL.Path) + require.Equal("/v1/nodes/1/rewards", r.URL.Path) require.Equal(http.MethodGet, r.Method) resp, err := json.Marshal(NodeCapacityReward{TfReward: 239843}) require.NoError(err) return http.StatusUnprocessableEntity, resp + // unauthorized requests case newClientWithNoAccount, getAccountWithPKStatusNotFount, From 02e4c780bf8a9d2a3af35361fb915328843e54a2 Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Tue, 24 Jun 2025 19:42:17 +0300 Subject: [PATCH 30/43] tests: add more test cases for the rewards --- node-registrar/client/node_test.go | 39 ++++++++++++++++++++++++++--- node-registrar/client/utils_test.go | 29 +++++++++++++++++++-- node-registrar/test/main.go | 31 +++++++++++++++++++++++ 3 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 node-registrar/test/main.go diff --git a/node-registrar/client/node_test.go b/node-registrar/client/node_test.go index b1ad3c6..46ef167 100644 --- a/node-registrar/client/node_test.go +++ b/node-registrar/client/node_test.go @@ -197,7 +197,7 @@ func TestGetNodeCapacityRewards(t *testing.T) { request = getNodeCapacityRewardsWithStatusOK resp, err := c.GetNodeCapacityRewards(nodeID) require.NoError(err) - require.Equal(resp, NodeCapacityReward{}) + require.Equal(NodeCapacityReward{}, resp) }) t.Run("get node rewards for non-existing node", func(t *testing.T) { @@ -208,8 +208,41 @@ func TestGetNodeCapacityRewards(t *testing.T) { t.Run("no reports available, status UnprocessableEntity", func(t *testing.T) { request = getNodeCapacityRewardsWithStatusUnprocessableEntity - res, err := c.GetNodeCapacityRewards(1) + res, err := c.GetNodeCapacityRewards(nodeID) require.Error(err) - require.Equal(res, NodeCapacityReward{}) + require.Equal(NodeCapacityReward{}, res) + + }) + + t.Run("node with partial uptime rewards calculation", func(t *testing.T) { + request = getNodeCapacityRewardsWithPartialUptime + res, err := c.GetNodeCapacityRewards(nodeID) + require.NoError(err) + expected := NodeCapacityReward{ + FarmerReward: 60.0, + TfReward: 20.0, + FpReward: 20.0, + Total: 100.0, + UpTimePercentage: 75.0, + } + require.Equal(expected, res) + // Verify reward distribution percentages are correct + require.InDelta(0.6, res.FarmerReward/res.Total, 0.001) + require.InDelta(0.2, res.TfReward/res.Total, 0.001) + require.InDelta(0.2, res.FpReward/res.Total, 0.001) + }) + + t.Run("bad request due to invalid node ID format", func(t *testing.T) { + request = getNodeCapacityRewardsWithBadRequest + res, err := c.GetNodeCapacityRewards(nodeID) + require.Error(err) + require.Equal(NodeCapacityReward{}, res) + }) + + t.Run("internal server error when calculating rewards", func(t *testing.T) { + request = getNodeCapacityRewardsWithServerError + res, err := c.GetNodeCapacityRewards(nodeID) + require.Error(err) + require.Equal(NodeCapacityReward{}, res) }) } diff --git a/node-registrar/client/utils_test.go b/node-registrar/client/utils_test.go index 9ec1866..376c005 100644 --- a/node-registrar/client/utils_test.go +++ b/node-registrar/client/utils_test.go @@ -47,6 +47,9 @@ const ( getNodeCapacityRewardsWithStatusOK getNodeCapacityRewardsWithStatusNotFound getNodeCapacityRewardsWithStatusUnprocessableEntity + getNodeCapacityRewardsWithPartialUptime + getNodeCapacityRewardsWithBadRequest + getNodeCapacityRewardsWithServerError testMnemonic = "bottom drive obey lake curtain smoke basket hold race lonely fit walk" @@ -239,9 +242,31 @@ func serverHandler(r *http.Request, request, count int, require *require.Asserti case getNodeCapacityRewardsWithStatusUnprocessableEntity: require.Equal("/v1/nodes/1/rewards", r.URL.Path) require.Equal(http.MethodGet, r.Method) - resp, err := json.Marshal(NodeCapacityReward{TfReward: 239843}) + return http.StatusUnprocessableEntity, nil + + case getNodeCapacityRewardsWithPartialUptime: + require.Equal("/v1/nodes/1/rewards", r.URL.Path) + require.Equal(http.MethodGet, r.Method) + resp, err := json.Marshal(NodeCapacityReward{ + FarmerReward: 60.0, + TfReward: 20.0, + FpReward: 20.0, + Total: 100.0, + UpTimePercentage: 75.0, + }) require.NoError(err) - return http.StatusUnprocessableEntity, resp + return http.StatusOK, resp + + case getNodeCapacityRewardsWithBadRequest: + require.Equal("/v1/nodes/1/rewards", r.URL.Path) + require.Equal(http.MethodGet, r.Method) + + return http.StatusBadRequest, nil + + case getNodeCapacityRewardsWithServerError: + require.Equal("/v1/nodes/1/rewards", r.URL.Path) + require.Equal(http.MethodGet, r.Method) + return http.StatusInternalServerError, nil // unauthorized requests case newClientWithNoAccount, diff --git a/node-registrar/test/main.go b/node-registrar/test/main.go new file mode 100644 index 0000000..fa024b6 --- /dev/null +++ b/node-registrar/test/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "fmt" + "log" + + "github.com/threefoldtech/tfgrid4-sdk-go/node-registrar/client" +) + +func main() { + // Create a new client with the registrar server URL + registrarClient, err := client.NewRegistrarClient("http://localhost:8080/api/v1") + if err != nil { + log.Fatalf("Failed to create client: %v", err) + } + + // Get node capacity rewards by node ID + nodeID := uint64(34) // Replace with actual node ID + rewards, err := registrarClient.GetNodeCapacityRewards(nodeID) + if err != nil { + log.Fatalf("Failed to get node capacity rewards: %v", err) + } + + // Print the rewards information + fmt.Printf("Node %d Rewards:\n", nodeID) + fmt.Printf("Total: %f\n", rewards.Total) + fmt.Printf("Farmer Reward: %f\n", rewards.FarmerReward) + fmt.Printf("TF Reward: %f\n", rewards.TfReward) + fmt.Printf("FP Reward: %f\n", rewards.FpReward) + fmt.Printf("Uptime Percentage: %f%%\n", rewards.UpTimePercentage) +} From 6646f651a0b05d1dfd2d96044aa34010a0a0b5d4 Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Tue, 24 Jun 2025 19:44:47 +0300 Subject: [PATCH 31/43] refactor: standardize reward field names and JSON tags in NodeCapacityReward struct --- node-registrar/client/node_test.go | 8 ++++---- node-registrar/client/types.go | 10 +++++----- node-registrar/client/utils_test.go | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/node-registrar/client/node_test.go b/node-registrar/client/node_test.go index 46ef167..ab872c4 100644 --- a/node-registrar/client/node_test.go +++ b/node-registrar/client/node_test.go @@ -220,16 +220,16 @@ func TestGetNodeCapacityRewards(t *testing.T) { require.NoError(err) expected := NodeCapacityReward{ FarmerReward: 60.0, - TfReward: 20.0, - FpReward: 20.0, + TFReward: 20.0, + FPReward: 20.0, Total: 100.0, UpTimePercentage: 75.0, } require.Equal(expected, res) // Verify reward distribution percentages are correct require.InDelta(0.6, res.FarmerReward/res.Total, 0.001) - require.InDelta(0.2, res.TfReward/res.Total, 0.001) - require.InDelta(0.2, res.FpReward/res.Total, 0.001) + require.InDelta(0.2, res.TFReward/res.Total, 0.001) + require.InDelta(0.2, res.FPReward/res.Total, 0.001) }) t.Run("bad request due to invalid node ID format", func(t *testing.T) { diff --git a/node-registrar/client/types.go b/node-registrar/client/types.go index 037b363..7717f6c 100644 --- a/node-registrar/client/types.go +++ b/node-registrar/client/types.go @@ -66,9 +66,9 @@ type Location struct { } type NodeCapacityReward struct { - FarmerReward float64 `json:"farmerReward"` // Reward amount for the node owner (60%) - TfReward float64 `json:"tfReward"` // Reward amount for Threefold Foundation (20%) - FpReward float64 `json:"fpReward"` // Reward amount for the Farming Pool (20%) - Total float64 `json:"total"` // Total reward amount - UpTimePercentage float64 `json:"uptimePercentage"` // Node's uptime percentage + FarmerReward float64 `json:"FarmerReward"` // Reward amount for the node owner (60%) + TFReward float64 `json:"TFReward"` // Reward amount for Threefold Foundation (20%) + FPReward float64 `json:"FPReward"` // Reward amount for the Farming Pool (20%) + Total float64 `json:"Total"` // Total reward amount + UpTimePercentage float64 `json:"UptimePercentage"` // Node's uptime percentage } diff --git a/node-registrar/client/utils_test.go b/node-registrar/client/utils_test.go index 376c005..b1898ab 100644 --- a/node-registrar/client/utils_test.go +++ b/node-registrar/client/utils_test.go @@ -249,8 +249,8 @@ func serverHandler(r *http.Request, request, count int, require *require.Asserti require.Equal(http.MethodGet, r.Method) resp, err := json.Marshal(NodeCapacityReward{ FarmerReward: 60.0, - TfReward: 20.0, - FpReward: 20.0, + TFReward: 20.0, + FPReward: 20.0, Total: 100.0, UpTimePercentage: 75.0, }) From d0ce507c6917569bfe5d489596873498b78ff59c Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Mon, 30 Jun 2025 12:49:27 +0300 Subject: [PATCH 32/43] revert: remove the test file --- node-registrar/test/main.go | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 node-registrar/test/main.go diff --git a/node-registrar/test/main.go b/node-registrar/test/main.go deleted file mode 100644 index fa024b6..0000000 --- a/node-registrar/test/main.go +++ /dev/null @@ -1,31 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "github.com/threefoldtech/tfgrid4-sdk-go/node-registrar/client" -) - -func main() { - // Create a new client with the registrar server URL - registrarClient, err := client.NewRegistrarClient("http://localhost:8080/api/v1") - if err != nil { - log.Fatalf("Failed to create client: %v", err) - } - - // Get node capacity rewards by node ID - nodeID := uint64(34) // Replace with actual node ID - rewards, err := registrarClient.GetNodeCapacityRewards(nodeID) - if err != nil { - log.Fatalf("Failed to get node capacity rewards: %v", err) - } - - // Print the rewards information - fmt.Printf("Node %d Rewards:\n", nodeID) - fmt.Printf("Total: %f\n", rewards.Total) - fmt.Printf("Farmer Reward: %f\n", rewards.FarmerReward) - fmt.Printf("TF Reward: %f\n", rewards.TfReward) - fmt.Printf("FP Reward: %f\n", rewards.FpReward) - fmt.Printf("Uptime Percentage: %f%%\n", rewards.UpTimePercentage) -} From ff3487fa3cdf659986a04a3448940c77096e7dc7 Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Mon, 30 Jun 2025 12:55:02 +0300 Subject: [PATCH 33/43] fix: use errors.Is() for proper error comparison in uptime calculation handler --- node-registrar/pkg/server/handlers.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node-registrar/pkg/server/handlers.go b/node-registrar/pkg/server/handlers.go index 620cdbf..9a8c041 100644 --- a/node-registrar/pkg/server/handlers.go +++ b/node-registrar/pkg/server/handlers.go @@ -324,11 +324,11 @@ func (s Server) getNodeRewardHandler(c *gin.Context) { upTimePercentage, err := calculateUpTimePercentage(reports, periodStart, now) if err != nil { - if err == ErrReportsNotInAscOrder { + if errors.Is(err, ErrReportsNotInAscOrder) { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - if err == ErrNoReportsAvailable { + if errors.Is(err, ErrNoReportsAvailable) { c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) return } From b407b77c5b27d4741186066199ce98c492bf71b6 Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Mon, 30 Jun 2025 14:25:22 +0300 Subject: [PATCH 34/43] refactor: replace floating point delta comparisons with exact truncated value checks in rewards tests --- node-registrar/pkg/server/rewards_test.go | 70 +++++++++++++---------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/node-registrar/pkg/server/rewards_test.go b/node-registrar/pkg/server/rewards_test.go index 251e546..38cbbc2 100644 --- a/node-registrar/pkg/server/rewards_test.go +++ b/node-registrar/pkg/server/rewards_test.go @@ -138,21 +138,20 @@ func AssertCapacityReward(t testing.TB, resources db.Resources, upTimePercentage // Apply uptime percentage total = total * (upTimePercentage / 100) + // Apply truncation to match the implementation expected := Reward{ - FarmerReward: total * FarmerRewardPercentage, - TfReward: total * TfRewardPercentage, - FpReward: total * FpRewardPercentage, - Total: total, + FarmerReward: truncateFloat(total*FarmerRewardPercentage, RewardPrecisionDecimalPlaces), + TfReward: truncateFloat(total*TfRewardPercentage, RewardPrecisionDecimalPlaces), + FpReward: truncateFloat(total*FpRewardPercentage, RewardPrecisionDecimalPlaces), + Total: truncateFloat(total, RewardPrecisionDecimalPlaces), UpTimePercentage: upTimePercentage, } - // Use precise floating point comparison - const delta = 1e-9 // Very small acceptable difference - assert.InDelta(t, expected.FarmerReward, got.FarmerReward, delta) - assert.InDelta(t, expected.TfReward, got.TfReward, delta) - assert.InDelta(t, expected.FpReward, got.FpReward, delta) - assert.InDelta(t, expected.Total, got.Total, delta) - assert.InDelta(t, expected.UpTimePercentage, got.UpTimePercentage, delta) + assert.Equal(t, expected.FarmerReward, got.FarmerReward) + assert.Equal(t, expected.TfReward, got.TfReward) + assert.Equal(t, expected.FpReward, got.FpReward) + assert.Equal(t, expected.Total, got.Total) + assert.Equal(t, expected.UpTimePercentage, got.UpTimePercentage) } // TestCalculatePeriodStart tests the calculatePeriodStart function with different inputs @@ -262,8 +261,8 @@ func TestCalculateTotalReward(t *testing.T) { t.Run(tt.name, func(t *testing.T) { result := calculateTotalReward(tt.capacity, tt.upTimePercentage) - // Use a small delta for floating point comparison - assert.InDelta(t, tt.expected, result, 0.001, "Total reward calculation incorrect") + truncatedExpected := truncateFloat(tt.expected, RewardPrecisionDecimalPlaces) + assert.Equal(t, truncatedExpected, result, "Total reward calculation incorrect") }) } } @@ -331,8 +330,8 @@ func TestCalculateBaseCapacityReward(t *testing.T) { t.Run(tt.name, func(t *testing.T) { result := calculateBaseCapacityReward(tt.capacity) - // Use a small delta for floating point comparison - assert.InDelta(t, tt.expected, result, 0.001, "Base capacity reward calculation incorrect") + truncatedExpected := truncateFloat(tt.expected, RewardPrecisionDecimalPlaces) + assert.Equal(t, truncatedExpected, result, "Base capacity reward calculation incorrect") }) } } @@ -363,33 +362,40 @@ func TestRewardFloatingPointPrecision(t *testing.T) { // Test precision and distribution t.Run("base reward calculation", func(t *testing.T) { - assert.InDelta(t, expectedBaseReward, calculateBaseCapacityReward(memoryOnlyCapacity), 0.001) + expectedTruncated := truncateFloat(expectedBaseReward, RewardPrecisionDecimalPlaces) + actualResult := calculateBaseCapacityReward(memoryOnlyCapacity) + assert.Equal(t, expectedTruncated, actualResult) }) t.Run("total reward", func(t *testing.T) { - assert.InDelta(t, expectedTotal, reward.Total, 0.001) + assert.Equal(t, truncateFloat(expectedTotal, RewardPrecisionDecimalPlaces), reward.Total) }) // Test the distribution percentages t.Run("farmer reward percentage", func(t *testing.T) { - assert.InDelta(t, expectedFarmerReward, reward.FarmerReward, 0.001) - assert.InDelta(t, 0.6, reward.FarmerReward/reward.Total, 0.001, "Farmer reward should be 60% of total") + assert.Equal(t, truncateFloat(expectedFarmerReward, RewardPrecisionDecimalPlaces), reward.FarmerReward) + ratio := truncateFloat(reward.FarmerReward/reward.Total, 3) + assert.Equal(t, 0.6, ratio, "Farmer reward should be 60% of total") }) t.Run("tf reward percentage", func(t *testing.T) { - assert.InDelta(t, expectedTfReward, reward.TfReward, 0.001) - assert.InDelta(t, 0.2, reward.TfReward/reward.Total, 0.001, "TF reward should be 20% of total") + assert.Equal(t, truncateFloat(expectedTfReward, RewardPrecisionDecimalPlaces), reward.TfReward) + ratio := truncateFloat(reward.TfReward/reward.Total, 3) + assert.Equal(t, 0.2, ratio, "TF reward should be 20% of total") }) t.Run("fp reward percentage", func(t *testing.T) { - assert.InDelta(t, expectedFpReward, reward.FpReward, 0.001) - assert.InDelta(t, 0.2, reward.FpReward/reward.Total, 0.001, "FP reward should be 20% of total") + assert.Equal(t, truncateFloat(expectedFpReward, RewardPrecisionDecimalPlaces), reward.FpReward) + ratio := truncateFloat(reward.FpReward/reward.Total, 3) + assert.Equal(t, 0.2, ratio, "FP reward should be 20% of total") }) // Test that sum of portions equals total (within rounding error) t.Run("reward portions sum to total", func(t *testing.T) { actualSum := reward.FarmerReward + reward.TfReward + reward.FpReward - assert.InDelta(t, reward.Total, actualSum, 0.001, "Sum of reward portions should equal total reward") + totalTruncated := truncateFloat(reward.Total, RewardPrecisionDecimalPlaces) + sumTruncated := truncateFloat(actualSum, RewardPrecisionDecimalPlaces) + assert.Equal(t, totalTruncated, sumTruncated, "Sum of reward portions should equal total reward") }) } @@ -622,12 +628,14 @@ func TestHelperFunctions(t *testing.T) { t.Run("1 GB", func(t *testing.T) { result := bytesToGB(1073741824) // 1 GB in bytes (2^30) - assert.InDelta(t, 1.0, result, 0.001, "1073741824 bytes should convert to 1 GB") + truncatedResult := truncateFloat(result, 3) + assert.Equal(t, 1.0, truncatedResult, "1073741824 bytes should convert to 1 GB") }) t.Run("1.5 GB", func(t *testing.T) { result := bytesToGB(1610612736) // 1.5 GB in bytes - assert.InDelta(t, 1.5, result, 0.001, "1610612736 bytes should convert to 1.5 GB") + truncatedResult := truncateFloat(result, 3) + assert.Equal(t, 1.5, truncatedResult, "1610612736 bytes should convert to 1.5 GB") }) }) @@ -640,12 +648,14 @@ func TestHelperFunctions(t *testing.T) { t.Run("1 TB", func(t *testing.T) { result := bytesToTB(1099511627776) // 1 TB in bytes (2^40) - assert.InDelta(t, 1.0, result, 0.001, "1099511627776 bytes should convert to 1 TB") + truncatedResult := truncateFloat(result, 3) + assert.Equal(t, 1.0, truncatedResult, "1099511627776 bytes should convert to 1 TB") }) t.Run("2.5 TB", func(t *testing.T) { result := bytesToTB(2748779069440) // 2.5 TB in bytes - assert.InDelta(t, 2.5, result, 0.001, "2748779069440 bytes should convert to 2.5 TB") + truncatedResult := truncateFloat(result, 3) + assert.Equal(t, 2.5, truncatedResult, "2748779069440 bytes should convert to 2.5 TB") }) }) @@ -655,12 +665,12 @@ func TestHelperFunctions(t *testing.T) { result := truncateFloat(123.456789, 2) assert.Equal(t, 123.45, result, "123.456789 truncated to 2 decimal places should be 123.45") }) - + t.Run("truncate 124", func(t *testing.T) { result := truncateFloat(124, 0) assert.Equal(t, 124.0, result, "124 truncated to 0 decimal places should be 124.0") }) - + t.Run("truncate to 0 places", func(t *testing.T) { result := truncateFloat(123.456789, 0) assert.Equal(t, 123.0, result, "123.456789 truncated to 0 decimal places should be 123.0") From cdd305fb2fe01f50125fd344a4286aaebc45d632 Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Mon, 30 Jun 2025 14:28:15 +0300 Subject: [PATCH 35/43] test: replace InDelta with Equal for reward distribution percentage checks --- node-registrar/client/node_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/node-registrar/client/node_test.go b/node-registrar/client/node_test.go index 4b49883..c75a6ba 100644 --- a/node-registrar/client/node_test.go +++ b/node-registrar/client/node_test.go @@ -231,9 +231,9 @@ func TestGetNodeCapacityRewards(t *testing.T) { } require.Equal(expected, res) // Verify reward distribution percentages are correct - require.InDelta(0.6, res.FarmerReward/res.Total, 0.001) - require.InDelta(0.2, res.TFReward/res.Total, 0.001) - require.InDelta(0.2, res.FPReward/res.Total, 0.001) + require.Equal(0.6, res.FarmerReward/res.Total) + require.Equal(0.2, res.TFReward/res.Total) + require.Equal(0.2, res.FPReward/res.Total) }) t.Run("bad request due to invalid node ID format", func(t *testing.T) { From 6a71bc3a85334f4d3d358fa91b81a2b7e71bbca1 Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Mon, 30 Jun 2025 14:46:48 +0300 Subject: [PATCH 36/43] refactor: simplify test assertions using require and assert packages --- node-registrar/pkg/server/rewards_test.go | 28 ++++++----------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/node-registrar/pkg/server/rewards_test.go b/node-registrar/pkg/server/rewards_test.go index 38cbbc2..e38df43 100644 --- a/node-registrar/pkg/server/rewards_test.go +++ b/node-registrar/pkg/server/rewards_test.go @@ -2,7 +2,6 @@ package server import ( "fmt" - "math" "testing" "time" @@ -99,17 +98,13 @@ func TestCalculateCapacityReward(t *testing.T) { // Error check if tt.wantError { - if err == nil { - t.Fatal("expected error, got nil") - } + require.Error(t, err) assert.Equal(t, tt.expectedError, err) return } // No error expected - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + require.NoError(t, err) // Check reward calculation AssertCapacityReward(t, tt.capacity, tt.upTimePercentage, got) @@ -821,24 +816,15 @@ func TestCalculateUpTimePercentage(t *testing.T) { // Check for expected error if tt.wantError { - if err == nil { - t.Errorf("calculateUpTimePercentage() expected error, got nil") - } - // Also check for specific error type - if tt.expectedError != nil { - assert.Equal(t, tt.expectedError, err, "Expected specific error type") - } + require.Error(t, err) + require.Equal(t, tt.expectedError, err, "Expected specific error type") return } // No error expected - if err != nil { - t.Errorf("calculateUpTimePercentage() unexpected error: %v", err) - return - } - if math.Abs(got-tt.expected) > 0.01 { - t.Errorf("calculateUpTimePercentage() = %v, want %v", got, tt.expected) - } + require.NoError(t, err) + + assert.Equal(t, tt.expected, got) }) } } From 2ae9121a4d4fce5ca0f5e0724721c3b0a4fdb516 Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Mon, 30 Jun 2025 14:53:16 +0300 Subject: [PATCH 37/43] fix: handle nil response in node rewards request --- node-registrar/client/node.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/node-registrar/client/node.go b/node-registrar/client/node.go index c52f565..9f6539d 100644 --- a/node-registrar/client/node.go +++ b/node-registrar/client/node.go @@ -335,6 +335,10 @@ func (c *RegistrarClient) getNodeCapacityRewards(nodeID uint64) (reward NodeCapa if err != nil { return reward, errors.Wrap(err, "failed to send request to rewards endpoint") } + if resp == nil { + return reward, errors.New("no response received") + } + defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return reward, ErrorNodeNotFound From 311f49d7d7f6c654ad2a6e8701564cbbe70242ed Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Mon, 30 Jun 2025 14:57:06 +0300 Subject: [PATCH 38/43] refactor: simplify error handling in node rewards and uptime calculations --- node-registrar/client/node.go | 4 ---- node-registrar/pkg/server/handlers.go | 4 ---- 2 files changed, 8 deletions(-) diff --git a/node-registrar/client/node.go b/node-registrar/client/node.go index 9f6539d..b0c8060 100644 --- a/node-registrar/client/node.go +++ b/node-registrar/client/node.go @@ -344,10 +344,6 @@ func (c *RegistrarClient) getNodeCapacityRewards(nodeID uint64) (reward NodeCapa return reward, ErrorNodeNotFound } - if resp.StatusCode == http.StatusUnprocessableEntity { - return reward, parseResponseError(resp.Body) - } - if resp.StatusCode != http.StatusOK { err = parseResponseError(resp.Body) return reward, errors.Wrap(err, "failed to get node rewards") diff --git a/node-registrar/pkg/server/handlers.go b/node-registrar/pkg/server/handlers.go index 9a8c041..6bad742 100644 --- a/node-registrar/pkg/server/handlers.go +++ b/node-registrar/pkg/server/handlers.go @@ -324,10 +324,6 @@ func (s Server) getNodeRewardHandler(c *gin.Context) { upTimePercentage, err := calculateUpTimePercentage(reports, periodStart, now) if err != nil { - if errors.Is(err, ErrReportsNotInAscOrder) { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } if errors.Is(err, ErrNoReportsAvailable) { c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) return From 724b0523421fd330f67b8bcb81c65a7ba963ac7a Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Mon, 14 Jul 2025 16:17:03 +0300 Subject: [PATCH 39/43] lint --- node-registrar/pkg/server/rewards.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node-registrar/pkg/server/rewards.go b/node-registrar/pkg/server/rewards.go index e2b4fc9..a5b31d2 100644 --- a/node-registrar/pkg/server/rewards.go +++ b/node-registrar/pkg/server/rewards.go @@ -176,7 +176,7 @@ func downtimeSinceLastReportTimestamp(lastReportTimestamp time.Time, currentTime // calculateUpTimePercentage calculates the uptime percentage for a given node within a specific period. // // This function calculates the node's uptime percentage based on a series of UptimeReports. -// It computes downtime by analyzing gaps between reports and by checking if there's been significant downtime since the last report. +// It computes downtime by analyzing gaps between reports and by checking if there's been significant downtime since the last report. // The uptime percentage is calculated as: (totalPeriod - downtime) / totalPeriod * 100. func calculateUpTimePercentage(reports []db.UptimeReport, periodStart, now time.Time) (float64, error) { From 036a1b2661533bc21b3e13690af6faa5f5e3d5f7 Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Mon, 14 Jul 2025 16:53:30 +0300 Subject: [PATCH 40/43] refactor: simplify bytes conversion using explicit constants instead of math.Pow --- node-registrar/pkg/server/rewards.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/node-registrar/pkg/server/rewards.go b/node-registrar/pkg/server/rewards.go index a5b31d2..c133668 100644 --- a/node-registrar/pkg/server/rewards.go +++ b/node-registrar/pkg/server/rewards.go @@ -93,12 +93,14 @@ func CalculateCapacityReward(capacity db.Resources, upTimePercentage float64) (R // bytesToGB converts bytes to gigabytes. func bytesToGB(bytes uint64) float64 { - return float64(bytes) / math.Pow(1024, 3) + const gibibyte = 1024 * 1024 * 1024 + return float64(bytes) / gibibyte } // bytesToTB converts bytes to terabytes. func bytesToTB(bytes uint64) float64 { - return float64(bytes) / math.Pow(1024, 4) + const terabyte = 1024 * 1024 * 1024 * 1024 + return float64(bytes) / terabyte } // calculatePeriodStart returns the start of the period that contains the reference time. From f0941764e93d5955daebd57519d33ca9984cc2fc Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Mon, 14 Jul 2025 18:17:51 +0300 Subject: [PATCH 41/43] refactor: consolidate reward calculation into single function with uptime computation --- node-registrar/pkg/server/handlers.go | 11 +------ node-registrar/pkg/server/rewards.go | 20 ++++++++++-- node-registrar/pkg/server/rewards_test.go | 38 +++++++++++++++++++++-- 3 files changed, 54 insertions(+), 15 deletions(-) diff --git a/node-registrar/pkg/server/handlers.go b/node-registrar/pkg/server/handlers.go index 6bad742..231c2a3 100644 --- a/node-registrar/pkg/server/handlers.go +++ b/node-registrar/pkg/server/handlers.go @@ -322,21 +322,12 @@ func (s Server) getNodeRewardHandler(c *gin.Context) { return } - upTimePercentage, err := calculateUpTimePercentage(reports, periodStart, now) + rewards, err := CalculateCapacityReward(node.Resources, reports, periodStart, now) if err != nil { - if errors.Is(err, ErrNoReportsAvailable) { - c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) - return - } c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - rewards, err := CalculateCapacityReward(node.Resources, upTimePercentage) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } c.JSON(http.StatusOK, rewards) } diff --git a/node-registrar/pkg/server/rewards.go b/node-registrar/pkg/server/rewards.go index c133668..abf5b50 100644 --- a/node-registrar/pkg/server/rewards.go +++ b/node-registrar/pkg/server/rewards.go @@ -53,6 +53,22 @@ type Reward struct { UpTimePercentage float64 //UpTimePercentage: the uptime percentage of the node } +// calculateCapacityReward calculates the reward for a node based on its capacity and uptime, during a specific period. +func CalculateCapacityReward(capacity db.Resources, reports []db.UptimeReport, periodStart, periodEnd time.Time) (Reward, error) { + + if len(reports) == 0 { + return Reward{}, ErrNoReportsAvailable + } + + if !areReportsOrderedCorrectly(reports) { + return Reward{}, ErrReportsNotInAscOrder + } + + upTimePercentage := calculatePercentage(periodEnd.Sub(periodStart), downtimeSinceLastReportTimestamp(reports[len(reports)-1].Timestamp, periodEnd)) + + return computeCapacityRewardWithUptime(capacity, upTimePercentage) +} + // calculateBaseCapacityReward calculates the base reward from node capacity without applying uptime. func calculateBaseCapacityReward(capacity db.Resources) float64 { mruReward := bytesToGB(capacity.MRU) * MemoryRewardPerGB @@ -69,10 +85,10 @@ func calculateTotalReward(capacity db.Resources, upTimePercentage float64) float return baseReward * (upTimePercentage / 100) } -// CalculateCapacityReward calculates the reward in INCA for a given node capacity. +// computeCapacityRewardWithUptime calculates the reward in INCA for a given node capacity and uptime percentage. // // - Note: if the uptime percentage is less than MinUptimePercentageForReward, the node will not receive any rewards. -func CalculateCapacityReward(capacity db.Resources, upTimePercentage float64) (Reward, error) { +func computeCapacityRewardWithUptime(capacity db.Resources, upTimePercentage float64) (Reward, error) { if upTimePercentage < 0 || upTimePercentage > 100 { return Reward{}, ErrInvalidUptimePercentage } diff --git a/node-registrar/pkg/server/rewards_test.go b/node-registrar/pkg/server/rewards_test.go index e38df43..7cf1ded 100644 --- a/node-registrar/pkg/server/rewards_test.go +++ b/node-registrar/pkg/server/rewards_test.go @@ -10,7 +10,7 @@ import ( "github.com/threefoldtech/tfgrid4-sdk-go/node-registrar/pkg/db" ) -func TestCalculateCapacityReward(t *testing.T) { +func TestComputeCapacityRewardWithUptime(t *testing.T) { // Define standard capacity for most tests standardCapacity := db.Resources{ CRU: 8, @@ -94,7 +94,7 @@ func TestCalculateCapacityReward(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := CalculateCapacityReward(tt.capacity, tt.upTimePercentage) + got, err := computeCapacityRewardWithUptime(tt.capacity, tt.upTimePercentage) // Error check if tt.wantError { @@ -352,7 +352,7 @@ func TestRewardFloatingPointPrecision(t *testing.T) { expectedFpReward := expectedTotal * FpRewardPercentage // 8.0 * 0.2 = 1.6 // Get the actual reward calculation - reward, err := CalculateCapacityReward(memoryOnlyCapacity, 100) + reward, err := computeCapacityRewardWithUptime(memoryOnlyCapacity, 100) require.NoError(t, err) // Test precision and distribution @@ -395,6 +395,38 @@ func TestRewardFloatingPointPrecision(t *testing.T) { } // TestAreReportsOrderedCorrectly tests the areReportsOrderedCorrectly function +func TestCalculateCapacityRewardd(t *testing.T) { + now := time.Now() + periodStart := now.Add(-24 * time.Hour) + periodEnd := now + + // Define standard capacity for tests + standardCapacity := db.Resources{ + CRU: 8, + MRU: 68719476736, // 64 GB + SRU: 1099511627776, // 1 TB + HRU: 10995116277760, // 10 TB + } + + t.Run("Test ErrNoReportsAvailable", func(t *testing.T) { + _, err := CalculateCapacityReward(standardCapacity, []db.UptimeReport{}, periodStart, periodEnd) + assert.ErrorIs(t, err, ErrNoReportsAvailable) + }) + t.Run("Test ErrReportsNotInAscOrder", func(t *testing.T) { + _, err := CalculateCapacityReward(standardCapacity, []db.UptimeReport{ + { + Timestamp: now.Add(-2 * time.Hour), + Duration: 2 * time.Hour, + }, + { + Timestamp: now.Add(-6 * time.Hour), + Duration: 1 * time.Hour, + }, + }, periodStart, periodEnd) + assert.ErrorIs(t, err, ErrReportsNotInAscOrder) + }) +} + func TestAreReportsOrderedCorrectly(t *testing.T) { now := time.Now() tests := []struct { From acfb7464aab85a6fb03b7f27482c3c30d5bb7f40 Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Mon, 14 Jul 2025 18:23:14 +0300 Subject: [PATCH 42/43] test: replace assert with require for consistent error checking in rewards test --- node-registrar/pkg/server/rewards_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node-registrar/pkg/server/rewards_test.go b/node-registrar/pkg/server/rewards_test.go index 7cf1ded..de7aa87 100644 --- a/node-registrar/pkg/server/rewards_test.go +++ b/node-registrar/pkg/server/rewards_test.go @@ -99,7 +99,7 @@ func TestComputeCapacityRewardWithUptime(t *testing.T) { // Error check if tt.wantError { require.Error(t, err) - assert.Equal(t, tt.expectedError, err) + require.Equal(t, tt.expectedError, err) return } From 9d4fd14531bc861f62c3d2102964c51c0e6fda0a Mon Sep 17 00:00:00 2001 From: Omar Kassem Date: Mon, 14 Jul 2025 18:29:13 +0300 Subject: [PATCH 43/43] refactor: simplify reward test assertions by comparing entire struct --- node-registrar/pkg/server/rewards_test.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/node-registrar/pkg/server/rewards_test.go b/node-registrar/pkg/server/rewards_test.go index de7aa87..4b94890 100644 --- a/node-registrar/pkg/server/rewards_test.go +++ b/node-registrar/pkg/server/rewards_test.go @@ -116,9 +116,7 @@ func AssertCapacityReward(t testing.TB, resources db.Resources, upTimePercentage t.Helper() if upTimePercentage < MinUptimePercentageForReward { - assert.Equal(t, Reward{ - UpTimePercentage: upTimePercentage, - }, got) + assert.Equal(t, Reward{UpTimePercentage: upTimePercentage}, got) return } @@ -142,11 +140,8 @@ func AssertCapacityReward(t testing.TB, resources db.Resources, upTimePercentage UpTimePercentage: upTimePercentage, } - assert.Equal(t, expected.FarmerReward, got.FarmerReward) - assert.Equal(t, expected.TfReward, got.TfReward) - assert.Equal(t, expected.FpReward, got.FpReward) - assert.Equal(t, expected.Total, got.Total) - assert.Equal(t, expected.UpTimePercentage, got.UpTimePercentage) + assert.Equal(t, expected, got) + } // TestCalculatePeriodStart tests the calculatePeriodStart function with different inputs