Skip to content
Open
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
15 changes: 15 additions & 0 deletions grpc/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,21 @@ func (info *JsonInfoV2) ExpectedNext() (expectedTime int64, expectedRound uint64
return current*p + info.GenesisTime, uint64(current) + 1
}

// SecondsUntilRound returns the number of seconds from now until the given round becomes available.
// Round N happens at GenesisTime + (N-1)*Period. Used for cache duration when returning "future beacon" errors.
func (info *JsonInfoV2) SecondsUntilRound(round uint64) int64 {
if round == 0 {
return 0
}
p := int64(info.Period)
roundTime := info.GenesisTime + int64(round-1)*p
secs := roundTime - clock().Unix()
if secs < 0 {
return 0
}
return secs
}

func (j *JsonInfoV2) V1() *JsonInfoV1 {
return &JsonInfoV1{
PublicKey: j.PublicKey,
Expand Down
59 changes: 59 additions & 0 deletions grpc/grpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,62 @@ func TestNextBeaconTime(t *testing.T) {
})
}
}

func TestSecondsUntilRound(t *testing.T) {
now := int64(1718551765)
clock = func() time.Time {
return time.Unix(now, 0)
}

tests := []struct {
name string
info *JsonInfoV2
round uint64
wantSecs int64
}{
{
name: "next round in 5 seconds",
info: &JsonInfoV2{
Period: 10,
GenesisTime: now - 25,
},
round: 4,
wantSecs: 5,
},
{
name: "round 0 returns 0",
info: &JsonInfoV2{
Period: 10,
GenesisTime: now - 25,
},
round: 0,
wantSecs: 0,
},
{
name: "round already passed returns 0",
info: &JsonInfoV2{
Period: 10,
GenesisTime: now - 100,
},
round: 5,
wantSecs: 0,
},
{
name: "round in 30 seconds",
info: &JsonInfoV2{
Period: 30,
GenesisTime: now,
},
round: 2,
wantSecs: 30,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.info.SecondsUntilRound(tt.round)
if got != tt.wantSecs {
t.Errorf("SecondsUntilRound() = %v, want %v", got, tt.wantSecs)
}
})
}
}
6 changes: 3 additions & 3 deletions routes_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ func getBeacon(c *grpc.Client, r *http.Request, round uint64) (*grpc.HexBeacon,
// we refuse rounds too far in the future
if round >= nextRound+1 {
slog.Error("[GetBeacon] Future beacon was requested, unexpected", "requested", round, "expected", nextRound, "from", r.RemoteAddr)
// TODO: we could have a more precise nextTime value instead of just period
// we return the negative time to cache this response
return nil, -int64(info.Period), fmt.Errorf("future beacon was requested")
// Use precise time until next round for cache duration instead of full period
preciseCacheSeconds := info.SecondsUntilRound(nextRound)
return nil, -preciseCacheSeconds, fmt.Errorf("future beacon was requested")
}

var beacon *grpc.HexBeacon
Expand Down