Skip to content

Commit 313d882

Browse files
authored
DS-1553 TS Precision (#204)
* Timestamp precision in encoding options
1 parent d7a44d4 commit 313d882

7 files changed

+598
-51
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ toolchain go1.24.4
77
require (
88
github.com/ethereum/go-ethereum v1.15.3
99
github.com/expr-lang/expr v1.17.5
10-
github.com/goccy/go-json v0.10.4
10+
github.com/goccy/go-json v0.10.5
1111
github.com/hashicorp/go-plugin v1.6.3
1212
github.com/klauspost/compress v1.18.0
1313
github.com/leanovate/gopter v0.2.11

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIx
190190
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
191191
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
192192
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
193+
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
194+
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
193195
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
194196
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
195197
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=

llo/reportcodecs/evm/report_codec_common.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,70 @@ import (
1616
ubig "github.com/smartcontractkit/chainlink-data-streams/llo/reportcodecs/evm/utils"
1717
)
1818

19+
// TimestampPrecision represents the precision for timestamp conversion
20+
type TimestampPrecision uint8
21+
22+
const (
23+
PrecisionSeconds TimestampPrecision = iota
24+
PrecisionMilliseconds
25+
PrecisionMicroseconds
26+
PrecisionNanoseconds
27+
)
28+
29+
func (tp TimestampPrecision) MarshalJSON() ([]byte, error) {
30+
var s string
31+
switch tp {
32+
case PrecisionSeconds:
33+
s = "s"
34+
case PrecisionMilliseconds:
35+
s = "ms"
36+
case PrecisionMicroseconds:
37+
s = "us"
38+
case PrecisionNanoseconds:
39+
s = "ns"
40+
default:
41+
return nil, fmt.Errorf("invalid timestamp precision %d", tp)
42+
}
43+
return json.Marshal(s)
44+
}
45+
46+
// UnmarshalJSON unmarshals TimestampPrecision from JSON - used to unmarshal from the Opts structs.
47+
func (tp *TimestampPrecision) UnmarshalJSON(data []byte) error {
48+
var s string
49+
if err := json.Unmarshal(data, &s); err != nil {
50+
return err
51+
}
52+
switch s {
53+
case "s":
54+
*tp = PrecisionSeconds
55+
case "ms":
56+
*tp = PrecisionMilliseconds
57+
case "us":
58+
*tp = PrecisionMicroseconds
59+
case "ns":
60+
*tp = PrecisionNanoseconds
61+
default:
62+
return fmt.Errorf("invalid timestamp precision %q", s)
63+
}
64+
return nil
65+
}
66+
67+
// ConvertTimestamp converts a nanosecond timestamp to a specified precision.
68+
func ConvertTimestamp(timestampNanos uint64, precision TimestampPrecision) uint64 {
69+
switch precision {
70+
case PrecisionSeconds:
71+
return timestampNanos / 1e9
72+
case PrecisionMilliseconds:
73+
return timestampNanos / 1e6
74+
case PrecisionMicroseconds:
75+
return timestampNanos / 1e3
76+
case PrecisionNanoseconds:
77+
return timestampNanos
78+
default:
79+
return timestampNanos
80+
}
81+
}
82+
1983
// Extracts nanosecond timestamps as uint32 number of seconds
2084
func ExtractTimestamps(report llo.Report) (validAfterSeconds, observationTimestampSeconds uint32, err error) {
2185
vas := report.ValidAfterNanoseconds / 1e9

llo/reportcodecs/evm/report_codec_evm_abi_encode_unpacked.go

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"errors"
66
"fmt"
7+
"math"
78
"math/big"
89

910
"github.com/goccy/go-json"
@@ -55,6 +56,10 @@ type ReportFormatEVMABIEncodeOpts struct {
5556
// top-level elements in this ABI array (stream 0 is always the native
5657
// token price and stream 1 is the link token price).
5758
ABI []ABIEncoder `json:"abi"`
59+
// TimestampPrecision is the precision of the timestamps in the report.
60+
// Seconds use uint32 ABI encoding, while milliseconds/microseconds/nanoseconds use uint64.
61+
// Defaults to "s" (seconds) if not specified.
62+
TimestampPrecision TimestampPrecision `json:"timestampPrecision,omitempty"`
5863
}
5964

6065
func (r *ReportFormatEVMABIEncodeOpts) Decode(opts []byte) error {
@@ -69,11 +74,11 @@ func (r *ReportFormatEVMABIEncodeOpts) Encode() ([]byte, error) {
6974

7075
type BaseReportFields struct {
7176
FeedID common.Hash
72-
ValidFromTimestamp uint32
73-
Timestamp uint32
77+
ValidFromTimestamp uint64
78+
Timestamp uint64
7479
NativeFee *big.Int
7580
LinkFee *big.Int
76-
ExpiresAt uint32
81+
ExpiresAt uint64
7782
}
7883

7984
func (r ReportCodecEVMABIEncodeUnpacked) Encode(report llo.Report, cd llotypes.ChannelDefinition) ([]byte, error) {
@@ -100,21 +105,19 @@ func (r ReportCodecEVMABIEncodeUnpacked) Encode(report llo.Report, cd llotypes.C
100105
return nil, fmt.Errorf("failed to decode opts; got: '%s'; %w", cd.Opts, err)
101106
}
102107

103-
validAfterSeconds, observationTimestampSeconds, err := ExtractTimestamps(report)
104-
if err != nil {
105-
return nil, fmt.Errorf("failed to extract timestamps; %w", err)
106-
}
108+
validAfter := ConvertTimestamp(report.ValidAfterNanoseconds, opts.TimestampPrecision)
109+
observationTimestamp := ConvertTimestamp(report.ObservationTimestampNanoseconds, opts.TimestampPrecision)
107110

108111
rf := BaseReportFields{
109112
FeedID: opts.FeedID,
110-
ValidFromTimestamp: validAfterSeconds + 1,
111-
Timestamp: observationTimestampSeconds,
113+
ValidFromTimestamp: validAfter + 1,
114+
Timestamp: observationTimestamp,
112115
NativeFee: CalculateFee(nativePrice, opts.BaseUSDFee),
113116
LinkFee: CalculateFee(linkPrice, opts.BaseUSDFee),
114-
ExpiresAt: observationTimestampSeconds + opts.ExpirationWindow,
117+
ExpiresAt: observationTimestamp + uint64(opts.ExpirationWindow),
115118
}
116119

117-
header, err := r.buildHeader(rf)
120+
header, err := r.buildHeader(rf, opts.TimestampPrecision)
118121
if err != nil {
119122
return nil, fmt.Errorf("failed to build base report; %w", err)
120123
}
@@ -179,9 +182,12 @@ func (r ReportCodecEVMABIEncodeUnpacked) Verify(cd llotypes.ChannelDefinition) e
179182
// EVMABIEncodeUnpacked reports.
180183
//
181184
// An arbitrary payload will be appended to this.
182-
var BaseSchema = getBaseSchema()
185+
var (
186+
BaseSchemaUint32 = getBaseSchema("uint32")
187+
BaseSchemaUint64 = getBaseSchema("uint64")
188+
)
183189

184-
func getBaseSchema() abi.Arguments {
190+
func getBaseSchema(timestampType string) abi.Arguments {
185191
mustNewType := func(t string) abi.Type {
186192
result, err := abi.NewType(t, "", []abi.ArgumentMarshaling{})
187193
if err != nil {
@@ -191,15 +197,15 @@ func getBaseSchema() abi.Arguments {
191197
}
192198
return abi.Arguments([]abi.Argument{
193199
{Name: "feedId", Type: mustNewType("bytes32")},
194-
{Name: "validFromTimestamp", Type: mustNewType("uint32")},
195-
{Name: "observationsTimestamp", Type: mustNewType("uint32")},
200+
{Name: "validFromTimestamp", Type: mustNewType(timestampType)},
201+
{Name: "observationsTimestamp", Type: mustNewType(timestampType)},
196202
{Name: "nativeFee", Type: mustNewType("uint192")},
197203
{Name: "linkFee", Type: mustNewType("uint192")},
198-
{Name: "expiresAt", Type: mustNewType("uint32")},
204+
{Name: "expiresAt", Type: mustNewType(timestampType)},
199205
})
200206
}
201207

202-
func (r ReportCodecEVMABIEncodeUnpacked) buildHeader(rf BaseReportFields) ([]byte, error) {
208+
func (r ReportCodecEVMABIEncodeUnpacked) buildHeader(rf BaseReportFields, precision TimestampPrecision) ([]byte, error) {
203209
var merr error
204210
if rf.LinkFee == nil {
205211
merr = errors.Join(merr, errors.New("linkFee may not be nil"))
@@ -214,7 +220,38 @@ func (r ReportCodecEVMABIEncodeUnpacked) buildHeader(rf BaseReportFields) ([]byt
214220
if merr != nil {
215221
return nil, merr
216222
}
217-
b, err := BaseSchema.Pack(rf.FeedID, rf.ValidFromTimestamp, rf.Timestamp, rf.NativeFee, rf.LinkFee, rf.ExpiresAt)
223+
224+
var b []byte
225+
var err error
226+
if precision == PrecisionSeconds {
227+
if rf.ValidFromTimestamp > math.MaxUint32 {
228+
return nil, fmt.Errorf("validFromTimestamp %d exceeds uint32 range", rf.ValidFromTimestamp)
229+
}
230+
if rf.Timestamp > math.MaxUint32 {
231+
return nil, fmt.Errorf("timestamp %d exceeds uint32 range", rf.Timestamp)
232+
}
233+
if rf.ExpiresAt > math.MaxUint32 {
234+
return nil, fmt.Errorf("expiresAt %d exceeds uint32 range", rf.ExpiresAt)
235+
}
236+
b, err = BaseSchemaUint32.Pack(
237+
rf.FeedID,
238+
uint32(rf.ValidFromTimestamp),
239+
uint32(rf.Timestamp),
240+
rf.NativeFee,
241+
rf.LinkFee,
242+
uint32(rf.ExpiresAt),
243+
)
244+
} else {
245+
b, err = BaseSchemaUint64.Pack(
246+
rf.FeedID,
247+
rf.ValidFromTimestamp,
248+
rf.Timestamp,
249+
rf.NativeFee,
250+
rf.LinkFee,
251+
rf.ExpiresAt,
252+
)
253+
}
254+
218255
if err != nil {
219256
return nil, fmt.Errorf("failed to pack base report blob; %w", err)
220257
}

llo/reportcodecs/evm/report_codec_evm_abi_encode_unpacked_expr.go

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package evm
33
import (
44
"errors"
55
"fmt"
6+
"math"
67

78
"github.com/ethereum/go-ethereum/common"
89

@@ -48,21 +49,19 @@ func (r ReportCodecEVMABIEncodeUnpackedExpr) Encode(report llo.Report, cd llotyp
4849
return nil, fmt.Errorf("failed to decode opts; got: '%s'; %w", cd.Opts, err)
4950
}
5051

51-
validAfterSeconds, observationTimestampSeconds, err := ExtractTimestamps(report)
52-
if err != nil {
53-
return nil, fmt.Errorf("failed to extract timestamps; %w", err)
54-
}
52+
validAfter := ConvertTimestamp(report.ValidAfterNanoseconds, opts.TimestampPrecision)
53+
observationTimestamp := ConvertTimestamp(report.ObservationTimestampNanoseconds, opts.TimestampPrecision)
5554

5655
rf := BaseReportFields{
5756
FeedID: opts.FeedID,
58-
ValidFromTimestamp: validAfterSeconds + 1,
59-
Timestamp: observationTimestampSeconds,
57+
ValidFromTimestamp: validAfter + 1,
58+
Timestamp: observationTimestamp,
6059
NativeFee: CalculateFee(nativePrice, opts.BaseUSDFee),
6160
LinkFee: CalculateFee(linkPrice, opts.BaseUSDFee),
62-
ExpiresAt: observationTimestampSeconds + opts.ExpirationWindow,
61+
ExpiresAt: observationTimestamp + uint64(opts.ExpirationWindow),
6362
}
6463

65-
header, err := r.buildHeader(rf)
64+
header, err := r.buildHeader(rf, opts.TimestampPrecision)
6665
if err != nil {
6766
return nil, fmt.Errorf("failed to build base report; %w", err)
6867
}
@@ -92,7 +91,7 @@ func (r ReportCodecEVMABIEncodeUnpackedExpr) Verify(cd llotypes.ChannelDefinitio
9291
return nil
9392
}
9493

95-
func (r ReportCodecEVMABIEncodeUnpackedExpr) buildHeader(rf BaseReportFields) ([]byte, error) {
94+
func (r ReportCodecEVMABIEncodeUnpackedExpr) buildHeader(rf BaseReportFields, precision TimestampPrecision) ([]byte, error) {
9695
var merr error
9796
if rf.LinkFee == nil {
9897
merr = errors.Join(merr, errors.New("linkFee may not be nil"))
@@ -107,7 +106,38 @@ func (r ReportCodecEVMABIEncodeUnpackedExpr) buildHeader(rf BaseReportFields) ([
107106
if merr != nil {
108107
return nil, merr
109108
}
110-
b, err := BaseSchema.Pack(rf.FeedID, rf.ValidFromTimestamp, rf.Timestamp, rf.NativeFee, rf.LinkFee, rf.ExpiresAt)
109+
110+
var b []byte
111+
var err error
112+
if precision == PrecisionSeconds {
113+
if rf.ValidFromTimestamp > math.MaxUint32 {
114+
return nil, fmt.Errorf("validFromTimestamp %d exceeds uint32 range", rf.ValidFromTimestamp)
115+
}
116+
if rf.Timestamp > math.MaxUint32 {
117+
return nil, fmt.Errorf("timestamp %d exceeds uint32 range", rf.Timestamp)
118+
}
119+
if rf.ExpiresAt > math.MaxUint32 {
120+
return nil, fmt.Errorf("expiresAt %d exceeds uint32 range", rf.ExpiresAt)
121+
}
122+
b, err = BaseSchemaUint32.Pack(
123+
rf.FeedID,
124+
uint32(rf.ValidFromTimestamp),
125+
uint32(rf.Timestamp),
126+
rf.NativeFee,
127+
rf.LinkFee,
128+
uint32(rf.ExpiresAt),
129+
)
130+
} else {
131+
b, err = BaseSchemaUint64.Pack(
132+
rf.FeedID,
133+
rf.ValidFromTimestamp,
134+
rf.Timestamp,
135+
rf.NativeFee,
136+
rf.LinkFee,
137+
rf.ExpiresAt,
138+
)
139+
}
140+
111141
if err != nil {
112142
return nil, fmt.Errorf("failed to pack base report blob; %w", err)
113143
}

0 commit comments

Comments
 (0)