Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 10 additions & 2 deletions routes/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,8 +318,9 @@ const (
RoutePathStateChecksum = "/api/v0/state-checksum"

// validators.go
RoutePathValidators = "/api/v0/validators"
RoutePathCheckNodeStatus = "/api/v0/check-node-status"
RoutePathValidators = "/api/v0/validators"
RoutePathCheckNodeStatus = "/api/v0/check-node-status"
RoutePathCurrentEpochProgress = "/api/v0/current-epoch-progress"

// stake.go
RoutePathStake = "/api/v0/stake"
Expand Down Expand Up @@ -1369,6 +1370,13 @@ func (fes *APIServer) NewRouter() *muxtrace.Router {
fes.CheckNodeStatus,
PublicAccess,
},
{
"GetCurrentEpochProgress",
[]string{"GET"},
RoutePathCurrentEpochProgress,
fes.GetCurrentEpochProgress,
PublicAccess,
},
{
"CreateStakeTxn",
[]string{"POST", "OPTIONS"},
Expand Down
31 changes: 31 additions & 0 deletions routes/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ package routes

import (
"encoding/hex"
"encoding/json"
"io"
"net/http"

"github.com/deso-protocol/core/lib"
"github.com/gorilla/mux"
"github.com/pkg/errors"
)

Expand All @@ -23,3 +28,29 @@ func decodeBlockHashFromHex(hexEncoding string) (*lib.BlockHash, error) {
}
return lib.NewBlockHash(decodedBytes), nil
}

func parseRequestBodyParams[TRequestParams any](request *http.Request) (*TRequestParams, error) {
var requestParams TRequestParams

decoder := json.NewDecoder(io.LimitReader(request.Body, MaxRequestBodySizeBytes))
if err := decoder.Decode(&requestParams); err != nil {
return nil, errors.Errorf("Error parsing request body: %v", err)
}

return &requestParams, nil
}

func parseRequestQueryParams[TRequestParams any](request *http.Request) (*TRequestParams, error) {
var requestParams TRequestParams

serializedJson, err := json.Marshal(mux.Vars(request))
if err != nil {
return nil, err
}

if err := json.Unmarshal(serializedJson, requestParams); err != nil {
return nil, err
}

return &requestParams, nil
}
142 changes: 138 additions & 4 deletions routes/validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"github.com/deso-protocol/core/bls"
"github.com/deso-protocol/core/lib"
"github.com/gorilla/mux"
"github.com/holiman/uint256"
"io"
"net"
"net/http"
"time"

"github.com/deso-protocol/core/bls"
"github.com/deso-protocol/core/collections"
"github.com/deso-protocol/core/lib"
"github.com/gorilla/mux"
"github.com/holiman/uint256"
)

type RegisterAsValidatorRequest struct {
Expand Down Expand Up @@ -354,6 +356,138 @@ func (fes *APIServer) GetValidatorByPublicKeyBase58Check(ww http.ResponseWriter,
}
}

// GetCurrentEpochProgressResponse encodes the current epoch entry, the leader schedule for it, and the
// progress throughout the epoch. Based on the data returned, the client can determine the chain's
// progress through the epoch, the current leader and all upcoming leaders.
type GetEpochProgressResponse struct {
// The full epoch entry object
EpochEntry lib.EpochEntry `safeForLogging:"true"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally I'd say we need to create a new response type, but since the epoch entry is just a bunch of uint64s and int64s, this is chill.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems fine to no create a duplicate type in this case. And it doesn't preclude us from creating a new backend-only type in the future if needed

LeaderSchedule []UserInfoBasic `safeForLogging:"true"`

CurrentView uint64 `safeForLogging:"true"`
CurrentTipHeight uint64 `safeForLogging:"true"`
}

type UserInfoBasic struct {
PublicKeyBase58Check string `safeForLogging:"true"`
Username string `safeForLogging:"true"`
}

func (fes *APIServer) GetCurrentEpochProgress(ww http.ResponseWriter, req *http.Request) {
// Fetch the current snapshot from the blockchain. We use the latest uncommitted tip.
utxoView, err := fes.backendServer.GetBlockchain().GetUncommittedTipView()
if err != nil {
_AddInternalServerError(ww, "GetCurrentEpochProgress: problem fetching uncommitted tip")
return
}

// Get the current epoch number.
currentEpochEntry, err := utxoView.GetCurrentEpochEntry()
if err != nil {
_AddInternalServerError(ww, "GetCurrentEpochProgress: problem fetching current epoch number")
return
}

// Get the current uncommitted tip.
currentTip := fes.backendServer.GetBlockchain().BlockTip()

// Get the leader schedule for the current snapshot epoch.
leaderSchedulePKIDs, err := utxoView.GetCurrentSnapshotLeaderSchedule()
if err != nil {
_AddInternalServerError(ww, "GetCurrentEpochProgress: problem fetching current snapshot epoch number")
return
}

// Fetch the leader schedule for the current epoch. For each leader in the schedule, we fetch
// the public key and username associated with the leader's PKID.
leaderSchedule := collections.Transform(leaderSchedulePKIDs, func(pkid *lib.PKID) UserInfoBasic {
publicKey := utxoView.GetPublicKeyForPKID(pkid)
publicKeyBase58Check := lib.Base58CheckEncode(publicKey, false, fes.Params)

// Fetch the profile entry for the leader's PKID.
profileEntry := utxoView.GetProfileEntryForPKID(pkid)
if profileEntry == nil {
// If the user has no profile, then we return an empty username.
return UserInfoBasic{PublicKeyBase58Check: publicKeyBase58Check, Username: ""}
}

// Happy path: we have both a username and a public key for the leader.
return UserInfoBasic{PublicKeyBase58Check: publicKeyBase58Check, Username: string(profileEntry.Username)}
})

// By default, set the current View to the tip block's view. The GetView() function is safe to use
// whether we are on PoW or PoS.
currentView := currentTip.Header.GetView()

// Try to fetch the current Fast-HotStuff view. If the server is running the Fast-HotStuff consensus,
// then this will return a non-zero value. This value always overrides the tip block's current view.
fastHotStuffConsensusView := fes.backendServer.GetLatestView()
if fastHotStuffConsensusView != 0 {
currentView = fastHotStuffConsensusView
}

// If the current tip is at or past the final PoW block height, but we don't have a view returned by the
// Fast-HotStuff consensus, then we can estimate the current view based on the Fast-HotStuff rules. This
// is the best fallback value we can use once the chain has transitioned to PoS.
if currentView == 0 && currentTip.Header.Height >= fes.Params.GetFinalPoWBlockHeight() {
timeoutDuration := time.Duration(utxoView.GetCurrentGlobalParamsEntry().TimeoutIntervalMillisecondsPoS) * time.Millisecond
currentTipTimestamp := time.Unix(0, currentTip.Header.TstampNanoSecs)
currentView = currentTip.Header.GetView() + estimateNumTimeoutsSinceTip(time.Now(), currentTipTimestamp, timeoutDuration)
}

// Construct the response
response := GetEpochProgressResponse{
EpochEntry: *currentEpochEntry,
LeaderSchedule: leaderSchedule,
CurrentView: currentView,
CurrentTipHeight: currentTip.Header.Height,
}

// Encode response.
if err = json.NewEncoder(ww).Encode(response); err != nil {
_AddInternalServerError(ww, "GetValidatorByPublicKeyBase58Check: problem encoding response as JSON")
return
}
}

// estimateNumTimeoutsSinceTip computes the number for PoS timeouts that have occurred since a tip block
// with the provided timestamp. It simulates the same math as in consensus and works whether the current
// node is running a PoS validator or not.
//
// Examples:
// - Current time = 8:59:00, tip time = 09:00:00, timeout duration = 1 min => 0 timeouts
// - Current time = 9:00:00, tip time = 09:00:00, timeout duration = 1 min => 0 timeouts
// - Current time = 9:01:00, tip time = 09:00:00, timeout duration = 1 min => 1 timeout
// - Current time = 9:02:00, tip time = 09:00:00, timeout duration = 1 min => 1 timeout
// - Current time = 9:03:00, tip time = 09:00:00, timeout duration = 1 min + 2 mins => 2 timeout
// - Current time = 9:05:00, tip time = 09:00:00, timeout duration = 1 min + 2 mins => 2 timeout
// - Current time = 9:07:00, tip time = 09:00:00, timeout duration = 1 min + 2 mins + 4 mins => 3 timeout
// - Current time = 9:14:59, tip time = 09:00:00, timeout duration = 1 min + 2 mins + 4 mins => 3 timeout
// - Current time = 9:15:00, tip time = 09:00:00, timeout duration = 1 min + 2 mins + 4 mins + 8 mins => 4 timeout
func estimateNumTimeoutsSinceTip(currentTimestamp time.Time, tipTimestamp time.Time, timeoutDuration time.Duration) uint64 {
// Count the number of timeouts.
numTimeouts := uint64(0)

// The first timeout occurs after the timeout duration elapses starting from the tip's
// timestamp. We use the updated timestamp as the starting time for the first timeout.
tipTimestampAndTimeouts := tipTimestamp.Add(timeoutDuration)

// Once the tip timestamp + cumulative timeout exceed the current time, then we have found
// the exact number of timeouts that have elapsed
for tipTimestampAndTimeouts.Compare(currentTimestamp) <= 0 {
// The timeout duration doubles on every timeout.
timeoutDuration *= 2

// The next timeout occurs after the timeout duration elapses after the previous timeout.
tipTimestampAndTimeouts = tipTimestampAndTimeouts.Add(timeoutDuration)

// Increment the number of timeouts.
numTimeouts++
}

return numTimeouts
}

type CheckNodeStatusRequest struct {
NodeHostPort string `safeForLogging:"true"`
}
Expand Down
76 changes: 76 additions & 0 deletions routes/validators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/deso-protocol/core/bls"
"github.com/deso-protocol/core/lib"
Expand Down Expand Up @@ -154,6 +155,81 @@ func TestValidatorRegistration(t *testing.T) {
}
}

func TestEstimateNumTimeoutsSinceTip(t *testing.T) {
// tip time = 9:00:00
tipTime := time.Date(2021, 1, 1, 9, 0, 0, 0, time.UTC)

// Current time = 8:59:00, tip time = 09:00:00, timeout duration = 1 min => 0 timeouts
{
currentTimestamp := tipTime.Add(-time.Minute)

numTimeouts := estimateNumTimeoutsSinceTip(currentTimestamp, tipTime, time.Minute)
require.Equal(t, numTimeouts, uint64(0))
}

// Current time = 9:00:00, tip time = 09:00:00, timeout duration = 1 min => 0 timeouts
{
numTimeouts := estimateNumTimeoutsSinceTip(tipTime, tipTime, time.Minute)
require.Equal(t, numTimeouts, uint64(0))
}

// Current time = 9:01:00, tip time = 09:00:00, timeout duration = 1 min => 1 timeout
{
currentTimestamp := tipTime.Add(time.Minute)

numTimeouts := estimateNumTimeoutsSinceTip(currentTimestamp, tipTime, time.Minute)
require.Equal(t, numTimeouts, uint64(1))
}

// Current time = 9:02:00, tip time = 09:00:00, timeout duration = 1 min => 1 timeout
{
currentTimestamp := tipTime.Add(2 * time.Minute)

numTimeouts := estimateNumTimeoutsSinceTip(currentTimestamp, tipTime, time.Minute)
require.Equal(t, numTimeouts, uint64(1))
}

// Current time = 9:03:00, tip time = 09:00:00, timeout duration = 1 min + 2 mins => 2 timeout
{
currentTimestamp := tipTime.Add(3 * time.Minute)

numTimeouts := estimateNumTimeoutsSinceTip(currentTimestamp, tipTime, time.Minute)
require.Equal(t, numTimeouts, uint64(2))
}

// Current time = 9:05:00, tip time = 09:00:00, timeout duration = 1 min + 2 mins => 2 timeout
{
currentTimestamp := tipTime.Add(5 * time.Minute)

numTimeouts := estimateNumTimeoutsSinceTip(currentTimestamp, tipTime, time.Minute)
require.Equal(t, numTimeouts, uint64(2))
}

// Current time = 9:07:00, tip time = 09:00:00, timeout duration = 1 min + 2 mins + 4 mins => 3 timeout
{
currentTimestamp := tipTime.Add(7 * time.Minute)

numTimeouts := estimateNumTimeoutsSinceTip(currentTimestamp, tipTime, time.Minute)
require.Equal(t, numTimeouts, uint64(3))
}

// Current time = 9:14:59, tip time = 09:00:00, timeout duration = 1 min + 2 mins + 4 mins => 3 timeout
{
currentTimestamp := tipTime.Add(14 * time.Minute).Add(59 * time.Second)

numTimeouts := estimateNumTimeoutsSinceTip(currentTimestamp, tipTime, time.Minute)
require.Equal(t, numTimeouts, uint64(3))
}

// Current time = 9:15:00, tip time = 09:00:00, timeout duration = 1 min + 2 mins + 4 mins + 8 mins => 4 timeout
{
currentTimestamp := tipTime.Add(15 * time.Minute)

numTimeouts := estimateNumTimeoutsSinceTip(currentTimestamp, tipTime, time.Minute)
require.Equal(t, numTimeouts, uint64(4))
}
}

func _generateVotingPublicKeyAndAuthorization(t *testing.T, transactorPkBytes []byte) (*bls.PublicKey, *bls.Signature) {
blsPrivateKey, err := bls.NewPrivateKey()
require.NoError(t, err)
Expand Down