Skip to content

Commit

Permalink
Merge pull request #124 from NodeFactoryIo/mmuftic/payout-script-impl…
Browse files Browse the repository at this point in the history
…ementation

Calculating reward distribution
  • Loading branch information
mpetrunic authored Nov 12, 2020
2 parents 209deb0 + 16ad486 commit f58f034
Show file tree
Hide file tree
Showing 16 changed files with 522 additions and 70 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- Valid flag on node [\#105](https://github.com/NodeFactoryIo/vedran/pull/105) ([mpetrun5](https://github.com/mpetrun5))
- Passing SSL certificates [\#112](https://github.com/NodeFactoryIo/vedran/pull/112) ([mpetrun5](https://github.com/mpetrun5))
- Expose stats endpoints [\#114](https://github.com/NodeFactoryIo/vedran/pull/114) ([MakMuftic](https://github.com/MakMuftic))
- Calculating reward distribution [\#124](https://github.com/NodeFactoryIo/vedran/pull/124) ([MakMuftic](https://github.com/MakMuftic))

### Fix

Expand Down
17 changes: 12 additions & 5 deletions internal/controllers/stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,25 @@ package controllers

import (
"encoding/json"
"github.com/NodeFactoryIo/vedran/internal/models"
"github.com/NodeFactoryIo/vedran/internal/configuration"
"github.com/NodeFactoryIo/vedran/internal/payout"
"github.com/NodeFactoryIo/vedran/internal/stats"
muxhelpper "github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
"net/http"
"time"
)

type StatsResponse struct {
Stats map[string]models.NodeStatsDetails `json:"stats"`
Stats map[string]payout.NodePayoutDetails `json:"stats"`
Fee float32 `json:"fee"`
}

var getNow = time.Now

func (c *ApiController) StatisticsHandlerAllStats(w http.ResponseWriter, r *http.Request) {
statisticsForPayout, err := stats.CalculateStatisticsFromLastPayout(c.repositories)
// should check for signature in body and only then record payout
payoutStatistics, err := payout.GetStatsForPayout(c.repositories, getNow(), false)
if err != nil {
log.Errorf("Failed to calculate statistics, because %v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
Expand All @@ -23,7 +29,8 @@ func (c *ApiController) StatisticsHandlerAllStats(w http.ResponseWriter, r *http

w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(StatsResponse{
Stats: statisticsForPayout,
Stats: payoutStatistics,
Fee: configuration.Config.Fee,
})
}

Expand All @@ -36,7 +43,7 @@ func (c *ApiController) StatisticsHandlerStatsForNode(w http.ResponseWriter, r *
return
}

nodeStatisticsFromLastPayout, err := stats.CalculateNodeStatisticsFromLastPayout(c.repositories, nodeId)
nodeStatisticsFromLastPayout, err := stats.CalculateNodeStatisticsFromLastPayout(c.repositories, nodeId, getNow())
if err != nil {
log.Errorf("Failed to calculate statistics for node %s, because %v", nodeId, err)
if err.Error() == "not found" {
Expand Down
16 changes: 14 additions & 2 deletions internal/controllers/stats_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import (

func TestApiController_StatisticsHandlerAllStats(t *testing.T) {
now := time.Now()
getNow = func() time.Time {
return now
}
tests := []struct {
name string
httpStatus int
Expand Down Expand Up @@ -86,6 +89,10 @@ func TestApiController_StatisticsHandlerAllStats(t *testing.T) {
nodeRepoMock.On("GetAll").Return(
test.nodeRepoGetAllReturns, test.nodeRepoGetAllError,
)
nodeRepoMock.On("FindByID", test.nodeId).Return(&models.Node{
ID: test.nodeId,
PayoutAddress: "0xtest-address",
}, nil)
recordRepoMock := mocks.RecordRepository{}
recordRepoMock.On("FindSuccessfulRecordsInsideInterval",
test.nodeId, mock.Anything, mock.Anything,
Expand Down Expand Up @@ -114,6 +121,7 @@ func TestApiController_StatisticsHandlerAllStats(t *testing.T) {
test.payoutRepoFindLatestPayoutReturns,
test.payoutRepoFindLatestPayoutError,
)
payoutRepoMock.On("Save", mock.Anything).Return(nil)
apiController := NewApiController(false, repositories.Repos{
NodeRepo: &nodeRepoMock,
PingRepo: &pingRepoMock,
Expand All @@ -135,15 +143,19 @@ func TestApiController_StatisticsHandlerAllStats(t *testing.T) {
var statsResponse StatsResponse
if rr.Code == http.StatusOK {
_ = json.Unmarshal(rr.Body.Bytes(), &statsResponse)
assert.LessOrEqual(t, test.nodeNumberOfPings, statsResponse.Stats[test.nodeId].TotalPings)
assert.Equal(t, test.nodeNumberOfRequests, statsResponse.Stats[test.nodeId].TotalRequests)
assert.LessOrEqual(t, test.nodeNumberOfPings, statsResponse.Stats[test.nodeId].Stats.TotalPings)
assert.Equal(t, test.nodeNumberOfRequests, statsResponse.Stats[test.nodeId].Stats.TotalRequests)
assert.Equal(t, "0xtest-address", statsResponse.Stats[test.nodeId].PayoutAddress)
}
})
}
}

func TestApiController_StatisticsHandlerStatsForNode(t *testing.T) {
now := time.Now()
getNow = func() time.Time {
return now
}
tests := []struct {
name string
httpStatus int
Expand Down
20 changes: 18 additions & 2 deletions internal/loadbalancer/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ package loadbalancer

import (
"fmt"
"net/http"

"github.com/NodeFactoryIo/vedran/internal/auth"
"github.com/NodeFactoryIo/vedran/internal/configuration"
"github.com/NodeFactoryIo/vedran/internal/models"
"github.com/NodeFactoryIo/vedran/internal/repositories"
"github.com/NodeFactoryIo/vedran/internal/router"
"github.com/NodeFactoryIo/vedran/internal/schedule/checkactive"
"github.com/NodeFactoryIo/vedran/internal/schedule/penalize"
"github.com/asdine/storm/v3"
log "github.com/sirupsen/logrus"
"net/http"
"time"
)

func StartLoadBalancerServer(props configuration.Configuration) {
Expand Down Expand Up @@ -45,6 +46,21 @@ func StartLoadBalancerServer(props configuration.Configuration) {
log.Fatalf("Failed reseting pings because of: %v", err)
}

// save initial payout if there isn't any saved payouts
p, err := repos.PayoutRepo.GetAll()
if err != nil {
log.Fatalf("Failed creating initial payout because of: %v", err)
} else if len(*p) == 0 {
err := repos.PayoutRepo.Save(&models.Payout{
ID: "1",
Timestamp: time.Now(),
PaymentDetails: nil,
})
if err != nil {
log.Fatalf("Failed creating initial payout because of: %v", err)
}
}

penalizedNodes, err := repos.NodeRepo.GetPenalizedNodes()
if err != nil {
log.Fatalf("Failed fetching penalized nodes because of: %v", err)
Expand Down
1 change: 1 addition & 0 deletions internal/models/payout.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package models
import "time"

type Payout struct {
ID string `storm:"id"`
Timestamp time.Time `json:"timestamp"`
PaymentDetails map[string]NodeStatsDetails
}
Expand Down
62 changes: 62 additions & 0 deletions internal/payout/distribution.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package payout

import (
"github.com/NodeFactoryIo/vedran/internal/models"
"math"
"math/big"
)

const (
livelinessRewardPercentage = 0.1
requestsRewardPercentage = 0.9
)

func CalculatePayoutDistributionByNode(
payoutDetails map[string]models.NodeStatsDetails,
totalReward float64,
loadBalancerFeePercentage float64,
) map[string]big.Int {
var rewardPool = totalReward

loadbalancerReward := rewardPool * loadBalancerFeePercentage
rewardPool -= loadbalancerReward

livelinessRewardPool := rewardPool * livelinessRewardPercentage
requestsRewardPool := rewardPool * requestsRewardPercentage

var totalNumberOfPings = float64(0)
var totalNumberOfRequests = float64(0)
for _, node := range payoutDetails {
totalNumberOfPings += node.TotalPings
totalNumberOfRequests += node.TotalRequests
}

totalDistributedLivelinessRewards := float64(0)
totalDistributedRequestsRewards := float64(0)
payoutAmountDistributionByNodes := make(map[string]big.Int, len(payoutDetails))

for nodeId, nodeStatsDetails := range payoutDetails {
// liveliness rewards
livelinessReward := float64(0)
if totalNumberOfPings != 0 && nodeStatsDetails.TotalPings != 0 {
nodeLivelinessRewardPercentage := nodeStatsDetails.TotalPings / totalNumberOfPings
livelinessReward = livelinessRewardPool * nodeLivelinessRewardPercentage
livelinessReward = math.Floor(livelinessReward)
totalDistributedLivelinessRewards += livelinessReward
}
// requests rewards
requestsReward := float64(0)
if totalNumberOfRequests != 0 && nodeStatsDetails.TotalRequests != 0 {
nodeRequestsRewardPercentage := nodeStatsDetails.TotalRequests / totalNumberOfRequests
requestsReward = requestsRewardPool * nodeRequestsRewardPercentage
requestsReward = math.Floor(requestsReward)
totalDistributedRequestsRewards += requestsReward
}

totalNodeReward := livelinessReward + requestsReward
totalNodeRewardAsInt, _ := big.NewFloat(totalNodeReward).Int(nil)
payoutAmountDistributionByNodes[nodeId] = *totalNodeRewardAsInt
}

return payoutAmountDistributionByNodes
}
75 changes: 75 additions & 0 deletions internal/payout/distribution_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package payout

import (
"github.com/NodeFactoryIo/vedran/internal/models"
"github.com/stretchr/testify/assert"
"math/big"
"testing"
)

func Test_CalculatePayoutDistributionByNode(t *testing.T) {
tests := []struct {
name string
payoutDetails map[string]models.NodeStatsDetails
totalReward float64
loadBalancerFee float64
resultDistribution map[string]big.Int
}{
{ // this test is set for 10/90 split between liveliness and requests
name: "test distribution",
payoutDetails: map[string]models.NodeStatsDetails{
"1": {
TotalPings: 100,
TotalRequests: 10,
},
"2": {
TotalPings: 100,
TotalRequests: 5,
},
"3": {
TotalPings: 90,
TotalRequests: 10,
},
"4": {
TotalPings: 90,
TotalRequests: 5,
},
"5": {
TotalPings: 50,
TotalRequests: 2,
},
"6": {
TotalPings: 40,
TotalRequests: 0,
},
},
totalReward: 100000000,
loadBalancerFee: 0.1,
resultDistribution: map[string]big.Int{
"1": *big.NewInt(27227393), // 27227393.617021276 // 100P 10R
"2": *big.NewInt(14571143), // 14571143.617021276 // 100P 5R
"3": *big.NewInt(27035904), // 27035904.255319147 // 90P 10R
"4": *big.NewInt(14379654), // 14379654.25531915 // 90P 5R
"5": *big.NewInt(6019946), // 6019946.808510638 // 50P 2R
"6": *big.NewInt(765957), // 765957.4468085106 // 40P 0R
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
distributionByNode := CalculatePayoutDistributionByNode(
test.payoutDetails, test.totalReward, test.loadBalancerFee,
)
assert.Equal(t, test.resultDistribution, distributionByNode)
totalDistributed := big.NewInt(0)
for _, amount := range distributionByNode {
totalDistributed.Add(totalDistributed, &amount)
}
totalShoudBeDistributed := test.totalReward * (float64(1) - test.loadBalancerFee)

totalShouldBeDistributedRounded, _ := big.NewFloat(totalShoudBeDistributed).Int(nil)
delta := big.NewInt(0).Sub(totalShouldBeDistributedRounded, totalDistributed)
assert.GreaterOrEqual(t, delta.Int64(), int64(0))
})
}
}
48 changes: 48 additions & 0 deletions internal/payout/payout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package payout

import (
"github.com/NodeFactoryIo/vedran/internal/models"
"github.com/NodeFactoryIo/vedran/internal/repositories"
"github.com/NodeFactoryIo/vedran/internal/stats"
log "github.com/sirupsen/logrus"
"time"
)

type NodePayoutDetails struct {
Stats models.NodeStatsDetails `json:"stats"`
PayoutAddress string `json:"payout_address"`
}

func GetStatsForPayout(
repos repositories.Repos,
intervalEnd time.Time,
recordPayout bool,
) (map[string]NodePayoutDetails, error) {

statistics, err := stats.CalculateStatisticsFromLastPayout(repos, intervalEnd)
if err != nil {
return nil, err
}

payoutStatistics := make(map[string]NodePayoutDetails, len(statistics))
for nodeId, statsDetails := range statistics {
node, _ := repos.NodeRepo.FindByID(nodeId)
payoutStatistics[nodeId] = NodePayoutDetails{
Stats: statsDetails,
PayoutAddress: node.PayoutAddress,
}
}

if recordPayout {
err = repos.PayoutRepo.Save(&models.Payout{
Timestamp: intervalEnd,
PaymentDetails: statistics,
})
if err != nil {
log.Errorf("Unable to save payout information to database, because of: %v", err)
return nil, err
}
}

return payoutStatistics, nil
}
Loading

0 comments on commit f58f034

Please sign in to comment.