Skip to content

Commit 3973850

Browse files
feat: support millisecond block times (#22)
`gastime.Time.FastForwardTo()` now supports fractional seconds, and `proxytime.ConvertMilliseconds()` is added to provide a conversion from ms to rate-denominated fractions. These are to be called at the beginning of a block, before transaction execution. Closes #16 --------- Signed-off-by: Arran Schlosberg <[email protected]> Co-authored-by: Michael Kaplan <[email protected]>
1 parent 1318968 commit 3973850

File tree

4 files changed

+168
-26
lines changed

4 files changed

+168
-26
lines changed

gastime/gastime.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,8 @@ func (tm *Time) Tick(g gas.Gas) {
134134

135135
// FastForwardTo is equivalent to [proxytime.Time.FastForwardTo] except that it
136136
// may also update the gas excess.
137-
func (tm *Time) FastForwardTo(to uint64) {
138-
sec, frac := tm.Time.FastForwardTo(to)
137+
func (tm *Time) FastForwardTo(to uint64, toFrac gas.Gas) {
138+
sec, frac := tm.Time.FastForwardTo(to, toFrac)
139139
if sec == 0 && frac.Numerator == 0 {
140140
return
141141
}

gastime/gastime_test.go

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,10 @@ func TestExcess(t *testing.T) {
177177
steps := []struct {
178178
desc string
179179
// Only one of fast-forwarding or ticking per step.
180-
ffToBefore uint64
181-
tickBefore gas.Gas
182-
want state
180+
ffToBefore uint64
181+
ffToBeforeFrac gas.Gas
182+
tickBefore gas.Gas
183+
want state
183184
}{
184185
{
185186
desc: "initial tick 1/2s",
@@ -235,6 +236,26 @@ func TestExcess(t *testing.T) {
235236
Excess: 45*rate/8 - 7*rate/8,
236237
},
237238
},
239+
{
240+
desc: "fast forward 0.5s to 13.5s",
241+
ffToBefore: 55,
242+
ffToBeforeFrac: rate / 2,
243+
want: state{
244+
UnixTime: 55,
245+
ConsumedThisSecond: frac(rate / 2),
246+
Excess: 45*rate/8 - 7*rate/8 - (rate/2)/2,
247+
},
248+
},
249+
{
250+
desc: "fast forward 0.75s to 14.25s",
251+
ffToBefore: 56,
252+
ffToBeforeFrac: rate / 4,
253+
want: state{
254+
UnixTime: 56,
255+
ConsumedThisSecond: frac(rate / 4),
256+
Excess: 45*rate/8 - 7*rate/8 - (rate/2)/2 - 3*(rate/4)/2,
257+
},
258+
},
238259
{
239260
desc: "fast forward causes overflow when seconds multiplied by R",
240261
ffToBefore: math.MaxUint64,
@@ -247,13 +268,16 @@ func TestExcess(t *testing.T) {
247268
}
248269

249270
for _, s := range steps {
250-
switch ff, tk := s.ffToBefore, s.tickBefore; {
251-
case ff > 0 && tk > 0:
271+
ffSec, ffFrac := s.ffToBefore, s.ffToBeforeFrac
272+
tick := s.tickBefore
273+
274+
switch ff := (ffSec > 0 || ffFrac > 0); {
275+
case ff && tick > 0:
252276
t.Fatalf("Bad test setup (%q) only FastForward() or Tick() before", s.desc)
253-
case ff > 0:
254-
tm.FastForwardTo(ff)
255-
case tk > 0:
256-
tm.Tick(tk)
277+
case ff:
278+
tm.FastForwardTo(ffSec, ffFrac)
279+
case tick > 0:
280+
tm.Tick(tick)
257281
}
258282
tm.requireState(t, s.desc, s.want, ignore)
259283
}

proxytime/proxytime.go

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -89,25 +89,55 @@ func (tm *Time[D]) Tick(d D) {
8989
tm.fraction = D(rem)
9090
}
9191

92-
// FastForwardTo sets the time to the specified Unix timestamp if it is in the
93-
// future, returning the integer and fraction number of seconds by which the
94-
// time was advanced. The fraction is always denominated in [Time.Rate].
95-
func (tm *Time[D]) FastForwardTo(to uint64) (uint64, FractionalSecond[D]) {
96-
if to <= tm.seconds {
92+
// FastForwardTo sets the time to the specified Unix timestamp and fraction of a
93+
// second, if it is in the future, returning the integer and fraction number of
94+
// seconds by which the time was advanced. Both the incoming argument and the
95+
// returned fraction are denominated in [Time.Rate]. Behaviour is undefined if
96+
// `to + toFrac / tm.Rate()` overflows 64 bits, which is impossible in practice.
97+
func (tm *Time[D]) FastForwardTo(to uint64, toFrac D) (uint64, FractionalSecond[D]) {
98+
to += uint64(toFrac / tm.hertz)
99+
toFrac %= tm.hertz
100+
101+
if !tm.isFuture(to, toFrac) {
97102
return 0, FractionalSecond[D]{0, tm.hertz}
98103
}
99104

100-
sec := to - tm.seconds
101-
var frac D
102-
if tm.fraction > 0 {
103-
frac = tm.hertz - tm.fraction
104-
sec--
105+
ffSec := to - tm.seconds
106+
ffFrac := FractionalSecond[D]{
107+
Denominator: tm.hertz,
108+
}
109+
110+
if tm.fraction > toFrac {
111+
ffSec--
112+
ffFrac.Numerator = tm.hertz - (tm.fraction - toFrac)
113+
} else {
114+
ffFrac.Numerator = toFrac - tm.fraction
105115
}
106116

107117
tm.seconds = to
108-
tm.fraction = 0
118+
tm.fraction = toFrac
119+
120+
return ffSec, ffFrac
121+
}
109122

110-
return sec, FractionalSecond[D]{frac, tm.hertz}
123+
func (tm *Time[D]) isFuture(sec uint64, num D) bool {
124+
if sec != tm.seconds {
125+
return sec > tm.seconds
126+
}
127+
return num > tm.fraction
128+
}
129+
130+
// ConvertMilliseconds returns `ms` as a number of seconds and a fraction of a
131+
// second, denominated in `rate` and rounded down if `rate` is not a multiple of
132+
// 1000.
133+
func ConvertMilliseconds[D Duration](rate D, ms uint64) (sec uint64, _ FractionalSecond[D]) {
134+
sec = ms / 1000
135+
ms %= 1000
136+
frac := FractionalSecond[D]{
137+
Denominator: rate,
138+
}
139+
frac.Numerator, _, _ = intmath.MulDiv(D(ms), rate, 1000) // overflow is impossible as ms < 1000
140+
return sec, frac
111141
}
112142

113143
// SetRate changes the unit rate at which time passes. The requisite integer

proxytime/proxytime_test.go

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,9 +246,16 @@ func TestFastForward(t *testing.T) {
246246
steps := []struct {
247247
tickBefore uint64
248248
ffTo uint64
249+
ffToFrac uint64
249250
wantSec uint64
250251
wantFrac FractionalSecond[uint64]
251252
}{
253+
{
254+
tickBefore: 0,
255+
ffTo: 41, // in the past
256+
wantSec: 0,
257+
wantFrac: frac(0, rate),
258+
},
252259
{
253260
tickBefore: 100, // 42.100
254261
ffTo: 42, // in the past
@@ -273,20 +280,101 @@ func TestFastForward(t *testing.T) {
273280
wantSec: 5,
274281
wantFrac: frac(800, rate),
275282
},
283+
{
284+
tickBefore: 0, // 50.000
285+
ffTo: 50,
286+
ffToFrac: 900,
287+
wantSec: 0,
288+
wantFrac: frac(900, rate),
289+
},
290+
{
291+
tickBefore: 0, // 50.900
292+
ffTo: 51,
293+
ffToFrac: 100,
294+
wantSec: 0,
295+
wantFrac: frac(200, rate),
296+
},
297+
{
298+
tickBefore: 100, // 51.200
299+
ffTo: 51,
300+
ffToFrac: 200,
301+
wantSec: 0,
302+
wantFrac: frac(0, rate),
303+
},
276304
}
277305

278306
for _, s := range steps {
279307
tm.Tick(s.tickBefore)
280-
gotSec, gotFrac := tm.FastForwardTo(s.ffTo)
281-
assert.Equal(t, s.wantSec, gotSec)
282-
assert.Equal(t, s.wantFrac, gotFrac)
308+
gotSec, gotFrac := tm.FastForwardTo(s.ffTo, s.ffToFrac)
309+
assert.Equal(t, s.wantSec, gotSec, "Fast-forwarded seconds")
310+
assert.Equal(t, s.wantFrac, gotFrac, "Fast-forwarded fractional numerator")
283311

284312
if t.Failed() {
285313
t.FailNow()
286314
}
287315
}
288316
}
289317

318+
func TestConvertMilliseconds(t *testing.T) {
319+
type Hz uint64
320+
321+
tests := []struct {
322+
rate Hz
323+
ms uint64
324+
wantSec uint64
325+
wantNumerator Hz // ms * (rate / 1000)
326+
}{
327+
{
328+
rate: 1000,
329+
ms: 42,
330+
wantNumerator: 42,
331+
},
332+
{
333+
rate: 1234 * 2,
334+
ms: 1000 / 2,
335+
wantNumerator: 1234,
336+
},
337+
{
338+
rate: 98765 * 4,
339+
ms: 1000 / 4,
340+
wantNumerator: 98765,
341+
},
342+
{
343+
rate: 142857 * 1000,
344+
ms: 1,
345+
wantNumerator: 142857,
346+
},
347+
{
348+
rate: 1_001,
349+
ms: 500,
350+
wantNumerator: 500,
351+
},
352+
{
353+
rate: 1000,
354+
ms: 1001,
355+
wantSec: 1,
356+
wantNumerator: 1,
357+
},
358+
{
359+
rate: 1000,
360+
ms: 314_159,
361+
wantSec: 314,
362+
wantNumerator: 159,
363+
},
364+
}
365+
366+
for _, tt := range tests {
367+
gotSec, gotFrac := ConvertMilliseconds(tt.rate, tt.ms)
368+
wantFrac := FractionalSecond[Hz]{
369+
Numerator: tt.wantNumerator,
370+
Denominator: tt.rate,
371+
}
372+
if gotSec != tt.wantSec || gotFrac != wantFrac {
373+
t.Errorf("ConvertMilliseconds(%d, %d) got (%v, %v); want (%v, %v)", tt.ms, tt.rate, gotSec, gotFrac, tt.wantSec, wantFrac)
374+
}
375+
}
376+
}
377+
290378
func TestCmpUnix(t *testing.T) {
291379
tests := []struct {
292380
tm *Time[uint64]

0 commit comments

Comments
 (0)