Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calculate max allowance #45

Open
wants to merge 32 commits into
base: primary
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8381a1e
Update .envrc file name in README
navFooh Mar 21, 2022
caba68c
Add FilplusApiKey environment variable
navFooh Mar 23, 2022
27d58c6
Create getVerifiedDealCount.go
navFooh Mar 23, 2022
fa1eb18
Create getLinkHeaderURI.go
navFooh Mar 23, 2022
775f831
Create getGitHubEventDates.go
navFooh Mar 23, 2022
7263e11
Update getGitHubEventDates.go
navFooh Mar 23, 2022
59c1562
Update getGitHubEventDates.go
navFooh Mar 23, 2022
bf6ca68
Create getVerifierScore.go
navFooh Mar 23, 2022
6b99d16
Apply formatter fixes
navFooh Mar 23, 2022
ef0f476
Make JSON bindings required
navFooh Mar 24, 2022
f610e8f
Rename MaxAllowanceBytes to BaseAllowanceBytes
navFooh Mar 24, 2022
ba501e1
Update getVerifierScore.go
navFooh Mar 24, 2022
e6c0b2e
getFilecoinDealsMultiplier is separate func
navFooh Mar 24, 2022
4539f87
Return zero on score calculation fail
navFooh Mar 24, 2022
42a335a
Rename getVerifierScore to getMaxAllowance
navFooh Mar 24, 2022
0398a3a
Add comments to getMaxAllowance
navFooh Mar 24, 2022
6585eda
Add GetMaxAllowance to User
navFooh Mar 24, 2022
dbb40b9
Add /max-allowance server endpoint
navFooh Mar 24, 2022
cd74a18
Calculate maximum allowance for verifier
navFooh Mar 24, 2022
8882959
Add /max-allowance-github testing route
navFooh Mar 24, 2022
a4ae44c
Rename githubMaxDateCount to githubMaxEventCount
navFooh Mar 25, 2022
d7fa451
Add getAbsoluteMaxAllowance() method and use for fiftyDataCaps
navFooh Mar 25, 2022
01c686d
Move GetMaxAllowance down in serveVerifyAccount
navFooh Mar 25, 2022
00ef3d7
Return max allowance in verify endpoint
navFooh Mar 25, 2022
01dbf06
Rename allowance variables
navFooh Mar 25, 2022
79cda29
Rename some max allowance endpoints
navFooh Mar 25, 2022
4e898f9
Rename getMaxAllowance to getAllowance
navFooh Mar 25, 2022
e479b45
Rename user.GetMaxAllowance to user.GetAllowance
navFooh Mar 25, 2022
f5c63db
Implement Go logger (#46)
navFooh Mar 31, 2022
c0612b2
Merge branch 'primary' into verifier-score
navFooh Jul 5, 2022
39387e5
restore indent
navFooh Jul 5, 2022
cc231aa
Merge branch 'primary' into verifier-score
navFooh Jul 6, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/aws/aws-sdk-go/aws"
awscreds "github.com/aws/aws-sdk-go/aws/credentials"
awssession "github.com/aws/aws-sdk-go/aws/session"
"github.com/filecoin-project/go-state-types/big"
"github.com/google/uuid"
"github.com/guregu/dynamo"
)
Expand Down Expand Up @@ -41,6 +42,20 @@ func (user User) HasAccountOlderThan(threshold time.Duration) bool {
return false
}

func (user User) GetAllowance(targetAddr string) (big.Int, error) {
account, hasAccount := user.Accounts["github"]
if !hasAccount {
return big.Zero(), errors.New("Can only get allowance for GitHub accounts")
}

allowance, err := getAllowanceGithub(account.Username, targetAddr)
if err != nil {
return big.Zero(), err
}

return allowance, nil
}

func dynamoTable(name string) dynamo.Table {
awsConfig := aws.NewConfig().
WithRegion(env.AWSRegion).
Expand Down
3 changes: 2 additions & 1 deletion env.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

// Mode allows the backend to run in only verifier or faucet mode
type Mode string

const (
// FaucetMode runs just the faucet
FaucetMode Mode = "FAUCET"
Expand All @@ -32,6 +33,7 @@ type Env struct {
BlockedAddresses string `env:"BLOCKED_ADDRESSES"`
GithubClientID string `env:"GITHUB_CLIENT_ID,required"`
GithubClientSecret string `env:"GITHUB_CLIENT_SECRET,required"`
FilplusApiKey string `env:"FILPLUS_API_KEY,required"`
SentryDsn string `env:"SENTRY_DSN"`
SentryEnv string `env:"SENTRY_ENV"`
MaxFee types.FIL `env:"MAX_FEE" envDefault:"0afil"`
Expand All @@ -40,7 +42,6 @@ type Env struct {
VerifierPrivateKey string `env:"VERIFIER_PK"`
VerifierMinAccountAgeDays uint `env:"VERIFIER_MIN_ACCOUNT_AGE_DAYS" envDefault:"180"`
VerifierRateLimit time.Duration `env:"VERIFIER_RATE_LIMIT" envDefault:"730h"`
MaxAllowanceBytes big.Int `env:"MAX_ALLOWANCE_BYTES"`
BaseAllowanceBytes big.Int `env:"BASE_ALLOWANCE_BYTES"`
MaxTotalAllocations uint `env:"MAX_TOTAL_ALLOCATIONS" envDefault:"0"`
AllocationsCounterResetPword string `env:"ALLOCATIONS_COUNTER_PWD"`
Expand Down
137 changes: 137 additions & 0 deletions getAllowance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package main

import (
"time"

"github.com/filecoin-project/go-state-types/big"
)

/*
* Returns the maximum allowance that can be granted
*/
func getMaxAllowance() big.Int {
return big.Mul(env.BaseAllowanceBytes, big.NewInt(16))
}

/*
* Get the allowance for the GitHub user and filecoin address
*/
func getAllowanceGithub(githubAccount string, filecoinAddress string) (big.Int, error) {
// Check GitHub account activity
activityCheck, err := checkGithubAccountActivity(githubAccount, 3)
if err != nil {
return big.Zero(), err
}

// Get Filecoin deals multiplier
dealsMultiplier, err := getFilecoinDealsMultiplier(filecoinAddress)
if err != nil {
return big.Zero(), err
}

// Calculate allowance
score := env.BaseAllowanceBytes
if activityCheck {
score = big.Mul(score, big.NewInt(2))
}
if dealsMultiplier != 1 {
score = big.Mul(score, big.NewInt(int64(dealsMultiplier)))
}
return score, nil
}

/*
* Returns true when there was public activity in the GitHub account for the past X months.
* Also returns true when there was too much recent activity and the returned events from
* GitHub do not reach back far enought to give a proper evaluation.
*/
func checkGithubAccountActivity(githubAccount string, months int) (bool, error) {
// Get event dates from the GitHub account
dates, err := getGitHubEventDates(githubAccount)
if err != nil {
return false, err
}

// Evaluate account activity
dateCount := len(dates)
githubMaxEventCount := 300
activityCheck, enoughDates := hasDateInEachMonthBefore(months, dates)
historyInsufficient := dateCount == githubMaxEventCount && !enoughDates

// Github limits the maximum event history. When we have
// the maximum amount of events but not enough data to go
// back far enough, we give the user the benefit of the doubt.
return activityCheck || historyInsufficient, nil
}

/*
* Returns a multiplier for the allowance based on the
* verified deal count for the supplied filecoin address
*/
func getFilecoinDealsMultiplier(filecoinAddress string) (int, error) {
// Get amount or verified deals for Filecoin address
dealCount, err := getVerifiedDealCount(filecoinAddress)
if err != nil {
return 0, err
}

// Return multiplier
if dealCount > 100 {
return 8, nil
}
if dealCount > 10 {
return 4, nil
}
if dealCount > 0 {
return 2, nil
}
return 1, nil
}

/*
* Checks for an X amount of months before today, whether the supplied dates
* contain a date between the start- and endtime of each of the months. For example,
* when today is 2022-03-23, a date need to be present in all the following months:
* 2022-02-23 to 2022-03-23,
* 2022-01-23 to 2022-02-23 and
* 2021-12-23 to 2022-01-23
* The second return value indicates whether the history of the dates went
* back far enough to check for the presence of a date in each of the months.
*/
func hasDateInEachMonthBefore(months int, dates []time.Time) (bool, bool) {
monthEndTime := time.Now()
for i := 0; i < months; i++ {
// Check whether a date exists in the month before monthEndTime
hasDate, enoughDates := hasDateInMonth(monthEndTime, dates)
// Return instantly when a date is not found and
// indicate whether we had enough dates to evaluate
if !hasDate {
return false, enoughDates
}
// Go back one month before checking again
monthEndTime = monthEndTime.AddDate(0, -1, 0)
}
return true, true
}

/*
* Checks whether a date in the supplied dates falls in the month ending at the supplied
* endTime. The second return value indicates whether there were enough dates. When none
* of the dates fall in or before the given month, more history might be required.
*/
func hasDateInMonth(endTime time.Time, dates []time.Time) (bool, bool) {
startTime := endTime.AddDate(0, -1, 0)
enoughDates := false
for _, date := range dates {
// Return instantly when date falls in the month
if date.After(startTime) && date.Before(endTime) {
return true, true
}
// If a date is before the start time
// the dataset should have been big enough
if date.Before(startTime) {
enoughDates = true
}
}
return false, enoughDates
}
79 changes: 79 additions & 0 deletions getGitHubEventDates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package main

import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)

type GithubEvent struct {
CreatedAt string `json:"created_at" binding:"required"`
}

/*
* Get all "created_at" dates for GitHub events belonging to the provided GitHub account
*/
func getGitHubEventDates(account string) ([]time.Time, error) {
dates := []time.Time{}
url := fmt.Sprintf("https://api.github.com/users/%v/events?per_page=100", account)
for url != "" {
pageDates, next, err := getGitHubEventPageDates(url)
if err != nil {
return nil, err
}
dates = append(dates, pageDates...)
url = next
}
return dates, nil
}

/*
* Get all "created_at" dates from the provided GitHub event page URL.
* Also returns the URL for the next GitHub event page, if it exists.
*/
func getGitHubEventPageDates(url string) ([]time.Time, string, error) {
// Create HTTP client and request
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, "", err
}

// Add headers and perform request
req.Header.Add("accept", "application/vnd.github.v3+json")
resp, err := client.Do(req)
if err != nil {
return nil, "", err
}

// Read the response body
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, "", err
}

// Get events from response body
var githubEvents []GithubEvent
err = json.Unmarshal(body, &githubEvents)
if err != nil {
return nil, "", err
}

// Extract "created_at" dates
dates := []time.Time{}
for _, event := range githubEvents {
date, err := time.Parse("2006-01-02T15:04:05Z", event.CreatedAt)
if err != nil {
return nil, "", err
}
dates = append(dates, date)
}

// Retrieve the next page URL
link := resp.Header.Get("link")
next := getLinkHeaderURI(link, "next")
return dates, next, nil
}
55 changes: 55 additions & 0 deletions getVerifiedDealCount.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package main

import (
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
)

type VerifiedDeals struct {
Count string `json:"count" binding:"required"`
}

/*
* Returns the amount of verified deals for a Filecoin address
*/
func getVerifiedDealCount(address string) (int, error) {
// Create HTTP client and request
client := &http.Client{}
url := fmt.Sprintf("https://api.filplus.d.interplanetary.one/public/api/getVerifiedDeals/%v?limit=1&page=1", address)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return 0, err
}

// Add headers and perform request
req.Header.Add("x-api-key", env.FilplusApiKey)
resp, err := client.Do(req)
if err != nil {
return 0, err
}

// Read the response body
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return 0, err
}

// Get count from response body
var verifiedDeals VerifiedDeals
err = json.Unmarshal(body, &verifiedDeals)
if err != nil {
return 0, err
}

// Convert count to integer
count, err := strconv.Atoi(verifiedDeals.Count)
if err != nil {
return 0, err
}

return count, nil
}
48 changes: 46 additions & 2 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func startVerifier(router *gin.Engine, c *cron.Cron) {
router.GET("/verifiers", serveListVerifiers)
router.GET("/verified-clients", serveListVerifiedClients)
router.GET("/allowance/:target_addr", serveAllowance)
router.GET("/allowance-github/:github_user/:target_addr", serveAllowanceGithub)
router.GET("/account-remaining-bytes/:target_addr", serveCheckAccountRemainingBytes)
router.GET("/verifier-remaining-bytes/:target_addr", serveCheckVerifierRemainingBytes)

Expand Down Expand Up @@ -335,7 +336,13 @@ func serveVerifyAccount(c *gin.Context) {
return
}

allowance := env.MaxAllowanceBytes
// Get maximum allowance for user / address combination
allowance, err := user.GetAllowance(targetAddrStr)
if err != nil {
logger.Errorf("CALCULATING MAX ALLOWANCE FAILED: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrMaxAllowanceFailed.Error()})
return
}

// Allocate the bytes
err = incrementCounter(c)
Expand Down Expand Up @@ -400,11 +407,48 @@ func serveListVerifiedClients(c *gin.Context) {
}

func serveAllowance(c *gin.Context) {
userID, err := getUserIDFromJWT(c)
if err != nil {
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return
}

user, err := getUserByID(userID)
if err != nil {
c.JSON(http.StatusForbidden, gin.H{"error": ErrStaleJWT.Error()})
return
}

// Get the allowance for the user
targetAddr := c.Param("target_addr")
allowance, err := user.GetAllowance(targetAddr)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

// Respond with allowance
type Response struct {
Allowance string `json:"allowance"`
}
c.JSON(http.StatusOK, Response{Allowance: allowance.String()})
}

func serveAllowanceGithub(c *gin.Context) {
// Get the allowance for the user
targetAddr := c.Param("target_addr")
githubUser := c.Param("github_user")
allowance, err := getAllowanceGithub(githubUser, targetAddr)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

// Respond with allowance
type Response struct {
Allowance string `json:"allowance"`
}
c.JSON(http.StatusOK, Response{Allowance: env.MaxAllowanceBytes.String()})
c.JSON(http.StatusOK, Response{Allowance: allowance.String()})
}

func serveCheckAccountRemainingBytes(c *gin.Context) {
Expand Down