Skip to content

Commit

Permalink
Add a ReportCodec implementation that uses the generic Codec, fallbac…
Browse files Browse the repository at this point in the history
…k to using the current specific ReportCodec if the generic codec is not provided
  • Loading branch information
nolag committed Dec 13, 2023
1 parent 9a85999 commit 9d0281b
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 9 deletions.
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ go 1.21.3

require (
github.com/hashicorp/go-plugin v1.5.2
github.com/smartcontractkit/chainlink-common v0.1.7-0.20231206181640-faad3f11cfad
github.com/smartcontractkit/chainlink-common v0.1.7-0.20231212210052-211c6b796115
github.com/smartcontractkit/libocr v0.0.0-20231020123319-d255366a6545
github.com/stretchr/testify v1.8.4
)

require (
Expand Down Expand Up @@ -40,7 +41,6 @@ require (
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.1 // indirect
github.com/stretchr/testify v1.8.4 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.45.0 // indirect
go.opentelemetry.io/otel v1.19.0 // indirect
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,10 @@ github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/smartcontractkit/chainlink-common v0.1.7-0.20231206181640-faad3f11cfad h1:ysPjfbCPJuVxxFZa1Ifv8OPE20pzvnEHjJrPDUo4gT0=
github.com/smartcontractkit/chainlink-common v0.1.7-0.20231206181640-faad3f11cfad/go.mod h1:IdlfCN9rUs8Q/hrOYe8McNBIwEOHEsi0jilb3Cw77xs=
github.com/smartcontractkit/chainlink-common v0.1.7-0.20231211220101-d2e5d984e9fe h1:dbJ/3cQNVrkQVvCvFNQgxbIka+kzUgEiAgomBbxRn8E=
github.com/smartcontractkit/chainlink-common v0.1.7-0.20231211220101-d2e5d984e9fe/go.mod h1:IdlfCN9rUs8Q/hrOYe8McNBIwEOHEsi0jilb3Cw77xs=
github.com/smartcontractkit/chainlink-common v0.1.7-0.20231212210052-211c6b796115 h1:ZYcKlUV3r1a1hbL/tHqVcM2fkw5ap09RCv7NitGJnjs=
github.com/smartcontractkit/chainlink-common v0.1.7-0.20231212210052-211c6b796115/go.mod h1:IdlfCN9rUs8Q/hrOYe8McNBIwEOHEsi0jilb3Cw77xs=
github.com/smartcontractkit/go-plugin v0.0.0-20231003134350-e49dad63b306 h1:ko88+ZznniNJZbZPWAvHQU8SwKAdHngdDZ+pvVgB5ss=
github.com/smartcontractkit/go-plugin v0.0.0-20231003134350-e49dad63b306/go.mod h1:w1sAEES3g3PuV/RzUrgow20W2uErMly84hhD3um1WL4=
github.com/smartcontractkit/grpc-proxy v0.0.0-20230731113816-f1be6620749f h1:hgJif132UCdjo8u43i7iPN1/MFnu49hv7lFGFftCHKU=
Expand Down
48 changes: 48 additions & 0 deletions median/aggregated_attribute_observation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package median

import (
"cmp"
"math/big"
"slices"
"sort"

"github.com/smartcontractkit/libocr/commontypes"
"github.com/smartcontractkit/libocr/offchainreporting2/reportingplugin/median"
)

type aggregatedAttributedObservation struct {
Timestamp uint32
Observers [32]commontypes.OracleID
Observations []*big.Int
JuelsPerFeeCoin *big.Int
}

func aggregate(observations []median.ParsedAttributedObservation) *aggregatedAttributedObservation {
// defensive copy
n := len(observations)
observations = append([]median.ParsedAttributedObservation{}, observations...)

aggregated := &aggregatedAttributedObservation{Observations: make([]*big.Int, len(observations))}

slices.SortFunc(observations, func(a, b median.ParsedAttributedObservation) int {
return cmp.Compare(a.Timestamp, b.Timestamp)
})
aggregated.Timestamp = observations[n/2].Timestamp

// get median juelsPerFeeCoin
sort.Slice(observations, func(i, j int) bool {
return observations[i].JuelsPerFeeCoin.Cmp(observations[j].JuelsPerFeeCoin) < 0
})
aggregated.JuelsPerFeeCoin = observations[n/2].JuelsPerFeeCoin

// sort by values
sort.Slice(observations, func(i, j int) bool {
return observations[i].Value.Cmp(observations[j].Value) < 0
})

for i, o := range observations {
aggregated.Observers[i] = o.Observer
aggregated.Observations[i] = o.Value
}
return aggregated
}
23 changes: 18 additions & 5 deletions median/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
"github.com/smartcontractkit/chainlink-common/pkg/types"
)

const contractName = "median"

type Plugin struct {
loop.Plugin
stop services.StopChan
Expand All @@ -27,6 +29,7 @@ func (p *Plugin) NewMedianFactory(ctx context.Context, provider types.MedianProv
var ctxVals loop.ContextValues
ctxVals.SetValues(ctx)
lggr := logger.With(p.Logger, ctxVals.Args()...)

factory := median.NumericalMedianFactory{
DataSource: dataSource,
JuelsPerFeeCoinDataSource: juelsPerFeeCoin,
Expand All @@ -38,13 +41,24 @@ func (p *Plugin) NewMedianFactory(ctx context.Context, provider types.MedianProv
}
}),
OnchainConfigCodec: provider.OnchainConfigCodec(),
ReportCodec: provider.ReportCodec(),
}

if cr := provider.ChainReader(); cr != nil {
factory.ContractTransmitter = &chainReaderContract{cr, types.BoundContract{Name: "median"}}
factory.ContractTransmitter = &chainReaderContract{chainReader: cr}
} else {
factory.ContractTransmitter = provider.MedianContract()
}

if codec := provider.Codec(); codec != nil {
var err error
if factory.ReportCodec, err = newReportCodec(codec); err != nil {
return nil, err
}
} else {
lggr.Warn("No codec provided, defaulting back to median specific ReportCodec")
factory.ReportCodec = provider.ReportCodec()
}

s := &reportingPluginFactoryService{lggr: logger.Named(lggr, "ReportingPluginFactory"), ReportingPluginFactory: factory}

p.SubService(s)
Expand Down Expand Up @@ -75,7 +89,6 @@ func (r *reportingPluginFactoryService) HealthReport() map[string]error {
// chainReaderContract adapts a [types.ChainReader] to [median.MedianContract].
type chainReaderContract struct {
chainReader types.ChainReader
contract types.BoundContract
}

type latestTransmissionDetailsResponse struct {
Expand All @@ -95,7 +108,7 @@ type latestRoundRequested struct {
func (c *chainReaderContract) LatestTransmissionDetails(ctx context.Context) (configDigest ocrtypes.ConfigDigest, epoch uint32, round uint8, latestAnswer *big.Int, latestTimestamp time.Time, err error) {
var resp latestTransmissionDetailsResponse

err = c.chainReader.GetLatestValue(ctx, c.contract, "LatestTransmissionDetails", nil, &resp)
err = c.chainReader.GetLatestValue(ctx, contractName, "LatestTransmissionDetails", nil, &resp)
if err != nil {
return
}
Expand All @@ -106,7 +119,7 @@ func (c *chainReaderContract) LatestTransmissionDetails(ctx context.Context) (co
func (c *chainReaderContract) LatestRoundRequested(ctx context.Context, lookback time.Duration) (configDigest ocrtypes.ConfigDigest, epoch uint32, round uint8, err error) {
var resp latestRoundRequested

err = c.chainReader.GetLatestValue(ctx, c.contract, "LatestRoundReported", map[string]any{"lookback": lookback}, &resp)
err = c.chainReader.GetLatestValue(ctx, contractName, "LatestRoundReported", map[string]any{"lookback": lookback}, &resp)
if err != nil {
return
}
Expand Down
44 changes: 44 additions & 0 deletions median/report_codec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package median

import (
"context"
"errors"
"math/big"

"github.com/smartcontractkit/chainlink-common/pkg/types"
"github.com/smartcontractkit/libocr/offchainreporting2/reportingplugin/median"
ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types"
)

const typeName = "MedianReport"

func newReportCodec(codec types.Codec) (median.ReportCodec, error) {
if codec == nil {
return nil, errors.New("codec cannot be nil")
}
return &reportCodec{codec: codec}, nil
}

type reportCodec struct {
codec types.Codec
}

var _ median.ReportCodec = reportCodec{}

func (r reportCodec) BuildReport(observations []median.ParsedAttributedObservation) (ocrtypes.Report, error) {
agg := aggregate(observations)
return r.codec.Encode(context.Background(), agg, typeName)
}

func (r reportCodec) MedianFromReport(report ocrtypes.Report) (*big.Int, error) {
agg := &aggregatedAttributedObservation{}
if err := r.codec.Decode(context.Background(), report, agg, typeName); err != nil {
return nil, err
}
medianObservation := len(agg.Observations) / 2
return agg.Observations[medianObservation], nil
}

func (r reportCodec) MaxReportLength(n int) (int, error) {
return r.codec.GetMaxDecodingSize(context.Background(), n, typeName)
}
170 changes: 170 additions & 0 deletions median/report_codec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package median

import (
"context"
"errors"
"math/big"
"testing"

"github.com/smartcontractkit/libocr/commontypes"
"github.com/smartcontractkit/libocr/offchainreporting2/reportingplugin/median"
"github.com/smartcontractkit/libocr/offchainreporting2plus/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestReportCodec(t *testing.T) {
anyReports := []median.ParsedAttributedObservation{
{
Timestamp: 123,
Value: big.NewInt(300),
JuelsPerFeeCoin: big.NewInt(100),
Observer: 0,
},
{
Timestamp: 125,
Value: big.NewInt(200),
JuelsPerFeeCoin: big.NewInt(110),
Observer: 1,
},
{
Timestamp: 124,
Value: big.NewInt(250),
JuelsPerFeeCoin: big.NewInt(90),
Observer: 2,
},
}

aggReports := aggregatedAttributedObservation{
Timestamp: 124,
Observers: [32]commontypes.OracleID{1, 2, 0},
Observations: []*big.Int{
big.NewInt(200),
big.NewInt(250),
big.NewInt(300),
},
JuelsPerFeeCoin: big.NewInt(100),
}

anyEncodedReport := []byte{5, 6, 7, 8}

anyError := errors.New("nope not today")

t.Run("newReportCodec returns error if codec is nil", func(t *testing.T) {
_, err := newReportCodec(nil)
assert.Error(t, err)
})

t.Run("BuildReport builds the type and delegates to generic codec", func(t *testing.T) {
reportCodec, err := newReportCodec(&testCodec{
t: t,
expected: &aggReports,
result: anyEncodedReport,
})
require.NoError(t, err)

encoded, err := reportCodec.BuildReport(anyReports)
require.NoError(t, err)
assert.Equal(t, types.Report(anyEncodedReport), encoded)
})

t.Run("BuildReport returns error if codec returns error", func(t *testing.T) {
reportCodec, err := newReportCodec(&testCodec{
t: t,
expected: &aggReports,
result: anyEncodedReport,
err: anyError,
})
require.NoError(t, err)

_, err = reportCodec.BuildReport(anyReports)
assert.Equal(t, anyError, err)
})

t.Run("MedianFromReport delegates to codec and gets the median", func(t *testing.T) {
reportCodec, err := newReportCodec(&testCodec{
t: t,
expected: anyEncodedReport,
result: aggReports,
})
require.NoError(t, err)

medianVal, err := reportCodec.MedianFromReport(anyEncodedReport)
require.NoError(t, err)
assert.Equal(t, big.NewInt(250), medianVal)
})

t.Run("MedianFromReport returns error if codec returns error", func(t *testing.T) {
reportCodec, err := newReportCodec(&testCodec{
t: t,
expected: anyEncodedReport,
result: aggReports,
err: anyError,
})
require.NoError(t, err)

_, err = reportCodec.MedianFromReport(anyEncodedReport)
assert.Equal(t, anyError, err)
})

anyN := 10
anyLen := 200
t.Run("MaxReportLength delegates to codec", func(t *testing.T) {
reportCodec, err := newReportCodec(&testCodec{
t: t,
expected: anyN,
result: anyLen,
})
require.NoError(t, err)

length, err := reportCodec.MaxReportLength(anyN)
require.NoError(t, err)
assert.Equal(t, anyLen, length)
})

t.Run("MaxReportLength returns error if codec returns error", func(t *testing.T) {
reportCodec, err := newReportCodec(&testCodec{
t: t,
expected: 10,
result: anyLen,
err: anyError,
})
require.NoError(t, err)

_, err = reportCodec.MaxReportLength(10)
assert.Equal(t, anyError, err)
})
}

type testCodec struct {
t *testing.T
expected any
result any
err error
}

func (t *testCodec) Encode(_ context.Context, item any, itemType string) ([]byte, error) {
assert.Equal(t.t, t.expected, item)
assert.Equal(t.t, typeName, itemType)
return t.result.([]byte), t.err
}

func (t *testCodec) GetMaxEncodingSize(_ context.Context, n int, itemType string) (int, error) {
assert.Equal(t.t, t.expected, n)
assert.Equal(t.t, typeName, itemType)
return t.result.(int), t.err
}

func (t *testCodec) Decode(_ context.Context, raw []byte, into any, itemType string) error {
assert.Equal(t.t, t.expected, raw)
assert.Equal(t.t, typeName, itemType)
set := into.(*aggregatedAttributedObservation)
*set = t.result.(aggregatedAttributedObservation)
return t.err
}

func (t *testCodec) GetMaxDecodingSize(_ context.Context, n int, itemType string) (int, error) {
assert.Equal(t.t, t.expected, n)
assert.Equal(t.t, typeName, itemType)
return t.result.(int), t.err
}

0 comments on commit 9d0281b

Please sign in to comment.