Skip to content

Commit ee935a4

Browse files
authored
Contract config validation for OCR1 jobs (#17697)
* Add contract config validation for OCR1 jobs * Add changeset * Clean test * Add toggle for validation behavior * Update changeset
1 parent 002887e commit ee935a4

26 files changed

+220
-5
lines changed

.changeset/gold-toys-matter.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink": patch
3+
---
4+
5+
#nops Added validation to ensure the RPC node used can fetch the required config logs for OCR1 jobs. This behavior can be toggled on/off by using the new `OCR.ConfigLogValidation` setting

core/config/docs/core.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,8 @@ TransmitterAddress = '0xa0788FC17B1dEe36f057c42B6F373A34B014687e' # Example
404404
CaptureEATelemetry = false # Default
405405
# TraceLogging enables trace level logging.
406406
TraceLogging = false # Default
407+
# ConfigLogValidation ensures contract configuration logs are accessible when validating OCR jobs. Enable this when using RPC providers that don't maintain complete historical logs.
408+
ConfigLogValidation = false # Default
407409

408410
# P2P has a versioned networking stack. Currenly only `[P2P.V2]` is supported.
409411
# All nodes in the OCR network should share the same networking stack.

core/config/ocr_config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ type OCR interface {
2121
TraceLogging() bool
2222
DefaultTransactionQueueDepth() uint32
2323
CaptureEATelemetry() bool
24+
ConfigLogValidation() bool
2425
}

core/config/toml/types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,6 +1142,7 @@ type OCR struct {
11421142
TransmitterAddress *types.EIP55Address
11431143
CaptureEATelemetry *bool
11441144
TraceLogging *bool
1145+
ConfigLogValidation *bool
11451146
}
11461147

11471148
func (o *OCR) setFrom(f *OCR) {
@@ -1178,6 +1179,9 @@ func (o *OCR) setFrom(f *OCR) {
11781179
if v := f.TraceLogging; v != nil {
11791180
o.TraceLogging = v
11801181
}
1182+
if v := f.ConfigLogValidation; v != nil {
1183+
o.ConfigLogValidation = v
1184+
}
11811185
}
11821186

11831187
type P2P struct {

core/services/chainlink/config_ocr.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,7 @@ func (o *ocrConfig) DefaultTransactionQueueDepth() uint32 {
6767
func (o *ocrConfig) CaptureEATelemetry() bool {
6868
return *o.c.CaptureEATelemetry
6969
}
70+
71+
func (o *ocrConfig) ConfigLogValidation() bool {
72+
return *o.c.ConfigLogValidation
73+
}

core/services/chainlink/config_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,7 @@ func TestConfig_Marshal(t *testing.T) {
433433
TransmitterAddress: ptr(types.MustEIP55Address("0xa0788FC17B1dEe36f057c42B6F373A34B014687e")),
434434
CaptureEATelemetry: ptr(false),
435435
TraceLogging: ptr(false),
436+
ConfigLogValidation: ptr(false),
436437
}
437438
full.P2P = toml.P2P{
438439
IncomingMessageBufferSize: ptr[int64](13),
@@ -1003,6 +1004,7 @@ SimulateTransactions = true
10031004
TransmitterAddress = '0xa0788FC17B1dEe36f057c42B6F373A34B014687e'
10041005
CaptureEATelemetry = false
10051006
TraceLogging = false
1007+
ConfigLogValidation = false
10061008
`},
10071009
{"OCR2", Config{Core: toml.Core{OCR2: full.OCR2}}, `[OCR2]
10081010
Enabled = true

core/services/chainlink/testdata/config-empty-effective.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ SimulateTransactions = false
157157
TransmitterAddress = ''
158158
CaptureEATelemetry = false
159159
TraceLogging = false
160+
ConfigLogValidation = false
160161

161162
[P2P]
162163
IncomingMessageBufferSize = 10

core/services/chainlink/testdata/config-full.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ SimulateTransactions = true
163163
TransmitterAddress = '0xa0788FC17B1dEe36f057c42B6F373A34B014687e'
164164
CaptureEATelemetry = false
165165
TraceLogging = false
166+
ConfigLogValidation = false
166167

167168
[P2P]
168169
IncomingMessageBufferSize = 13

core/services/chainlink/testdata/config-multi-chain-effective.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ SimulateTransactions = false
157157
TransmitterAddress = ''
158158
CaptureEATelemetry = false
159159
TraceLogging = false
160+
ConfigLogValidation = false
160161

161162
[P2P]
162163
IncomingMessageBufferSize = 999

core/services/ocr/validate.go

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
package ocr
22

33
import (
4+
"context"
5+
"fmt"
46
"math/big"
57
"time"
68

9+
"github.com/ethereum/go-ethereum/common"
710
"github.com/lib/pq"
811
"github.com/pelletier/go-toml"
912
"github.com/pkg/errors"
10-
13+
"github.com/smartcontractkit/libocr/gethwrappers/offchainaggregator"
1114
"github.com/smartcontractkit/libocr/offchainreporting"
1215

16+
"github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/offchain_aggregator_wrapper"
17+
"github.com/smartcontractkit/chainlink-evm/pkg/client"
1318
evmconfig "github.com/smartcontractkit/chainlink-evm/pkg/config"
1419
"github.com/smartcontractkit/chainlink-evm/pkg/config/chaintype"
1520
"github.com/smartcontractkit/chainlink-evm/pkg/types"
1621
"github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm"
1722
coreconfig "github.com/smartcontractkit/chainlink/v2/core/config"
23+
"github.com/smartcontractkit/chainlink/v2/core/logger"
1824
"github.com/smartcontractkit/chainlink/v2/core/services/job"
1925
"github.com/smartcontractkit/chainlink/v2/core/services/ocrcommon"
2026
)
@@ -44,16 +50,22 @@ type insecureConfig interface {
4450

4551
// ValidatedOracleSpecToml validates an oracle spec that came from TOML
4652
func ValidatedOracleSpecToml(gcfg GeneralConfig, legacyChains legacyevm.LegacyChainContainer, tomlString string) (job.Job, error) {
47-
return ValidatedOracleSpecTomlCfg(gcfg, func(id *big.Int) (evmconfig.ChainScopedConfig, error) {
53+
return ValidatedOracleSpecTomlCfg(gcfg, func(id *big.Int, contractAddress types.EIP55Address) (evmconfig.ChainScopedConfig, error) {
4854
c, err := legacyChains.Get(id.String())
4955
if err != nil {
5056
return nil, err
5157
}
58+
if gcfg.OCR().ConfigLogValidation() {
59+
_, err = validateContractConfig(legacyChains, id, contractAddress)
60+
if err != nil {
61+
return nil, err
62+
}
63+
}
5264
return c.Config(), nil
5365
}, tomlString)
5466
}
5567

56-
func ValidatedOracleSpecTomlCfg(gcfg GeneralConfig, configFn func(id *big.Int) (evmconfig.ChainScopedConfig, error), tomlString string) (job.Job, error) {
68+
func ValidatedOracleSpecTomlCfg(gcfg GeneralConfig, configFn func(id *big.Int, contractAddress types.EIP55Address) (evmconfig.ChainScopedConfig, error), tomlString string) (job.Job, error) {
5769
var jb = job.Job{}
5870
var spec job.OCROracleSpec
5971
tree, err := toml.Load(tomlString)
@@ -91,7 +103,7 @@ func ValidatedOracleSpecTomlCfg(gcfg GeneralConfig, configFn func(id *big.Int) (
91103
}
92104
}
93105

94-
cfg, err := configFn(jb.OCROracleSpec.EVMChainID.ToInt())
106+
cfg, err := configFn(jb.OCROracleSpec.EVMChainID.ToInt(), spec.ContractAddress)
95107
if err != nil {
96108
return jb, err
97109
}
@@ -106,6 +118,7 @@ func ValidatedOracleSpecTomlCfg(gcfg GeneralConfig, configFn func(id *big.Int) (
106118
if err := validateTimingParameters(cfg.EVM(), cfg.EVM().OCR(), gcfg.Insecure(), spec, gcfg.OCR()); err != nil {
107119
return jb, err
108120
}
121+
109122
return jb, nil
110123
}
111124

@@ -167,3 +180,82 @@ func validateNonBootstrapSpec(tree *toml.Tree, spec job.Job, ocrObservationTimeo
167180
}
168181
return nil
169182
}
183+
184+
func validateContractConfig(legacyChains legacyevm.LegacyChainContainer, id *big.Int, contractAddress types.EIP55Address) (evmconfig.ChainScopedConfig, error) {
185+
chain, err := legacyChains.Get(id.String())
186+
if err != nil {
187+
return nil, err
188+
}
189+
190+
// Check if contract exists
191+
isDeployed, err := isContractDeployed(chain.Client(), contractAddress.Address())
192+
if err != nil {
193+
return nil, err
194+
}
195+
if !isDeployed {
196+
return chain.Config(), nil
197+
}
198+
199+
ct, err := newContractTracker(chain, contractAddress)
200+
if err != nil {
201+
return nil, err
202+
}
203+
204+
// Validate contract configuration
205+
if err := validateContractLogs(ct, contractAddress); err != nil {
206+
return nil, err
207+
}
208+
209+
return chain.Config(), nil
210+
}
211+
212+
func isContractDeployed(client client.Client, address common.Address) (bool, error) {
213+
code, err := client.CodeAt(context.Background(), address, nil)
214+
if err != nil {
215+
return false, fmt.Errorf("failed to get code at address: %w", err)
216+
}
217+
return len(code) > 0, nil
218+
}
219+
220+
func newContractTracker(chain legacyevm.Chain, contractAddress types.EIP55Address) (*OCRContractTracker, error) {
221+
contractCaller, err := offchainaggregator.NewOffchainAggregatorCaller(contractAddress.Address(), chain.Client())
222+
if err != nil {
223+
return nil, errors.Wrap(err, "could not instantiate NewOffchainAggregatorCaller")
224+
}
225+
226+
contract, err := offchain_aggregator_wrapper.NewOffchainAggregator(contractAddress.Address(), chain.Client())
227+
if err != nil {
228+
return nil, errors.Wrap(err, "failed to create OffchainAggregator wrapper")
229+
}
230+
231+
filterer, err := offchainaggregator.NewOffchainAggregatorFilterer(contractAddress.Address(), chain.Client())
232+
if err != nil {
233+
return nil, errors.Wrap(err, "failed to create OffchainAggregator filterer")
234+
}
235+
236+
return &OCRContractTracker{
237+
contractCaller: contractCaller,
238+
blockTranslator: ocrcommon.NewBlockTranslator(chain.Config().EVM(), chain.Client(), logger.NullLogger),
239+
ethClient: chain.Client(),
240+
contract: contract,
241+
contractFilterer: filterer,
242+
}, nil
243+
}
244+
245+
func validateContractLogs(ct *OCRContractTracker, contractAddress types.EIP55Address) error {
246+
ctx := context.Background()
247+
changedInBlock, _, err := ct.LatestConfigDetails(ctx)
248+
if err != nil {
249+
return fmt.Errorf("failed to get config at address %s: %w", contractAddress.String(), err)
250+
}
251+
252+
if changedInBlock == 0 {
253+
return nil // Contract is not configured, skip validation
254+
}
255+
256+
if _, configErr := ct.ConfigFromLogs(ctx, changedInBlock); configErr != nil {
257+
return errors.Wrap(configErr, "could not fetch OCR contract config, try switching to an archive node")
258+
}
259+
260+
return nil
261+
}

core/services/ocr/validate_test.go

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
11
package ocr_test
22

33
import (
4+
"encoding/hex"
45
"math/big"
6+
"strings"
57
"testing"
68
"time"
79

10+
"github.com/ethereum/go-ethereum/accounts/abi"
11+
"github.com/ethereum/go-ethereum/common"
812
"github.com/ethereum/go-ethereum/common/hexutil"
13+
types2 "github.com/ethereum/go-ethereum/core/types"
914
"github.com/manyminds/api2go/jsonapi"
15+
"github.com/smartcontractkit/libocr/gethwrappers/offchainaggregator"
16+
"github.com/smartcontractkit/libocr/gethwrappers/testoffchainaggregator"
1017
"github.com/stretchr/testify/assert"
18+
"github.com/stretchr/testify/mock"
1119
"github.com/stretchr/testify/require"
1220
"gopkg.in/guregu/null.v4"
1321

1422
commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config"
23+
"github.com/smartcontractkit/chainlink-evm/pkg/client/clienttest"
1524
evmconfig "github.com/smartcontractkit/chainlink-evm/pkg/config"
25+
"github.com/smartcontractkit/chainlink-evm/pkg/types"
26+
"github.com/smartcontractkit/chainlink/v2/core/internal/cltest"
27+
"github.com/smartcontractkit/chainlink/v2/core/internal/testutils"
1628
"github.com/smartcontractkit/chainlink/v2/core/internal/testutils/configtest"
1729
"github.com/smartcontractkit/chainlink/v2/core/internal/testutils/evmtest"
1830
"github.com/smartcontractkit/chainlink/v2/core/services/chainlink"
@@ -425,10 +437,79 @@ answer1 [type=median index=0];
425437
}
426438
})
427439

428-
s, err := ocr.ValidatedOracleSpecTomlCfg(c, func(id *big.Int) (evmconfig.ChainScopedConfig, error) {
440+
s, err := ocr.ValidatedOracleSpecTomlCfg(c, func(id *big.Int, contractAddress types.EIP55Address) (evmconfig.ChainScopedConfig, error) {
429441
return evmtest.NewChainScopedConfig(t, c), nil
430442
}, tc.toml)
431443
tc.assertion(t, s, err)
432444
})
433445
}
434446
}
447+
448+
func TestOnChainContractAvailability(t *testing.T) {
449+
// Because some RPCs prune logs we have scenarios in which a job spec update will lead to outages because of the inability to get the logs. We need to safeguard against these outages by checking if the node can access the OCR configuration
450+
// There are 4 possible scenarios:
451+
// 1. Contract is not deployed
452+
// 2. Contract is deployed but NOT configured
453+
// 3. Contract is deployed but config log event is NOT accessible
454+
// 4. Contract is deployed and config log event is accessible
455+
456+
// Mock chain
457+
client := clienttest.NewClient(t)
458+
contractBytes, err := hex.DecodeString(strings.TrimPrefix(testoffchainaggregator.OffchainAggregatorMetaData.Bin, "0x"))
459+
require.NoError(t, err, "could not decode contract binary")
460+
461+
cfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) {
462+
c.OCR.ConfigLogValidation = testutils.Ptr(true)
463+
})
464+
legacyChain := cltest.NewLegacyChainsWithMockChain(t, client, cfg)
465+
jobSpec := `
466+
type = "offchainreporting"
467+
schemaVersion = 1
468+
evmChainID = 0
469+
contractAddress = "0x613a38AC1659769640aaE063C651F48E0250454C"
470+
isBootstrapPeer = false
471+
observationSource = """
472+
ds1 [type=bridge name=voter_turnout];
473+
ds1_parse [type=jsonparse path="one,two"];
474+
ds1_multiply [type=multiply times=1.23];
475+
ds1 -> ds1_parse -> ds1_multiply -> answer1;
476+
answer1 [type=median index=0];
477+
"""
478+
`
479+
480+
// Contract is not deployed
481+
client.On("CodeAt", mock.Anything, mock.Anything, mock.Anything).Return([]byte{}, nil).Once()
482+
_, err = ocr.ValidatedOracleSpecToml(cfg, legacyChain, jobSpec)
483+
require.NoError(t, err)
484+
485+
abi, err := abi.JSON(strings.NewReader(offchainaggregator.OffchainAggregatorMetaData.ABI))
486+
require.NoError(t, err, "could not parse ABI")
487+
488+
noConfigDetails, err := abi.Methods["latestConfigDetails"].Outputs.Pack(uint32(0), uint32(0), [16]byte{})
489+
require.NoError(t, err, "could not pack outputs for latestConfigDetails")
490+
491+
// Contract is deployed but not configured
492+
client.On("CodeAt", mock.Anything, mock.Anything, mock.Anything).Return(contractBytes, nil).Once()
493+
client.On("CallContract", mock.Anything, mock.Anything, mock.Anything).Return(noConfigDetails, nil).Once()
494+
_, err = ocr.ValidatedOracleSpecToml(cfg, legacyChain, jobSpec)
495+
require.NoError(t, err)
496+
497+
// Contract is deployed but config log event is NOT accessible
498+
goodConfigDetails, err := abi.Methods["latestConfigDetails"].Outputs.Pack(uint32(1), uint32(1), [16]byte{})
499+
require.NoError(t, err, "could not pack outputs for latestConfigDetails")
500+
501+
client.On("CodeAt", mock.Anything, mock.Anything, mock.Anything).Return(contractBytes, nil).Once()
502+
client.On("CallContract", mock.Anything, mock.Anything, mock.Anything).Return(goodConfigDetails, nil).Once()
503+
client.On("FilterLogs", mock.Anything, mock.Anything, mock.Anything).Return([]types2.Log{}, nil).Once() // When the RPC has pruned the data the logs will come back empty
504+
_, err = ocr.ValidatedOracleSpecToml(cfg, legacyChain, jobSpec)
505+
require.ErrorContains(t, err, "could not fetch OCR contract config, try switching to an archive node")
506+
507+
// Contract is configured
508+
client.On("CodeAt", mock.Anything, mock.Anything, mock.Anything).Return(contractBytes, nil).Once()
509+
client.On("CallContract", mock.Anything, mock.Anything, mock.Anything).Return(goodConfigDetails, nil).Once()
510+
client.On("FilterLogs", mock.Anything, mock.Anything, mock.Anything).Return([]types2.Log{{
511+
Address: common.HexToAddress("0x613a38ac1659769640aae063c651f48e0250454c"),
512+
Topics: []common.Hash{common.HexToHash("0x25d719d88a4512dd76c7442b910a83360845505894eb444ef299409e180f8fb9")}}}, nil).Once()
513+
_, err = ocr.ValidatedOracleSpecToml(cfg, legacyChain, jobSpec)
514+
require.NoError(t, err)
515+
}

core/web/resolver/testdata/config-empty-effective.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ SimulateTransactions = false
157157
TransmitterAddress = ''
158158
CaptureEATelemetry = false
159159
TraceLogging = false
160+
ConfigLogValidation = false
160161

161162
[P2P]
162163
IncomingMessageBufferSize = 10

core/web/resolver/testdata/config-full.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ SimulateTransactions = true
163163
TransmitterAddress = '0xa0788FC17B1dEe36f057c42B6F373A34B014687e'
164164
CaptureEATelemetry = false
165165
TraceLogging = false
166+
ConfigLogValidation = false
166167

167168
[P2P]
168169
IncomingMessageBufferSize = 13

core/web/resolver/testdata/config-multi-chain-effective.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ SimulateTransactions = false
157157
TransmitterAddress = ''
158158
CaptureEATelemetry = false
159159
TraceLogging = false
160+
ConfigLogValidation = false
160161

161162
[P2P]
162163
IncomingMessageBufferSize = 999

docs/CONFIG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,6 +1049,7 @@ SimulateTransactions = false # Default
10491049
TransmitterAddress = '0xa0788FC17B1dEe36f057c42B6F373A34B014687e' # Example
10501050
CaptureEATelemetry = false # Default
10511051
TraceLogging = false # Default
1052+
ConfigLogValidation = false # Default
10521053
```
10531054
This section applies only if you are running off-chain reporting jobs.
10541055

@@ -1127,6 +1128,12 @@ TraceLogging = false # Default
11271128
```
11281129
TraceLogging enables trace level logging.
11291130

1131+
### ConfigLogValidation
1132+
```toml
1133+
ConfigLogValidation = false # Default
1134+
```
1135+
ConfigLogValidation ensures contract configuration logs are accessible when validating OCR jobs. Enable this when using RPC providers that don't maintain complete historical logs.
1136+
11301137
## P2P
11311138
```toml
11321139
[P2P]

0 commit comments

Comments
 (0)