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

[Merged by Bors] - Add jitter to spread out requests to get poet proof and submit challenge #4871

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
44 changes: 39 additions & 5 deletions activation/nipost.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"math/rand"
"time"

"github.com/spacemeshos/merkle-tree"
Expand All @@ -22,6 +23,24 @@ import (
"github.com/spacemeshos/go-spacemesh/signing"
)

const (
// Jitter values to avoid all nodes querying the poet at the same time.
// Note: the jitter values are represented as a percentage of cycle gap.
// mainnet cycle-gap: 12h
// systest cycle-gap: 30s.

// Minimum jitter value before querying for the proof.
// Gives the poet service time to generate proof after a round ends (~8s on mainnet).
// mainnet -> 8.64s
// systest -> 0.36s.
MinPoetGetProofJitter = 0.02

// The maximum jitter value before querying for the proof.
// mainnet -> 17.28s
// systest -> 0.72s.
MaxPoetGetProofJitter = 0.04
)

//go:generate mockgen -package=activation -destination=./nipost_mocks.go -source=./nipost.go PoetProvingServiceClient

// PoetProvingServiceClient provides a gateway to a trust-less public proving service, which may serve many PoET
Expand Down Expand Up @@ -385,12 +404,9 @@ func (nb *NIPostBuilder) getBestProof(ctx context.Context, challenge types.Hash3
continue
}
round := r.PoetRound.ID
// Time to wait before querying for the proof
// The additional second is an optimization to be nicer to poet
// and don't accidentally ask it to soon and have to retry.
waitTime := time.Until(r.PoetRound.End.IntoTime()) + time.Second
waitTime, jitter := calcGetProofWaitTime(r.PoetRound.End.IntoTime(), nb.poetCfg.CycleGap)
eg.Go(func() error {
logger.With().Info("waiting till poet round end", log.Duration("wait time", waitTime))
logger.With().Info("waiting till poet round end", log.Duration("wait time", waitTime), log.Duration("jitter", jitter))
poszu marked this conversation as resolved.
Show resolved Hide resolved
select {
case <-ctx.Done():
return fmt.Errorf("waiting to query proof: %w", ctx.Err())
Expand Down Expand Up @@ -479,3 +495,21 @@ func constructMerkleProof(challenge types.Hash32, members []types.Member) (*type
Nodes: nodesH32,
}, nil
}

func randomDurationInRange(min, max time.Duration) time.Duration {
return min + time.Duration(float64((max-min).Nanoseconds())*rand.Float64())
poszu marked this conversation as resolved.
Show resolved Hide resolved
}

// Time to wait before querying for the proof
// We add a jitter to avoid all nodes querying for the proof at the same time.
// There's no need to add jitter if the round has already ended.
func calcGetProofWaitTime(roundEnd time.Time, cycleGap time.Duration) (waitTime, jitter time.Duration) {
waitTime = time.Until(roundEnd)
if waitTime > 0 {
poszu marked this conversation as resolved.
Show resolved Hide resolved
minWaitTime := time.Duration(float64(cycleGap) * MinPoetGetProofJitter / 100.0)
maxWaitTime := time.Duration(float64(cycleGap) * MaxPoetGetProofJitter / 100.0)
jitter := randomDurationInRange(minWaitTime, maxWaitTime)
return waitTime, jitter
poszu marked this conversation as resolved.
Show resolved Hide resolved
}
return 0, 0
}
42 changes: 42 additions & 0 deletions activation/nipost_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1054,3 +1054,45 @@ func FuzzBuilderStateConsistency(f *testing.F) {
func FuzzBuilderStateSafety(f *testing.F) {
tester.FuzzSafety[types.NIPostBuilderState](f)
}

func TestRandomDurationInRange(t *testing.T) {
t.Parallel()
test := func(min, max time.Duration) {
for i := 0; i < 100; i++ {
waittime := randomDurationInRange(min, max)
require.LessOrEqual(t, waittime, max)
require.GreaterOrEqual(t, waittime, min)
}
}
t.Run("min = 0", func(t *testing.T) {
t.Parallel()
test(0, 7*time.Second)
})
t.Run("min != 0", func(t *testing.T) {
t.Parallel()
test(5*time.Second, 7*time.Second)
})
}

func TestCalculatingGetProofWaitTime(t *testing.T) {
t.Parallel()
t.Run("past round end", func(t *testing.T) {
t.Parallel()
roundEnd := time.Now().Add(-time.Hour)
waitTime, jitter := calcGetProofWaitTime(roundEnd, time.Hour*12)

require.Equal(t, time.Duration(0), waitTime)
require.Equal(t, time.Duration(0), jitter)
})
t.Run("before round end", func(t *testing.T) {
t.Parallel()
roundEnd := time.Now().Add(time.Hour)
waitTime, jitter := calcGetProofWaitTime(roundEnd, time.Hour*12)

require.Greater(t, waitTime, time.Duration(0))
require.LessOrEqual(t, waitTime, time.Hour)

require.NotZero(t, jitter)
require.LessOrEqual(t, jitter, time.Duration(12*float64(time.Hour)*MaxPoetGetProofJitter/100))
})
}