diff --git a/grpc/grpc.go b/grpc/grpc.go index 0e82e29..95a6eab 100644 --- a/grpc/grpc.go +++ b/grpc/grpc.go @@ -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, diff --git a/grpc/grpc_test.go b/grpc/grpc_test.go index 5060cb9..7589a81 100644 --- a/grpc/grpc_test.go +++ b/grpc/grpc_test.go @@ -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) + } + }) + } +} diff --git a/routes_handler.go b/routes_handler.go index 3a9132c..f3b5d9e 100644 --- a/routes_handler.go +++ b/routes_handler.go @@ -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