Skip to content

Commit

Permalink
Chain Reader config improvements (#11679)
Browse files Browse the repository at this point in the history
* core/services/job: remove JSONConfig.Bytes hack

* Shorten chain reader cfg abi in median test, change readType string

* minimize abi; pretty formatting; use text marshaller

* integrations-tests/client: use TOML for relayconfig

* minimize chain reader median contract abi in features ocr2 test

* pretty ABI JSON

* add ModifiersConfig wrapper to include type field

* fix TestOCR2TaskJobSpec_String

---------

Co-authored-by: ilija <[email protected]>
  • Loading branch information
jmank88 and ilija42 authored Jan 16, 2024
1 parent 5403f5e commit d5c1100
Show file tree
Hide file tree
Showing 13 changed files with 773 additions and 80 deletions.
127 changes: 124 additions & 3 deletions core/internal/features/ocr2/features_ocr2_test.go

Large diffs are not rendered by default.

24 changes: 3 additions & 21 deletions core/services/job/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,26 +277,13 @@ func (s *OCROracleSpec) SetID(value string) error {
return nil
}

// JSONConfig is a Go mapping for JSON based database properties.
// JSONConfig is a map for config properties which are encoded as JSON in the database by implementing
// sql.Scanner and driver.Valuer.
type JSONConfig map[string]interface{}

// Bytes returns the raw bytes
func (r JSONConfig) Bytes() []byte {
var retCopy = make(JSONConfig, len(r))
for key, value := range r {
copiedVal := value
// If the value is a json structure string, unmarshal it to preserve JSON structure
// e.g. instead of this {"key":"{\"nestedKey\":{\"nestedValue\":123}}"}
// we want this {"key":{"nestedKey":{"nestedValue":123}}},
if strValue, ok := copiedVal.(string); ok {
if object, ok := asObject(strValue); ok {
copiedVal = object
}
}
retCopy[key] = copiedVal
}

b, _ := json.Marshal(retCopy)
b, _ := json.Marshal(r)
return b
}

Expand Down Expand Up @@ -326,11 +313,6 @@ func (r JSONConfig) MercuryCredentialName() (string, error) {
return name, nil
}

func asObject(s string) (any, bool) {
var js map[string]interface{}
return js, json.Unmarshal([]byte(s), &js) == nil
}

var ForwardersSupportedPlugins = []types.OCR2PluginType{types.Median, types.DKG, types.OCR2VRF, types.OCR2Keeper, types.Functions}

// OCR2OracleSpec defines the job spec for OCR2 jobs.
Expand Down
190 changes: 190 additions & 0 deletions core/services/job/models_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
package job

import (
_ "embed"
"reflect"
"testing"
"time"

"github.com/pelletier/go-toml/v2"
"github.com/stretchr/testify/require"
"gopkg.in/guregu/null.v4"

"github.com/smartcontractkit/chainlink-common/pkg/codec"
"github.com/smartcontractkit/chainlink-common/pkg/types"
"github.com/smartcontractkit/chainlink/v2/core/services/relay"
evmtypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types"
"github.com/smartcontractkit/chainlink/v2/core/store/models"
)

func TestOCR2OracleSpec_RelayIdentifier(t *testing.T) {
Expand Down Expand Up @@ -71,3 +81,183 @@ func TestOCR2OracleSpec_RelayIdentifier(t *testing.T) {
})
}
}

var (
//go:embed testdata/compact.toml
compact string
//go:embed testdata/pretty.toml
pretty string
)

func TestOCR2OracleSpec(t *testing.T) {
val := OCR2OracleSpec{
Relay: relay.EVM,
PluginType: types.Median,
ContractID: "foo",
OCRKeyBundleID: null.StringFrom("bar"),
TransmitterID: null.StringFrom("baz"),
ContractConfigConfirmations: 1,
ContractConfigTrackerPollInterval: *models.NewInterval(time.Second),
RelayConfig: map[string]interface{}{
"chainID": 1337,
"fromBlock": 42,
"chainReader": evmtypes.ChainReaderConfig{
Contracts: map[string]evmtypes.ChainContractReader{
"median": {
ContractABI: `[
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "requester",
"type": "address"
},
{
"indexed": false,
"internalType": "bytes32",
"name": "configDigest",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "uint32",
"name": "epoch",
"type": "uint32"
},
{
"indexed": false,
"internalType": "uint8",
"name": "round",
"type": "uint8"
}
],
"name": "RoundRequested",
"type": "event"
},
{
"inputs": [],
"name": "latestTransmissionDetails",
"outputs": [
{
"internalType": "bytes32",
"name": "configDigest",
"type": "bytes32"
},
{
"internalType": "uint32",
"name": "epoch",
"type": "uint32"
},
{
"internalType": "uint8",
"name": "round",
"type": "uint8"
},
{
"internalType": "int192",
"name": "latestAnswer_",
"type": "int192"
},
{
"internalType": "uint64",
"name": "latestTimestamp_",
"type": "uint64"
}
],
"stateMutability": "view",
"type": "function"
}
]
`,
Configs: map[string]*evmtypes.ChainReaderDefinition{
"LatestTransmissionDetails": {
ChainSpecificName: "latestTransmissionDetails",
OutputModifications: codec.ModifiersConfig{
&codec.EpochToTimeModifierConfig{
Fields: []string{"LatestTimestamp_"},
},
&codec.RenameModifierConfig{
Fields: map[string]string{
"LatestAnswer_": "LatestAnswer",
"LatestTimestamp_": "LatestTimestamp",
},
},
},
},
"LatestRoundRequested": {
ChainSpecificName: "RoundRequested",
ReadType: evmtypes.Event,
},
},
},
},
},
"codec": evmtypes.CodecConfig{
Configs: map[string]evmtypes.ChainCodecConfig{
"MedianReport": {
TypeABI: `[
{
"Name": "Timestamp",
"Type": "uint32"
},
{
"Name": "Observers",
"Type": "bytes32"
},
{
"Name": "Observations",
"Type": "int192[]"
},
{
"Name": "JuelsPerFeeCoin",
"Type": "int192"
}
]
`,
},
},
},
},
PluginConfig: map[string]interface{}{"juelsPerFeeCoinSource": ` // data source 1
ds1 [type=bridge name="%s"];
ds1_parse [type=jsonparse path="data"];
ds1_multiply [type=multiply times=2];
// data source 2
ds2 [type=http method=GET url="%s"];
ds2_parse [type=jsonparse path="data"];
ds2_multiply [type=multiply times=2];
ds1 -> ds1_parse -> ds1_multiply -> answer1;
ds2 -> ds2_parse -> ds2_multiply -> answer1;
answer1 [type=median index=0];
`,
},
}

t.Run("marshal", func(t *testing.T) {
gotB, err := toml.Marshal(val)
require.NoError(t, err)
t.Log("marshaled:", string(gotB))
require.Equal(t, compact, string(gotB))
})

t.Run("round-trip", func(t *testing.T) {
var gotVal OCR2OracleSpec
require.NoError(t, toml.Unmarshal([]byte(compact), &gotVal))
gotB, err := toml.Marshal(gotVal)
require.NoError(t, err)
require.Equal(t, compact, string(gotB))
t.Run("pretty", func(t *testing.T) {
var gotVal OCR2OracleSpec
require.NoError(t, toml.Unmarshal([]byte(pretty), &gotVal))
gotB, err := toml.Marshal(gotVal)
require.NoError(t, err)
t.Log("marshaled compact:", string(gotB))
require.Equal(t, compact, string(gotB))
})
})
}
34 changes: 34 additions & 0 deletions core/services/job/testdata/compact.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
contractID = 'foo'
relay = 'evm'
chainID = ''
p2pv2Bootstrappers = []
ocrKeyBundleID = 'bar'
monitoringEndpoint = ''
transmitterID = 'baz'
blockchainTimeout = '0s'
contractConfigTrackerPollInterval = '1s'
contractConfigConfirmations = 1
pluginType = 'median'
captureEATelemetry = false
captureAutomationCustomTelemetry = false

[relayConfig]
chainID = 1337
fromBlock = 42

[relayConfig.chainReader]
[relayConfig.chainReader.contracts]
[relayConfig.chainReader.contracts.median]
contractABI = "[\n {\n \"anonymous\": false,\n \"inputs\": [\n {\n \"indexed\": true,\n \"internalType\": \"address\",\n \"name\": \"requester\",\n \"type\": \"address\"\n },\n {\n \"indexed\": false,\n \"internalType\": \"bytes32\",\n \"name\": \"configDigest\",\n \"type\": \"bytes32\"\n },\n {\n \"indexed\": false,\n \"internalType\": \"uint32\",\n \"name\": \"epoch\",\n \"type\": \"uint32\"\n },\n {\n \"indexed\": false,\n \"internalType\": \"uint8\",\n \"name\": \"round\",\n \"type\": \"uint8\"\n }\n ],\n \"name\": \"RoundRequested\",\n \"type\": \"event\"\n },\n {\n \"inputs\": [],\n \"name\": \"latestTransmissionDetails\",\n \"outputs\": [\n {\n \"internalType\": \"bytes32\",\n \"name\": \"configDigest\",\n \"type\": \"bytes32\"\n },\n {\n \"internalType\": \"uint32\",\n \"name\": \"epoch\",\n \"type\": \"uint32\"\n },\n {\n \"internalType\": \"uint8\",\n \"name\": \"round\",\n \"type\": \"uint8\"\n },\n {\n \"internalType\": \"int192\",\n \"name\": \"latestAnswer_\",\n \"type\": \"int192\"\n },\n {\n \"internalType\": \"uint64\",\n \"name\": \"latestTimestamp_\",\n \"type\": \"uint64\"\n }\n ],\n \"stateMutability\": \"view\",\n \"type\": \"function\"\n }\n]\n"

[relayConfig.chainReader.contracts.median.configs]
LatestRoundRequested = "{\n \"chainSpecificName\": \"RoundRequested\",\n \"readType\": \"event\"\n}\n"
LatestTransmissionDetails = "{\n \"chainSpecificName\": \"latestTransmissionDetails\",\n \"output_modifications\": [\n {\n \"Fields\": [\n \"LatestTimestamp_\"\n ],\n \"Type\": \"epoch to time\"\n },\n {\n \"Fields\": {\n \"LatestAnswer_\": \"LatestAnswer\",\n \"LatestTimestamp_\": \"LatestTimestamp\"\n },\n \"Type\": \"rename\"\n }\n ]\n}\n"

[relayConfig.codec]
[relayConfig.codec.configs]
[relayConfig.codec.configs.MedianReport]
typeABI = "[\n {\n \"Name\": \"Timestamp\",\n \"Type\": \"uint32\"\n },\n {\n \"Name\": \"Observers\",\n \"Type\": \"bytes32\"\n },\n {\n \"Name\": \"Observations\",\n \"Type\": \"int192[]\"\n },\n {\n \"Name\": \"JuelsPerFeeCoin\",\n \"Type\": \"int192\"\n }\n]\n"

[pluginConfig]
juelsPerFeeCoinSource = " // data source 1\n ds1 [type=bridge name=\"%s\"];\n ds1_parse [type=jsonparse path=\"data\"];\n ds1_multiply [type=multiply times=2];\n\n // data source 2\n ds2 [type=http method=GET url=\"%s\"];\n ds2_parse [type=jsonparse path=\"data\"];\n ds2_multiply [type=multiply times=2];\n\n ds1 -> ds1_parse -> ds1_multiply -> answer1;\n ds2 -> ds2_parse -> ds2_multiply -> answer1;\n\n answer1 [type=median index=0];\n"
Loading

0 comments on commit d5c1100

Please sign in to comment.