From 6860e253a4114c03351eaa05eec71496d0da9404 Mon Sep 17 00:00:00 2001 From: Bolek Kulbabinski <1416262+bolekk@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:37:44 -0800 Subject: [PATCH] [CRE-1924] Support DONtime plugin config in CLD --- .../configure_capabilities_registry_test.go | 99 ++++++++++++++++ deployment/cre/ocr3/config.go | 8 ++ deployment/cre/ocr3/config_test.go | 57 +++++++++ deployment/cre/ocr3/oracle_config.go | 66 +++++++++++ deployment/cre/ocr3/oracle_config_test.go | 110 ++++++++++++++++++ 5 files changed, 340 insertions(+) diff --git a/deployment/cre/capabilities_registry/v2/changeset/configure_capabilities_registry_test.go b/deployment/cre/capabilities_registry/v2/changeset/configure_capabilities_registry_test.go index e267d0e8ebd..fd58a234ed9 100644 --- a/deployment/cre/capabilities_registry/v2/changeset/configure_capabilities_registry_test.go +++ b/deployment/cre/capabilities_registry/v2/changeset/configure_capabilities_registry_test.go @@ -444,6 +444,105 @@ dons: assert.Equal(t, 40*time.Second, oc.ConsensusCapOffchainConfig.RequestTimeout) } +func TestConfigureCapabilitiesRegistryInput_YAMLFromFile_DontimeConfig(t *testing.T) { + yamlConfig := ` +chainSelector: 421614 +capabilitiesRegistryAddress: "0x1234567890123456789012345678901234567890" +nops: + - admin: "0x1111111111111111111111111111111111111111" + name: "Node Operator Alpha" +capabilities: + - capabilityID: "dontime@1.0.0" + configurationContract: "0x0000000000000000000000000000000000000000" + metadata: + capabilityType: 2 + responseType: 0 +nodes: + - nop: "test-nop" + signer: ` + signer1 + ` + p2pID: ` + p2pID1 + ` + encryptionPublicKey: ` + encryptionPublicKey + ` + csaKey: ` + csaKey + ` + capabilityIDs: ["dontime@1.0.0"] +dons: + - name: "dontime-don" + donFamilies: ["dontime"] + config: + defaultConfig: {} + capabilityConfigurations: + - capabilityID: "dontime@1.0.0" + config: + ocr3Configs: + __default__: + deltaProgressMillis: 3000 + deltaResendMillis: 5000 + deltaInitialMillis: 500 + deltaRoundMillis: 500 + deltaGraceMillis: 200 + deltaCertifiedCommitRequestMillis: 1000 + deltaStageMillis: 30000 + maxRoundsPerEpoch: 10 + transmissionSchedule: [7] + maxFaultyOracles: 2 + uniqueReports: true + maxDurationQueryMillis: 500 + maxDurationObservationMillis: 500 + maxDurationShouldAcceptMillis: 500 + maxDurationShouldTransmitMillis: 500 + dontimeOffchainConfig: + maxQueryLengthBytes: 500000 + maxObservationLengthBytes: 500000 + maxOutcomeLengthBytes: 500000 + maxReportLengthBytes: 500000 + maxReportCount: 10 + maxBatchSize: 50 + minTimeIncrease: 100 + executionRemovalTime: "10m" + nodes: [` + nodeID1 + `] + f: 1 + isPublic: true + acceptsWorkflows: false +` + + var input changeset.ConfigureCapabilitiesRegistryInput + err := yaml.Unmarshal([]byte(yamlConfig), &input) + require.NoError(t, err, "should be able to parse YAML config with dontime offchain config") + + assert.Equal(t, uint64(421614), input.ChainSelector) + require.Len(t, input.DONs, 1) + assert.Equal(t, "dontime-don", input.DONs[0].Name) + + require.Len(t, input.DONs[0].CapabilityConfigurations, 1) + assert.Equal(t, "dontime@1.0.0", input.DONs[0].CapabilityConfigurations[0].CapabilityID) + + ocr3Configs, ok := input.DONs[0].CapabilityConfigurations[0].Config["ocr3Configs"].(map[string]any) + require.True(t, ok, "ocr3Configs should be a map") + defaultEntry, ok := ocr3Configs["__default__"] + require.True(t, ok, "__default__ key should exist in ocr3Configs") + + ocrJSON, err := json.Marshal(defaultEntry) + require.NoError(t, err) + var oc ocr3.OracleConfig + require.NoError(t, json.Unmarshal(ocrJSON, &oc)) + + assert.Equal(t, uint32(3000), oc.DeltaProgressMillis) + assert.Equal(t, 2, oc.MaxFaultyOracles) + assert.True(t, oc.UniqueReports) + assert.Nil(t, oc.ConsensusCapOffchainConfig, "consensusCapOffchainConfig should be nil") + assert.Nil(t, oc.ChainCapOffchainConfig, "chainCapOffchainConfig should be nil") + + require.NotNil(t, oc.DontimeOffchainConfig, "dontimeOffchainConfig should be parsed") + dt := oc.DontimeOffchainConfig + assert.Equal(t, uint32(500000), dt.MaxQueryLengthBytes) + assert.Equal(t, uint32(500000), dt.MaxObservationLengthBytes) + assert.Equal(t, uint32(500000), dt.MaxOutcomeLengthBytes) + assert.Equal(t, uint32(500000), dt.MaxReportLengthBytes) + assert.Equal(t, uint32(10), dt.MaxReportCount) + assert.Equal(t, uint32(50), dt.MaxBatchSize) + assert.Equal(t, int64(100), dt.MinTimeIncrease) + assert.Equal(t, 10*time.Minute, dt.ExecutionRemovalTime) +} + // setupCapabilitiesRegistryWithMCMS sets up a test environment with MCMS infrastructure func setupCapabilitiesRegistryWithMCMS(t *testing.T) *testFixture { selector := chainselectors.TEST_90000001.Selector diff --git a/deployment/cre/ocr3/config.go b/deployment/cre/ocr3/config.go index 7b8e5404928..a6ac522be11 100644 --- a/deployment/cre/ocr3/config.go +++ b/deployment/cre/ocr3/config.go @@ -231,6 +231,14 @@ func getOffchainCfg(oracleCfg OracleConfig) (offchainConfig, error) { result = oracleCfg.ChainCapOffchainConfig } + if oracleCfg.DontimeOffchainConfig != nil { + if result != nil { + return nil, fmt.Errorf("multiple offchain configs specified: %+v. Only one allowed", oracleCfg) + } + + result = oracleCfg.DontimeOffchainConfig + } + return result, nil } diff --git a/deployment/cre/ocr3/config_test.go b/deployment/cre/ocr3/config_test.go index 155862e650c..5017d6b4611 100644 --- a/deployment/cre/ocr3/config_test.go +++ b/deployment/cre/ocr3/config_test.go @@ -196,3 +196,60 @@ func loadTestData(t *testing.T, path string) []deployment.Node { require.Len(t, nodes, 10) return nodes } + +func Test_getOffchainCfg(t *testing.T) { + t.Run("nil when no offchain config set", func(t *testing.T) { + cfg := OracleConfig{} + got, err := getOffchainCfg(cfg) + require.NoError(t, err) + require.Nil(t, got) + }) + t.Run("returns ConsensusCapOffchainConfig", func(t *testing.T) { + cfg := OracleConfig{ + ConsensusCapOffchainConfig: &ConsensusCapOffchainConfig{MaxBatchSize: 1}, + } + got, err := getOffchainCfg(cfg) + require.NoError(t, err) + require.Equal(t, cfg.ConsensusCapOffchainConfig, got) + }) + t.Run("returns ChainCapOffchainConfig", func(t *testing.T) { + cfg := OracleConfig{ + ChainCapOffchainConfig: &ChainCapOffchainConfig{MaxBatchSize: 2}, + } + got, err := getOffchainCfg(cfg) + require.NoError(t, err) + require.Equal(t, cfg.ChainCapOffchainConfig, got) + }) + t.Run("returns DontimeOffchainConfig", func(t *testing.T) { + cfg := OracleConfig{ + DontimeOffchainConfig: &DontimeOffchainConfig{MaxBatchSize: 3}, + } + got, err := getOffchainCfg(cfg) + require.NoError(t, err) + require.Equal(t, cfg.DontimeOffchainConfig, got) + }) + t.Run("error when ConsensusCapOffchainConfig and DontimeOffchainConfig both set", func(t *testing.T) { + cfg := OracleConfig{ + ConsensusCapOffchainConfig: &ConsensusCapOffchainConfig{MaxBatchSize: 1}, + DontimeOffchainConfig: &DontimeOffchainConfig{MaxBatchSize: 3}, + } + _, err := getOffchainCfg(cfg) + require.ErrorContains(t, err, "multiple offchain configs specified") + }) + t.Run("error when ChainCapOffchainConfig and DontimeOffchainConfig both set", func(t *testing.T) { + cfg := OracleConfig{ + ChainCapOffchainConfig: &ChainCapOffchainConfig{MaxBatchSize: 2}, + DontimeOffchainConfig: &DontimeOffchainConfig{MaxBatchSize: 3}, + } + _, err := getOffchainCfg(cfg) + require.ErrorContains(t, err, "multiple offchain configs specified") + }) + t.Run("error when ConsensusCapOffchainConfig and ChainCapOffchainConfig both set", func(t *testing.T) { + cfg := OracleConfig{ + ConsensusCapOffchainConfig: &ConsensusCapOffchainConfig{MaxBatchSize: 1}, + ChainCapOffchainConfig: &ChainCapOffchainConfig{MaxBatchSize: 2}, + } + _, err := getOffchainCfg(cfg) + require.ErrorContains(t, err, "multiple offchain configs specified") + }) +} diff --git a/deployment/cre/ocr3/oracle_config.go b/deployment/cre/ocr3/oracle_config.go index 4a56d722ea4..165cf2214bd 100644 --- a/deployment/cre/ocr3/oracle_config.go +++ b/deployment/cre/ocr3/oracle_config.go @@ -11,6 +11,7 @@ import ( capocr3types "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/types" evmcapocr3types "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/consensus/ocr3/types" + dontimepb "github.com/smartcontractkit/chainlink-common/pkg/workflows/dontime/pb" ) type OracleConfig struct { @@ -34,6 +35,7 @@ type OracleConfig struct { ConsensusCapOffchainConfig *ConsensusCapOffchainConfig ChainCapOffchainConfig *ChainCapOffchainConfig + DontimeOffchainConfig *DontimeOffchainConfig } func (oc *OracleConfig) UnmarshalJSON(data []byte) error { @@ -170,3 +172,67 @@ func (oc *ChainCapOffchainConfig) ToProto() (proto.Message, error) { MaxBatchSize: oc.MaxBatchSize, }, nil } + +type DontimeOffchainConfig struct { + MaxQueryLengthBytes uint32 + MaxObservationLengthBytes uint32 + MaxOutcomeLengthBytes uint32 + MaxReportLengthBytes uint32 + MaxReportCount uint32 + MaxBatchSize uint32 + MinTimeIncrease int64 + ExecutionRemovalTime time.Duration +} + +func (oc *DontimeOffchainConfig) UnmarshalJSON(data []byte) error { + type aliasT DontimeOffchainConfig + temp := &struct { + ExecutionRemovalTime string `json:"ExecutionRemovalTime"` + *aliasT + }{ + aliasT: (*aliasT)(oc), + } + if err := json.Unmarshal(data, temp); err != nil { + return fmt.Errorf("failed to unmarshal DontimeOffchainConfig: %w", err) + } + + if temp.ExecutionRemovalTime == "" { + oc.ExecutionRemovalTime = 0 + } else { + d, err := time.ParseDuration(temp.ExecutionRemovalTime) + if err != nil { + return fmt.Errorf("failed to parse ExecutionRemovalTime: %w", err) + } + oc.ExecutionRemovalTime = d + } + + return nil +} + +func (oc *DontimeOffchainConfig) MarshalJSON() ([]byte, error) { + type aliasT DontimeOffchainConfig + return json.Marshal(&struct { + ExecutionRemovalTime string `json:"ExecutionRemovalTime"` + *aliasT + }{ + ExecutionRemovalTime: oc.ExecutionRemovalTime.String(), + aliasT: (*aliasT)(oc), + }) +} + +func (oc *DontimeOffchainConfig) ToProto() (proto.Message, error) { + var execRemovalTime *durationpb.Duration + if oc.ExecutionRemovalTime > 0 { + execRemovalTime = durationpb.New(oc.ExecutionRemovalTime) + } + return &dontimepb.Config{ + MaxQueryLengthBytes: oc.MaxQueryLengthBytes, + MaxObservationLengthBytes: oc.MaxObservationLengthBytes, + MaxOutcomeLengthBytes: oc.MaxOutcomeLengthBytes, + MaxReportLengthBytes: oc.MaxReportLengthBytes, + MaxReportCount: oc.MaxReportCount, + MaxBatchSize: oc.MaxBatchSize, + MinTimeIncrease: oc.MinTimeIncrease, + ExecutionRemovalTime: execRemovalTime, + }, nil +} diff --git a/deployment/cre/ocr3/oracle_config_test.go b/deployment/cre/ocr3/oracle_config_test.go index 032c1a4d0fc..37c544c9ef7 100644 --- a/deployment/cre/ocr3/oracle_config_test.go +++ b/deployment/cre/ocr3/oracle_config_test.go @@ -5,8 +5,12 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/durationpb" "gopkg.in/yaml.v3" + + dontimepb "github.com/smartcontractkit/chainlink-common/pkg/workflows/dontime/pb" ) func TestOracleConfig_JSON(t *testing.T) { @@ -48,6 +52,45 @@ func TestOracleConfig_JSON(t *testing.T) { asJSON, err := json.Marshal(cfg) require.NoError(t, err) + var fromJSON OracleConfig + err = json.Unmarshal(asJSON, &fromJSON) + require.NoError(t, err) + require.Equal(t, cfg, fromJSON) + }) + t.Run("Dontime OCR Config", func(t *testing.T) { + var cfg OracleConfig + err := json.Unmarshal([]byte(dontimeOcr3Cfg), &cfg) + require.NoError(t, err) + require.Equal(t, 2, cfg.MaxFaultyOracles) + dt := cfg.DontimeOffchainConfig + require.NotNil(t, dt) + require.Equal(t, uint32(500000), dt.MaxQueryLengthBytes) + require.Equal(t, uint32(500000), dt.MaxObservationLengthBytes) + require.Equal(t, uint32(500000), dt.MaxOutcomeLengthBytes) + require.Equal(t, uint32(500000), dt.MaxReportLengthBytes) + require.Equal(t, uint32(10), dt.MaxReportCount) + require.Equal(t, uint32(50), dt.MaxBatchSize) + require.Equal(t, int64(100), dt.MinTimeIncrease) + require.Equal(t, 10*time.Minute, dt.ExecutionRemovalTime) + + asJSON, err := json.Marshal(cfg) + require.NoError(t, err) + var cfg2 OracleConfig + err = json.Unmarshal(asJSON, &cfg2) + require.NoError(t, err) + require.Equal(t, cfg, cfg2) + }) + t.Run("Dontime OCR Config with zero ExecutionRemovalTime", func(t *testing.T) { + cfg := OracleConfig{ + DeltaProgressMillis: 3000, + DontimeOffchainConfig: &DontimeOffchainConfig{ + MaxBatchSize: 25, + MinTimeIncrease: 50, + }, + } + asJSON, err := json.Marshal(cfg) + require.NoError(t, err) + var fromJSON OracleConfig err = json.Unmarshal(asJSON, &fromJSON) require.NoError(t, err) @@ -107,3 +150,70 @@ var legacyOcr3Cfg = ` "MaxDurationShouldTransmitMillis": 1000, "MaxFaultyOracles": 3 }` + +var dontimeOcr3Cfg = ` +{ + "DontimeOffchainConfig": { + "MaxQueryLengthBytes": 500000, + "MaxObservationLengthBytes": 500000, + "MaxOutcomeLengthBytes": 500000, + "MaxReportLengthBytes": 500000, + "MaxReportCount": 10, + "MaxBatchSize": 50, + "MinTimeIncrease": 100, + "ExecutionRemovalTime": "10m" + }, + "UniqueReports": true, + "DeltaProgressMillis": 5000, + "DeltaResendMillis": 5000, + "DeltaInitialMillis": 5000, + "DeltaRoundMillis": 2000, + "DeltaGraceMillis": 500, + "DeltaCertifiedCommitRequestMillis": 1000, + "DeltaStageMillis": 30000, + "MaxRoundsPerEpoch": 10, + "TransmissionSchedule": [7], + "MaxDurationQueryMillis": 1000, + "MaxDurationObservationMillis": 1000, + "MaxDurationShouldAcceptMillis": 1000, + "MaxDurationShouldTransmitMillis": 1000, + "MaxFaultyOracles": 2 +}` + +func TestDontimeOffchainConfig_ToProto(t *testing.T) { + t.Run("all fields set", func(t *testing.T) { + cfg := &DontimeOffchainConfig{ + MaxQueryLengthBytes: 100, + MaxObservationLengthBytes: 200, + MaxOutcomeLengthBytes: 300, + MaxReportLengthBytes: 400, + MaxReportCount: 5, + MaxBatchSize: 10, + MinTimeIncrease: 42, + ExecutionRemovalTime: 5 * time.Minute, + } + msg, err := cfg.ToProto() + require.NoError(t, err) + pb, ok := msg.(*dontimepb.Config) + require.True(t, ok) + assert.Equal(t, uint32(100), pb.MaxQueryLengthBytes) + assert.Equal(t, uint32(200), pb.MaxObservationLengthBytes) + assert.Equal(t, uint32(300), pb.MaxOutcomeLengthBytes) + assert.Equal(t, uint32(400), pb.MaxReportLengthBytes) + assert.Equal(t, uint32(5), pb.MaxReportCount) + assert.Equal(t, uint32(10), pb.MaxBatchSize) + assert.Equal(t, int64(42), pb.MinTimeIncrease) + assert.Equal(t, durationpb.New(5*time.Minute), pb.ExecutionRemovalTime) + }) + t.Run("zero ExecutionRemovalTime yields nil", func(t *testing.T) { + cfg := &DontimeOffchainConfig{ + MaxBatchSize: 10, + } + msg, err := cfg.ToProto() + require.NoError(t, err) + pb, ok := msg.(*dontimepb.Config) + require.True(t, ok) + assert.Nil(t, pb.ExecutionRemovalTime) + assert.Equal(t, uint32(10), pb.MaxBatchSize) + }) +}