Skip to content

Commit

Permalink
Add circulation and decentral endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
kacpersaw committed Aug 6, 2024
1 parent 4155fd6 commit 264760b
Show file tree
Hide file tree
Showing 14 changed files with 291 additions and 51 deletions.
4 changes: 1 addition & 3 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ type Api struct {
Echo *echo.Echo
}

func Init(db *sql.Database, dbClient storage.DatabaseClient, allowedOrigins []string, debug bool, layersPerEpoch int64,
marshaler *marshaler.Marshaler, routes func(e *echo.Echo)) *Api {

func Init(db *sql.Database, dbClient storage.DatabaseClient, allowedOrigins []string, debug bool, layersPerEpoch int64, marshaler *marshaler.Marshaler, routes func(e *echo.Echo)) *Api {
e := echo.New()
e.Use(middleware.Recover())
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
Expand Down
30 changes: 30 additions & 0 deletions api/handler/circulation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package handler

import (
"context"
"github.com/labstack/echo/v4"
"github.com/spacemeshos/explorer-backend/api/storage"
"github.com/spacemeshos/go-spacemesh/log"
"net/http"
)

func Circulation(c echo.Context) error {
cc := c.(*ApiContext)

if cached, err := cc.Cache.Get(context.Background(), "circulation", new(*storage.Circulation)); err == nil {
return c.JSON(http.StatusOK, cached)
}

circulation, err := cc.StorageClient.GetCirculation(cc.Storage)
if err != nil {
log.Warning("failed to get circulation: %v", err)
return c.NoContent(http.StatusInternalServerError)
}

if err = cc.Cache.Set(context.Background(), "circulation", circulation); err != nil {
log.Warning("failed to cache circulation: %v", err)
return c.NoContent(http.StatusInternalServerError)
}

return c.JSON(http.StatusOK, circulation)
}
50 changes: 49 additions & 1 deletion api/handler/epoch.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package handler

import (
"context"
"github.com/eko/gocache/lib/v4/store"
"github.com/labstack/echo/v4"
"github.com/spacemeshos/explorer-backend/api/storage"
"github.com/spacemeshos/go-spacemesh/log"
"net/http"
"strconv"
"time"
)

func Epoch(c echo.Context) error {
Expand All @@ -26,7 +28,7 @@ func Epoch(c echo.Context) error {
return c.NoContent(http.StatusInternalServerError)
}

if err = cc.Cache.Set(context.Background(), "epochStats"+c.Param("id"), epochStats); err != nil {
if err = cc.Cache.Set(context.Background(), "epochStats"+c.Param("id"), epochStats, store.WithExpiration(1*time.Minute)); err != nil {
log.Warning("failed to cache epoch stats: %v", err)
return c.NoContent(http.StatusInternalServerError)
}
Expand Down Expand Up @@ -54,3 +56,49 @@ func EpochRefresh(c echo.Context) error {

return c.NoContent(http.StatusOK)
}

func EpochDecentral(c echo.Context) error {
cc := c.(*ApiContext)
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return c.NoContent(http.StatusBadRequest)
}

if cached, err := cc.Cache.Get(context.Background(), "epochStatsDecentral"+c.Param("id"), new(*storage.EpochStats)); err == nil {
return c.JSON(http.StatusOK, cached)
}

epochStats, err := cc.StorageClient.GetEpochDecentralRatio(cc.Storage, int64(id))
if err != nil {
log.Warning("failed to get epoch stats: %v", err)
return c.NoContent(http.StatusInternalServerError)
}

if err = cc.Cache.Set(context.Background(), "epochStatsDecentral"+c.Param("id"), epochStats, store.WithExpiration(1*time.Minute)); err != nil {
log.Warning("failed to cache epoch stats: %v", err)
return c.NoContent(http.StatusInternalServerError)
}

return c.JSON(http.StatusOK, epochStats)
}

func EpochDecentralRefresh(c echo.Context) error {
cc := c.(*ApiContext)
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return c.NoContent(http.StatusBadRequest)
}

epochStats, err := cc.StorageClient.GetEpochDecentralRatio(cc.Storage, int64(id))
if err != nil {
log.Warning("failed to get epoch stats: %v", err)
return c.NoContent(http.StatusInternalServerError)
}

if err = cc.Cache.Set(context.Background(), "epochStatsDecentral"+c.Param("id"), epochStats); err != nil {
log.Warning("failed to cache epoch stats: %v", err)
return c.NoContent(http.StatusInternalServerError)
}

return c.NoContent(http.StatusOK)
}
3 changes: 3 additions & 0 deletions api/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@ import (
func Router(e *echo.Echo) {
e.GET("/layer/:id", handler.Layer)
e.GET("/epoch/:id", handler.Epoch)
e.GET("/epoch/:id/decentral", handler.EpochDecentral)
e.GET("/account/:address", handler.Account)
e.GET("/smeshers/:epoch", handler.SmeshersByEpoch)
e.GET("/smeshers", handler.Smeshers)
e.GET("/smesher/:smesherId", handler.Smesher)
e.GET("/overview", handler.Overview)
e.GET("/circulation", handler.Circulation)
}

func RefreshRouter(e *echo.Echo) {
g := e.Group("/refresh")
g.GET("/epoch/:id", handler.EpochRefresh)
e.GET("/epoch/:id/decentral", handler.EpochDecentralRefresh)
g.GET("/overview", handler.OverviewRefresh)
g.GET("/smeshers/:epoch", handler.SmeshersByEpochRefresh)
g.GET("/smeshers", handler.SmeshersRefresh)
Expand Down
30 changes: 30 additions & 0 deletions api/storage/circulation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package storage

import (
"github.com/spacemeshos/economics/vesting"
"github.com/spacemeshos/go-spacemesh/log"
"github.com/spacemeshos/go-spacemesh/sql"
)

type Circulation struct {
Circulation uint64 `json:"circulation"`
}

func (c *Client) GetCirculation(db *sql.Database) (*Circulation, error) {
circulation := &Circulation{
Circulation: 0,
}
if !c.Testnet {
accumulatedVest := vesting.AccumulatedVestAtLayer(c.NodeClock.CurrentLayer().Uint32())
circulation.Circulation = accumulatedVest
}

rewardsSum, _, err := c.GetRewardsSum(db)
if err != nil {
log.Warning("failed to get rewards count: %v", err)
return nil, err
}
circulation.Circulation += rewardsSum

return circulation, nil
}
56 changes: 50 additions & 6 deletions api/storage/epoch.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
package storage

import (
"github.com/spacemeshos/explorer-backend/utils"
"github.com/spacemeshos/go-spacemesh/common/types"
"github.com/spacemeshos/go-spacemesh/log"
"github.com/spacemeshos/go-spacemesh/sql"
"github.com/spacemeshos/go-spacemesh/sql/atxs"
"github.com/spacemeshos/go-spacemesh/sql/builder"
"math"
)

type EpochStats struct {
TransactionsCount uint64 `json:"transactions_count"`
ActivationsCount uint64 `json:"activations_count"`
RewardsCount uint64 `json:"rewards_count"`
RewardsSum uint64 `json:"rewards_sum"`
NumUnits uint64 `json:"num_units"`
SmeshersCount uint64 `json:"smeshers_count"`
TransactionsCount uint64 `json:"transactions_count,omitempty"`
ActivationsCount uint64 `json:"activations_count,omitempty"`
RewardsCount uint64 `json:"rewards_count,omitempty"`
RewardsSum uint64 `json:"rewards_sum,omitempty"`
NumUnits uint64 `json:"num_units,omitempty"`
SmeshersCount uint64 `json:"smeshers_count,omitempty"`
Decentral uint64 `json:"decentral,omitempty"`
}

func (c *Client) GetEpochStats(db *sql.Database, epoch int64, layersPerEpoch int64) (*EpochStats, error) {
Expand Down Expand Up @@ -100,3 +104,43 @@ FROM (

return stats, err
}

func (c *Client) GetEpochDecentralRatio(db *sql.Database, epoch int64) (*EpochStats, error) {
stats := &EpochStats{
Decentral: 0,
}

_, err := db.Exec(`SELECT COUNT(*) FROM (SELECT DISTINCT pubkey FROM atxs WHERE epoch = ?1)`,
func(stmt *sql.Statement) {
stmt.BindInt64(1, epoch-1)
},
func(stmt *sql.Statement) bool {
stats.SmeshersCount = uint64(stmt.ColumnInt64(0))
return true
})
if err != nil {
return nil, err
}

a := math.Min(float64(stats.SmeshersCount), 1e4)
// pubkey: commitment size
smeshers := make(map[string]uint64)
_, err = db.Exec(`SELECT pubkey, effective_num_units FROM atxs WHERE epoch = ?1`,
func(stmt *sql.Statement) {
stmt.BindInt64(1, epoch-1)
},
func(stmt *sql.Statement) bool {
var smesher types.NodeID
stmt.ColumnBytes(0, smesher[:])
smeshers[smesher.String()] = uint64(stmt.ColumnInt64(1)) * ((c.BitsPerLabel * c.LabelsPerUnit) / 8)
return true
})
if err != nil {
return nil, err
}

stats.Decentral = uint64(100.0 * (0.5*(a*a)/1e8 + 0.5*(1.0-utils.Gini(smeshers))))

return stats, nil

}
11 changes: 10 additions & 1 deletion api/storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"github.com/spacemeshos/go-spacemesh/common/types"
"github.com/spacemeshos/go-spacemesh/sql"
"github.com/spacemeshos/go-spacemesh/timesync"
)

type DatabaseClient interface {
Expand All @@ -13,6 +14,7 @@ type DatabaseClient interface {
GetLayersCount(db *sql.Database) (uint64, error)

GetEpochStats(db *sql.Database, epoch int64, layersPerEpoch int64) (*EpochStats, error)
GetEpochDecentralRatio(db *sql.Database, epoch int64) (*EpochStats, error)

GetSmeshers(db *sql.Database, limit, offset uint64) (*SmesherList, error)
GetSmeshersByEpoch(db *sql.Database, limit, offset, epoch uint64) (*SmesherList, error)
Expand All @@ -29,9 +31,16 @@ type DatabaseClient interface {

GetTransactionsCount(db *sql.Database) (uint64, error)
GetTotalNumUnits(db *sql.Database) (uint64, error)

GetCirculation(db *sql.Database) (*Circulation, error)
}

type Client struct{}
type Client struct {
NodeClock *timesync.NodeClock
Testnet bool
LabelsPerUnit uint64
BitsPerLabel uint64
}

func Setup(path string) (db *sql.Database, err error) {
db, err = sql.Open(fmt.Sprintf("file:%s?mode=ro", path),
Expand Down
77 changes: 74 additions & 3 deletions cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ import (
"github.com/spacemeshos/explorer-backend/api/storage"
"github.com/spacemeshos/go-spacemesh/common/types"
"github.com/spacemeshos/go-spacemesh/log"
"github.com/spacemeshos/go-spacemesh/timesync"
"github.com/urfave/cli/v2"
"go.uber.org/zap"
"os"
"sync"
"time"
)

var (
Expand All @@ -28,6 +31,10 @@ var (
debug bool
sqlitePathStringFlag string
layersPerEpoch int64
genesisTimeStringFlag string
layerDuration time.Duration
labelsPerUnit uint64
bitsPerLabel uint64
)

var flags = []cli.Flag{
Expand Down Expand Up @@ -83,6 +90,38 @@ var flags = []cli.Flag{
Value: 4032,
EnvVars: []string{"SPACEMESH_LAYERS_PER_EPOCH"},
},
&cli.StringFlag{
Name: "genesis-time",
Usage: "Genesis time in RFC3339 format",
Required: true,
Destination: &genesisTimeStringFlag,
Value: "2024-06-21T13:00:00.000Z",
EnvVars: []string{"SPACEMESH_GENESIS_TIME"},
},
&cli.DurationFlag{
Name: "layer-duration",
Usage: "Duration of a single layer",
Required: false,
Destination: &layerDuration,
Value: 30 * time.Second,
EnvVars: []string{"SPACEMESH_LAYER_DURATION"},
},
&cli.Uint64Flag{
Name: "labels-per-unit",
Usage: "Number of labels per unit",
Required: false,
Destination: &labelsPerUnit,
Value: 1024,
EnvVars: []string{"SPACEMESH_LABELS_PER_UNIT"},
},
&cli.Uint64Flag{
Name: "bits-per-label",
Usage: "Number of bits per label",
Required: false,
Destination: &bitsPerLabel,
Value: 128,
EnvVars: []string{"SPACEMESH_BITS_PER_LABEL"},
},
}

func main() {
Expand All @@ -104,25 +143,57 @@ func main() {

c := cache.New()

gTime, err := time.Parse(time.RFC3339, genesisTimeStringFlag)
if err != nil {
return fmt.Errorf("cannot parse genesis time %s: %w", genesisTimeStringFlag, err)
}

clock, err := timesync.NewClock(
timesync.WithLayerDuration(layerDuration),
timesync.WithTickInterval(1*time.Second),
timesync.WithGenesisTime(gTime),
timesync.WithLogger(zap.NewNop()),
)
if err != nil {
return fmt.Errorf("cannot create clock: %w", err)
}

db, err := storage.Setup(sqlitePathStringFlag)
if err != nil {
log.Info("SQLite storage open error %v", err)
return err
}
dbClient := &storage.Client{}
dbClient := &storage.Client{
NodeClock: clock,
Testnet: testnetBoolFlag,
LabelsPerUnit: labelsPerUnit,
BitsPerLabel: bitsPerLabel,
}

var wg sync.WaitGroup
wg.Add(2)
// start api server
server := api.Init(db, dbClient, allowedOrigins.Value(), debug, layersPerEpoch, c, router.Router)
server := api.Init(db,
dbClient,
allowedOrigins.Value(),
debug,
layersPerEpoch,
c,
router.Router)
go func() {
defer wg.Done()
log.Info(fmt.Sprintf("starting api server on %s", listenStringFlag))
server.Run(listenStringFlag)
}()

// start refresh api server
refreshServer := api.Init(db, dbClient, allowedOrigins.Value(), debug, layersPerEpoch, c, router.RefreshRouter)
refreshServer := api.Init(db,
dbClient,
allowedOrigins.Value(),
debug,
layersPerEpoch,
c,
router.RefreshRouter)
go func() {
defer wg.Done()
log.Info(fmt.Sprintf("starting refresh api server on %s", refreshListenStringFlag))
Expand Down
Loading

0 comments on commit 264760b

Please sign in to comment.