diff --git a/core/config/env/env.go b/core/config/env/env.go index 56bc2af45d4..e94968c3b23 100644 --- a/core/config/env/env.go +++ b/core/config/env/env.go @@ -24,15 +24,16 @@ var ( // LOOPP commands and vars var ( - MedianPlugin = NewPlugin("median") - MercuryPlugin = NewPlugin("mercury") - AptosPlugin = NewPlugin("aptos") - EVMPlugin = NewPlugin("evm") - CosmosPlugin = NewPlugin("cosmos") - SolanaPlugin = NewPlugin("solana") - StarknetPlugin = NewPlugin("starknet") - TronPlugin = NewPlugin("tron") - TONPlugin = NewPlugin("ton") + MedianPlugin = NewPlugin("median") + MercuryPlugin = NewPlugin("mercury") + AptosPlugin = NewPlugin("aptos") + EVMPlugin = NewPlugin("evm") + CosmosPlugin = NewPlugin("cosmos") + SolanaPlugin = NewPlugin("solana") + StarknetPlugin = NewPlugin("starknet") + TronPlugin = NewPlugin("tron") + TONPlugin = NewPlugin("ton") + SecureMintPlugin = NewPlugin("securemint") // PrometheusDiscoveryHostName is the externally accessible hostname // published by the node in the `/discovery` endpoint. Generally, it is expected to match // the public hostname of node. diff --git a/core/scripts/go.mod b/core/scripts/go.mod index c32d5d868e7..41cc409f76e 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -47,12 +47,12 @@ require ( github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/chainlink-automation v0.8.1 github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20250825135846-84f0d5167f8f - github.com/smartcontractkit/chainlink-common v0.9.5-0.20250908082700-aa3f5927af8c + github.com/smartcontractkit/chainlink-common v0.9.5-0.20250909120425-33154edc7a38 github.com/smartcontractkit/chainlink-data-streams v0.1.2 github.com/smartcontractkit/chainlink-deployments-framework v0.42.0 github.com/smartcontractkit/chainlink-evm v0.3.3-0.20250903140346-aacd485a7dea github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20250827130336-5922343458be - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250829155125-f4655b0b4605 + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250905211734-167560f092c1 github.com/smartcontractkit/chainlink-protos/job-distributor v0.13.1 github.com/smartcontractkit/chainlink-testing-framework/framework v0.10.17 github.com/smartcontractkit/chainlink-testing-framework/framework/components/dockercompose v0.1.13 diff --git a/core/scripts/go.sum b/core/scripts/go.sum index 21e17723715..a76ecb15fd8 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -1538,8 +1538,8 @@ github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250805210128-7 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250805210128-7f8a0f403c3a/go.mod h1:Ve1xD71bl193YIZQEoJMmBqLGQJdNs29bwbuObwvbhQ= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250805210128-7f8a0f403c3a h1:38dAlTPRUQHZus5dCnBnQyf/V4oYn0p2svWlbPgHDQ4= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250805210128-7f8a0f403c3a/go.mod h1:xtZNi6pOKdC3sLvokDvXOhgHzT+cyBqH/gWwvxTxqrg= -github.com/smartcontractkit/chainlink-common v0.9.5-0.20250908082700-aa3f5927af8c h1:2cnAGt0nedGS/M0deXRCIJVsxTWi6CzYPXkTxqVwViY= -github.com/smartcontractkit/chainlink-common v0.9.5-0.20250908082700-aa3f5927af8c/go.mod h1:b5KI42+P0ZmUXuvOFzSH9uIB8K83wvXq1GNVoY+ePeg= +github.com/smartcontractkit/chainlink-common v0.9.5-0.20250909120425-33154edc7a38 h1:dCvWsUaZsEkX6iH6CwsmtDORH6rHoAHQ9Vi0H/Zb444= +github.com/smartcontractkit/chainlink-common v0.9.5-0.20250909120425-33154edc7a38/go.mod h1:1diMLMwfIACeqJFt7ySGaBrJIeUwHTLhVVYlb41EyKk= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.1 h1:ca2z5OXgnbBPQRxpwXwBLJsUA1+cAp5ncfW4Ssvd6eY= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.1/go.mod h1:NZv/qKYGFRnkjOYBouajnDfFoZ+WDa6H2KNmSf1dnKc= github.com/smartcontractkit/chainlink-common/pkg/monitoring v0.0.0-20250415235644-8703639403c7 h1:9wh1G+WbXwPVqf0cfSRSgwIcaXTQgvYezylEAfwmrbw= @@ -1564,8 +1564,8 @@ github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250729142306- github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250729142306-508e798f6a5d/go.mod h1:2JTBNp3FlRdO/nHc4dsc9bfxxMClMO1Qt8sLJgtreBY= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20250722225531-876fd6b94976 h1:mF3FiDUoV0QbJcks9R2y7ydqntNL1Z0VCPBJgx/Ms+0= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20250722225531-876fd6b94976/go.mod h1:HHGeDUpAsPa0pmOx7wrByCitjQ0mbUxf0R9v+g67uCA= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250829155125-f4655b0b4605 h1:yVH5tLDzW2ZBUpmkHF5nci1SRSXTcU3A1VZ8iS5qudA= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250829155125-f4655b0b4605/go.mod h1:jUC52kZzEnWF9tddHh85zolKybmLpbQ1oNA4FjOHt1Q= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250905211734-167560f092c1 h1:HZt/80mhcNw6/MlYBIRracxfHWNqFF0iZ5nZEVZBUgo= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250905211734-167560f092c1/go.mod h1:jUC52kZzEnWF9tddHh85zolKybmLpbQ1oNA4FjOHt1Q= github.com/smartcontractkit/chainlink-protos/job-distributor v0.13.1 h1:PWwLGimBt37eDzpbfZ9V/ZkW4oCjcwKjKiAwKlSfPc0= github.com/smartcontractkit/chainlink-protos/job-distributor v0.13.1/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE= github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 h1:0eroOyBwmdoGUwUdvMI0/J7m5wuzNnJDMglSOK1sfNY= diff --git a/core/services/job/orm.go b/core/services/job/orm.go index 6b50e6dfb0f..f2888a7e8ba 100644 --- a/core/services/job/orm.go +++ b/core/services/job/orm.go @@ -167,7 +167,7 @@ func (o *orm) AssertBridgesExist(ctx context.Context, p pipeline.Pipeline) error return nil } -// CreateJob creates the job, and it's associated spec record. +// CreateJob creates the job, and its associated spec record. // Expects an unmarshalled job spec as the jb argument i.e. output from ValidatedXX. // Scans all persisted records back into jb func (o *orm) CreateJob(ctx context.Context, jb *Job) error { @@ -647,7 +647,7 @@ func (o *orm) insertGatewaySpec(ctx context.Context, spec *GatewaySpec) (specID // ValidateKeyStoreMatch confirms that the key has a valid match in the keystore func ValidateKeyStoreMatch(ctx context.Context, spec *OCR2OracleSpec, keyStore keystore.Master, key string) (err error) { switch spec.PluginType { - case types.Mercury, types.LLO: + case types.Mercury, types.LLO, types.SecureMint: _, err = keyStore.CSA().Get(key) if err != nil { err = errors.Errorf("no CSA key matching: %q", key) diff --git a/core/services/ocr2/delegate.go b/core/services/ocr2/delegate.go index d968fab94a0..2735a956239 100644 --- a/core/services/ocr2/delegate.go +++ b/core/services/ocr2/delegate.go @@ -45,6 +45,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/types" cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" "github.com/smartcontractkit/chainlink-common/pkg/types/core" + sm "github.com/smartcontractkit/chainlink-common/pkg/types/core/securemint" llotypes "github.com/smartcontractkit/chainlink-common/pkg/types/llo" "github.com/smartcontractkit/chainlink-common/pkg/utils/mailbox" "github.com/smartcontractkit/chainlink-common/pkg/workflows/dontime" @@ -79,6 +80,8 @@ import ( ocr2keeper21core "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/core" vaultocrplugin "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/vault" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/validate" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr3/securemint" + sm_adapter "github.com/smartcontractkit/chainlink/v2/core/services/ocr3/securemint/keyringadapter" "github.com/smartcontractkit/chainlink/v2/core/services/ocrcommon" "github.com/smartcontractkit/chainlink/v2/core/services/pipeline" "github.com/smartcontractkit/chainlink/v2/core/services/relay" @@ -579,6 +582,8 @@ func (d *Delegate) ServicesForSpec(ctx context.Context, jb job.Job) ([]job.Servi case types.DonTimePlugin: return d.newDonTimePlugin(ctx, lggr, jb, bootstrapPeers, kb, ocrDB, lc) + case types.SecureMint: + return d.newServicesSecureMint(ctx, lggr, jb, bootstrapPeers, kb, ocrDB, lc) default: return nil, errors.Errorf("plugin type %s not supported", spec.PluginType) @@ -1498,6 +1503,54 @@ func (d *Delegate) newServicesMedian( return medianServices, err2 } +func (d *Delegate) newServicesSecureMint( + ctx context.Context, + lggr logger.SugaredLogger, + jb job.Job, + bootstrapPeers []commontypes.BootstrapperLocator, + kb ocr2key.KeyBundle, + ocrDB *db, + lc ocrtypes.LocalConfig, +) ([]job.ServiceCtx, error) { + spec := jb.OCR2OracleSpec + rid, err := spec.RelayID() + if err != nil { + return nil, ErrJobSpecNoRelayer{Err: err, PluginName: "securemint"} + } + + ocrLogger := ocrcommon.NewOCRWrapper(lggr, d.cfg.OCR2().TraceLogging(), func(ctx context.Context, msg string) { + lggr.ErrorIf(d.jobORM.RecordError(ctx, jb.ID, msg), "unable to record error") + }) + + oracleArgsNoPlugin := libocr2.OCR3OracleArgs[sm.ChainSelector]{ + BinaryNetworkEndpointFactory: d.peerWrapper.Peer2, + V2Bootstrappers: bootstrapPeers, + Database: ocrDB, + LocalConfig: lc, + Logger: ocrLogger, + MonitoringEndpoint: d.monitoringEndpointGen.GenMonitoringEndpoint(rid.Network, rid.ChainID, spec.ContractID, synchronization.OCR2Median), + OffchainKeyring: kb, + OnchainKeyring: sm_adapter.NewSecureMintOCR3OnchainKeyringAdapter(kb), + MetricsRegisterer: prometheus.WrapRegistererWith(map[string]string{"job_name": jb.Name.ValueOrZero()}, prometheus.DefaultRegisterer), + } + + smConfig := securemint.NewJobConfig( + d.cfg.JobPipeline().MaxSuccessfulRuns(), + d.cfg.JobPipeline().ResultWriteQueueDepth(), + d.cfg, + ) + + relayer, err := d.RelayGetter.Get(rid) + if err != nil { + return nil, ErrRelayNotEnabled{Err: err, PluginName: "securemint", Relay: spec.Relay} + } + + secureMintServices, err := securemint.NewSecureMintServices(ctx, jb, d.isNewlyCreatedJob, relayer, d.pipelineRunner, lggr, oracleArgsNoPlugin, smConfig, d.capabilitiesRegistry) + secureMintServices = append(secureMintServices, ocrLogger) + + return secureMintServices, err +} + func (d *Delegate) newServicesOCR2Keepers( ctx context.Context, lggr logger.SugaredLogger, diff --git a/core/services/ocr2/validate/validate.go b/core/services/ocr2/validate/validate.go index 249d8cf3954..c9ea13d8eba 100644 --- a/core/services/ocr2/validate/validate.go +++ b/core/services/ocr2/validate/validate.go @@ -12,8 +12,6 @@ import ( "github.com/pelletier/go-toml" pkgerrors "github.com/pkg/errors" - libocr2 "github.com/smartcontractkit/libocr/offchainreporting2plus" - "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/loop/reportingplugins" "github.com/smartcontractkit/chainlink-common/pkg/types" @@ -25,11 +23,13 @@ import ( lloconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/llo/config" mercuryconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/mercury/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/vault" + sm_config "github.com/smartcontractkit/chainlink/v2/core/services/ocr3/securemint/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocrcommon" "github.com/smartcontractkit/chainlink/v2/core/services/pipeline" "github.com/smartcontractkit/chainlink/v2/core/services/relay" evmtypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" "github.com/smartcontractkit/chainlink/v2/plugins" + libocr2 "github.com/smartcontractkit/libocr/offchainreporting2plus" ) // ValidatedOracleSpecToml validates an oracle spec that came from TOML @@ -132,6 +132,8 @@ func validateSpec(ctx context.Context, tree *toml.Tree, spec job.Job, rc plugins return validateVaultPluginSpec(spec.OCR2OracleSpec.PluginConfig) case types.DonTimePlugin: return validateDonTimePluginSpec(spec.OCR2OracleSpec.PluginConfig) + case types.SecureMint: + return validateSecureMintSpec(spec.OCR2OracleSpec.PluginConfig) case "": return errors.New("no plugin specified") default: @@ -405,3 +407,20 @@ func validateOCR2LLOSpec(jsonConfig job.JSONConfig) error { } return pkgerrors.Wrap(pluginConfig.Validate(), "LLO PluginConfig is invalid") } + +func validateSecureMintSpec(jsonConfig job.JSONConfig) error { + if jsonConfig == nil { + return errors.New("secure mint plugin config is empty") + } + + smConfig, err := sm_config.Parse(jsonConfig.Bytes()) + if err != nil { + return pkgerrors.Wrap(err, "error while parsing secure mint plugin config") + } + + if err := smConfig.Validate(); err != nil { + return fmt.Errorf("invalid secure mint plugin config: %#v, err: %w", smConfig, err) + } + + return nil +} diff --git a/core/services/ocr3/securemint/README.md b/core/services/ocr3/securemint/README.md new file mode 100644 index 00000000000..f97a0440218 --- /dev/null +++ b/core/services/ocr3/securemint/README.md @@ -0,0 +1,117 @@ +# SecureMint Plugin + +## Overview + +The SecureMint plugin is a plugin that allows for secure minting of tokens. +It's looppified, its implementation can be found in https://github.com/smartcontractkit/chainlink-secure-mint/. +Make sure to install the plugin before running the integration test. + +### Secure mint plugin version + +The current code works with [v0.1 of the secure mint plugin](https://github.com/smartcontractkit/chainlink-secure-mint/commit/548f7e4753a11b2bcd69f53345ca6a0d696dff9d). + +## Validation + +Validating whether the SecureMint plugin is working as expected is done by running the integration test. + +The test is located in the `core/services/ocr3/securemint` directory. + +### Prerequisites: +```bash +docker run --name cl-postgres -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=dbname -p 5432:5432 -d postgres +make setup-testdb +``` + +### Run test: +```bash + time CL_SECUREMINT_CMD=chainlink-secure-mint CL_DATABASE_URL=postgresql://chainlink_dev:insecurepassword@localhost:5432/chainlink_development_test?sslmode=disable go test -timeout 2m -run ^TestIntegration_SecureMint_happy_path$ github.com/smartcontractkit/chainlink/v2/core/services/ocr3/securemint -v 2>&1 | tee all.log | awk '/DEBUG|INFO|WARN|ERROR/ { print > "node_logs.log"; next }; { print > "other.log" }; tail all.log' +``` + +### If you change any dependencies: +```bash +go mod tidy && go mod vendor && modvendor -copy="**/*.a **/*.h" -v +``` + +(the `modvendor` step might not be necessary, but for me it was (see also https://github.com/marcboeker/go-duckdb/issues/174#issuecomment-1979097864)) + +### Logs + +* other.log: Contains all non-node output from the test run, this can be used to quickly see test failures +* node_logs.log: Contains all logs from the nodes started up in the test run, this can be used to see the full output of the test run +* all.log: Contains the complete output of the test run, this can be used to see test failures within the context of the node logs + + +### Debug test with VSCode: + +Create a launch.json file in the .vscode directory with the following content: + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Secure Mint Integration Test", + "type": "go", + "request": "launch", + "mode": "test", + "program": "${workspaceFolder}/core/services/ocr3/securemint/integrationtest", + "args": [ + "-test.run", + "^TestIntegration_SecureMint_happy_path$", + "-test.v", + "-test.timeout", + "2m", + "2>&1", + "|", + "tee", + "all.log", + "|", + "awk '/DEBUG|INFO|WARN|ERROR/ { print > 'node_logs.log'; next }; { print > 'other.log' }'", + ], + "env": { + "ENV": "test", + "CL_DATABASE_URL": "postgresql://chainlink_dev:insecurepassword@localhost:5432/chainlink_development_test?sslmode=disable", + "CL_SECUREMINT_CMD": "chainlink-secure-mint", + } + } + ] +} +``` + +Then run the test by Cmd+P: "Start Debugging". + +## Hacks + + +### XXX_SingletonTransmitter + +This is a hack to allow the `TestIntegration_SecureMint_happy_path` integration test to assess whether secure mint reports are being transmitted as a trigger to a Workflow. + +It gives the integration test access to the SecureMint transmitter, which is used to assert on the number of transmissions. + + +## Secure Mint Workflow + +The Secure Mint plugin's reports are triggers for a CRE Workflow. + +The secure mint workflow, and specifically the securemint aggregator (see `chainlink-common/pkg/capabilities/consensus/ocr3/datafeeds/securemint_aggregator.go`) are tested in `core/capabilities/integration_tests/keystone/securemint_workflow_test.go`. + +You can run the `Test_runSecureMintWorkflow` test as follows: +```bash +time CL_DATABASE_URL=postgresql://chainlink_dev:insecurepassword@localhost:5432/chainlink_development_test?sslmode=disable go test -timeout 2m -run ^Test_runSecureMintWorkflow$ github.com/smartcontractkit/chainlink/v2/core/capabilities/integration_tests/keystone -v 2>&1 | tee all.log | awk '/DEBUG|INFO|WARN|ERROR/ { print > "node_logs.log"; next }; { print > "other.log" }'; tail all.log +``` + +### Layers of abstraction in sending a Workflow trigger + +When sending a Workflow trigger, the SecureMint report is wrapped in several layers of abstraction. + +From top to bottom: + +The secure mint transmitter sends a: +- `capabilities.TriggerResponse{Event: capabilities.TriggerEvent, Err}`, which contains a: +- `capabilities.TriggerEvent{TriggerType: 0, ID: "securemint-trigger", Outputs: values.Map, Payload: nil}`, which contains: +- `values.Map{"sigs": signatures, "configDigest": cfgDigest, "seqNr": seqNr, "report": }`, which contains a +- `ocr3types.ReportWithInfo{Report: json-marshaled PorReport, Info: chainSelector}`, which contains a +- `securemint.Report{ConfigDigest, SeqNr, Block, Mintable}`, which: + +is created by the secure mint plugin. diff --git a/core/services/ocr3/securemint/config/config.go b/core/services/ocr3/securemint/config/config.go new file mode 100644 index 00000000000..faa7c80467a --- /dev/null +++ b/core/services/ocr3/securemint/config/config.go @@ -0,0 +1,57 @@ +package config + +import ( + "encoding/json" + + "github.com/pkg/errors" +) + +// SecureMintConfig holds secure mint specific configuration +type SecureMintConfig struct { + Token string `json:"token"` + Reserves string `json:"reserves"` + ChainSelectors []string `json:"chainSelectors"` + + // Trigger capability configuration + TriggerCapabilityName string `json:"triggerCapabilityName"` + TriggerCapabilityVersion string `json:"triggerCapabilityVersion"` + TriggerTickerMinResolutionMs int `json:"triggerTickerMinResolutionMs"` + TriggerSendChannelBufferSize int `json:"triggerSendChannelBufferSize"` +} + +// SecureMintTriggerConfig holds configuration for secure mint trigger subscribers +type SecureMintTriggerConfig struct { + // The interval in milliseconds after which a new trigger event is generated. + MaxFrequencyMs uint64 `json:"maxFrequencyMs" yaml:"maxFrequencyMs" mapstructure:"maxFrequencyMs"` +} + +// Parse parses the secure mint configuration from JSON bytes +func Parse(configBytes []byte) (*SecureMintConfig, error) { + if len(configBytes) == 0 { + return nil, errors.New("secure mint config cannot be empty") + } + + var config SecureMintConfig + if err := json.Unmarshal(configBytes, &config); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal SecureMintConfig") + } + + return &config, nil +} + +// Validate validates the secure mint plugin-specific config. +func (cfg *SecureMintConfig) Validate() error { + if cfg == nil { + return errors.New("secure mint plugin config cannot be nil") + } + + if cfg.Token == "" { + return errors.New("token cannot be empty") + } + + if cfg.Reserves == "" { + return errors.New("reserves cannot be empty") + } + + return nil +} diff --git a/core/services/ocr3/securemint/config/config_test.go b/core/services/ocr3/securemint/config/config_test.go new file mode 100644 index 00000000000..7f238fdd812 --- /dev/null +++ b/core/services/ocr3/securemint/config/config_test.go @@ -0,0 +1,129 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_Validate(t *testing.T) { + tests := []struct { + name string + cfg *SecureMintConfig + err bool + }{ + { + name: "valid config", + cfg: &SecureMintConfig{ + Token: "eth", + Reserves: "platform", + }, + err: false, + }, + { + name: "nil config", + cfg: nil, + err: true, + }, + { + name: "invalid token", + cfg: &SecureMintConfig{ + Token: "", + Reserves: "platform", + }, + err: true, + }, + { + name: "invalid reserves", + cfg: &SecureMintConfig{ + Token: "eth", + Reserves: "", + }, + err: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.cfg.Validate() + if tt.err { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestParseSecureMintConfig(t *testing.T) { + tests := []struct { + name string + configJSON string + expectedToken string + expectedReserves string + expectedChainSelectors []string + expectError bool + }{ + { + name: "empty config is invalid", + configJSON: "", + expectError: true, + }, + { + name: "custom values", + configJSON: `{"token": "btc", "reserves": "custom", "chainSelectors": ["8953668971247136127", "729797994450396300"]}`, + expectedToken: "btc", + expectedReserves: "custom", + expectedChainSelectors: []string{"8953668971247136127", "729797994450396300"}, + expectError: false, + }, + { + name: "partial config uses empty string", + configJSON: `{"token": "link", "chainSelectors": ["8953668971247136127", "729797994450396300"]}`, + expectedToken: "link", + expectedReserves: "", + expectedChainSelectors: []string{"8953668971247136127", "729797994450396300"}, + expectError: false, + }, + { + name: "partial config uses empty string 2", + configJSON: `{"reserves": "custom", "chainSelectors": ["8953668971247136127", "729797994450396300"]}`, + expectedToken: "", + expectedReserves: "custom", + expectedChainSelectors: []string{"8953668971247136127", "729797994450396300"}, + expectError: false, + }, + { + name: "partial config uses empty slice", + configJSON: `{"token": "btc", "reserves": "custom"}`, + expectedToken: "btc", + expectedReserves: "custom", + expectedChainSelectors: nil, + expectError: false, + }, + { + name: "invalid JSON", + configJSON: `{"token": "btc", "reserves":}`, + expectedToken: "", + expectedReserves: "", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config, err := Parse([]byte(tt.configJSON)) + + if tt.expectError { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.NotNil(t, config) + require.Equal(t, tt.expectedToken, config.Token) + require.Equal(t, tt.expectedReserves, config.Reserves) + require.Equal(t, tt.expectedChainSelectors, config.ChainSelectors) + }) + } +} diff --git a/core/services/ocr3/securemint/ea/ea.go b/core/services/ocr3/securemint/ea/ea.go new file mode 100644 index 00000000000..18c8d728fb6 --- /dev/null +++ b/core/services/ocr3/securemint/ea/ea.go @@ -0,0 +1,184 @@ +package ea + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "math/big" + "strconv" + "time" + + "github.com/smartcontractkit/chainlink-common/pkg/types/core/securemint" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/job" + sm_config "github.com/smartcontractkit/chainlink/v2/core/services/ocr3/securemint/config" + "github.com/smartcontractkit/chainlink/v2/core/services/ocrcommon" + "github.com/smartcontractkit/chainlink/v2/core/services/pipeline" +) + +// externalAdapter implements securemint.ExternalAdapter +var _ securemint.ExternalAdapter = &externalAdapter{} + +type externalAdapter struct { + config *sm_config.SecureMintConfig + chainSelectors []uint64 // use parsed chain selectors from config + runner pipeline.Runner + job job.Job + spec pipeline.Spec + saver ocrcommon.Saver + lggr logger.Logger +} + +func NewExternalAdapter(config *sm_config.SecureMintConfig, runner pipeline.Runner, job job.Job, spec pipeline.Spec, saver ocrcommon.Saver, lggr logger.Logger) (*externalAdapter, error) { + chainSelectors := make([]uint64, 0, len(config.ChainSelectors)) + for _, chainSelector := range config.ChainSelectors { + chainSelectorUint64, err := strconv.ParseUint(chainSelector, 10, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse chain selector: %s", chainSelector) + } + chainSelectors = append(chainSelectors, chainSelectorUint64) + } + + return &externalAdapter{config: config, chainSelectors: chainSelectors, runner: runner, job: job, spec: spec, saver: saver, lggr: lggr}, nil +} + +// GetPayload retrieves the payload for the given blocks by executing a pipeline run. +func (ea *externalAdapter) GetPayload(ctx context.Context, blocks securemint.Blocks) (securemint.ExternalAdapterPayload, error) { + ea.lggr.Debugf("GetPayload called with blocks parameter: %v", blocks) + + // Create the request for the external adapter + req := Request{ + Token: ea.config.Token, + Reserves: ea.config.Reserves, + } + + // coalesce blocks with config.ChainSelectors + coalescedBlocks := make(map[uint64]uint64) + for _, chainSelector := range ea.chainSelectors { + coalescedBlocks[chainSelector] = uint64(blocks[securemint.ChainSelector(chainSelector)]) + } + + // add coalesced blocks to request + for chainSelector, blockNumber := range coalescedBlocks { + req.SupplyChains = append(req.SupplyChains, strconv.FormatUint(chainSelector, 10)) + req.SupplyChainBlocks = append(req.SupplyChainBlocks, blockNumber) + } + + // Serialize EA request to JSON + reqJSON, err := json.Marshal(req) + if err != nil { + return securemint.ExternalAdapterPayload{}, fmt.Errorf("failed to marshal ea request: %w (request: %#v)", err, req) + } + + ea.lggr.Debugf("GetPayload serialized ea request to JSON: %v", string(reqJSON)) + + // Execute the request + vars := map[string]any{ + "jb": map[string]any{ + "databaseID": ea.job.ID, + "externalJobID": ea.job.ExternalJobID, + "name": ea.job.Name.ValueOrZero(), + }, + "action": "get_payload", + "ea_request": req, + } + + run, trrs, err := ea.runner.ExecuteRun(ctx, ea.spec, pipeline.NewVarsFrom(vars)) + if err != nil { + return securemint.ExternalAdapterPayload{}, fmt.Errorf("failed to execute GetPayload: %w", err) + } + + ea.saver.Save(run) + + // Parse and return results + for _, trr := range trrs { + if !trr.IsTerminal() { + continue + } + + resultMap, ok := trr.Result.Value.(map[string]any) + if !ok { + return securemint.ExternalAdapterPayload{}, fmt.Errorf("unexpected result type for GetPayload: %T", trr.Result.Value) + } + + payload, err := ea.convertMapToPayload(resultMap) + if err != nil { + return securemint.ExternalAdapterPayload{}, fmt.Errorf("failed to convert EA response map to payload: %w, map: %#v", err, resultMap) + } + + ea.lggr.Debugw("GetPayload result", "payload", payload) + if len(blocks) == 0 { + ea.lggr.Debugw("Plugin does not know about any chains or blocks yet, not returning any mintables") + // set Mintables to empty map - plugin will error out if it's not empty when it hasn't requested any mintables yet + // NB: this will be fixed in v0.5 of the plugin. + payload.Mintables = make(securemint.Mintables) + } + ea.lggr.Debugw("GetPayload returning", "payload", payload) + + return payload, nil + } + + return securemint.ExternalAdapterPayload{}, errors.New("no terminal result for GetPayload") +} + +// convertMapToPayload converts a map[string]any response to securemint.ExternalAdapterPayload +func (ea *externalAdapter) convertMapToPayload(resultMap map[string]any) (securemint.ExternalAdapterPayload, error) { + // Marshal and unmarshal to convert to Response struct + b, err := json.Marshal(resultMap) + if err != nil { + return securemint.ExternalAdapterPayload{}, fmt.Errorf("failed to marshal EA payload map: %w", err) + } + + ea.lggr.Debugf("EA response: %s", string(b)) + + var eaResponse Response + if err := json.Unmarshal(b, &eaResponse); err != nil { + return securemint.ExternalAdapterPayload{}, fmt.Errorf("failed to unmarshal EA response: %w", err) + } + + // Create the payload + payload := securemint.ExternalAdapterPayload{ + Mintables: make(securemint.Mintables), + LatestBlocks: make(securemint.Blocks), + } + + // Convert mintables + for chainSelector, mintable := range eaResponse.Mintables { + chainSelectorUint64, err := strconv.ParseUint(chainSelector, 10, 64) + if err != nil { + return securemint.ExternalAdapterPayload{}, fmt.Errorf("failed to parse chain selector: %s", chainSelector) + } + + mintableAmount, ok := new(big.Int).SetString(mintable.Mintable, 10) + if !ok { + return securemint.ExternalAdapterPayload{}, fmt.Errorf("failed to parse mintable amount: %s", mintable.Mintable) + } + + payload.Mintables[securemint.ChainSelector(chainSelectorUint64)] = securemint.BlockMintablePair{ + Block: securemint.BlockNumber(mintable.Block), + Mintable: mintableAmount, + } + } + + // Convert reserve info + reserveAmount, ok := new(big.Int).SetString(eaResponse.ReserveInfo.ReserveAmount, 10) + if !ok { + return securemint.ExternalAdapterPayload{}, fmt.Errorf("failed to parse reserve amount: %s", eaResponse.ReserveInfo.ReserveAmount) + } + payload.ReserveInfo = securemint.ReserveInfo{ + ReserveAmount: reserveAmount, + Timestamp: time.UnixMilli(eaResponse.ReserveInfo.Timestamp), + } + + // Convert latest blocks + for chainSelector, block := range eaResponse.LatestBlocks { + chainSelectorUint64, err := strconv.ParseUint(chainSelector, 10, 64) + if err != nil { + return securemint.ExternalAdapterPayload{}, fmt.Errorf("failed to parse chain selector: %s", chainSelector) + } + payload.LatestBlocks[securemint.ChainSelector(chainSelectorUint64)] = securemint.BlockNumber(block) + } + + return payload, nil +} diff --git a/core/services/ocr3/securemint/ea/ea_test.go b/core/services/ocr3/securemint/ea/ea_test.go new file mode 100644 index 00000000000..0d3fdaa47df --- /dev/null +++ b/core/services/ocr3/securemint/ea/ea_test.go @@ -0,0 +1,119 @@ +package ea + +import ( + "context" + "encoding/json" + "math/big" + "testing" + "time" + + "github.com/smartcontractkit/chainlink-common/pkg/types/core/securemint" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/job" + sm_config "github.com/smartcontractkit/chainlink/v2/core/services/ocr3/securemint/config" + "github.com/smartcontractkit/chainlink/v2/core/services/ocrcommon" + "github.com/smartcontractkit/chainlink/v2/core/services/pipeline" + "github.com/smartcontractkit/chainlink/v2/core/services/pipeline/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func Test_GetPayload(t *testing.T) { + + // Setup test context, logger, and other dependencies + ctx := testutils.Context(t) + lggr := logger.NullLogger + runner := mocks.NewRunner(t) + saver := ocrcommon.NewResultRunSaver( + runner, + lggr, + 1000, + 100, + ) + + config := &sm_config.SecureMintConfig{ + Token: "eth", + Reserves: "platform", + ChainSelectors: []string{"5009297550715157269"}, + } + job := job.Job{} + spec := pipeline.Spec{} + executedRun := &pipeline.Run{} + + ea, err := NewExternalAdapter(config, runner, job, spec, saver, lggr) + require.NoError(t, err) + + results := pipeline.TaskRunResults{ + { + Task: &pipeline.AnyTask{}, + Result: pipeline.Result{ + Value: map[string]any{ // outer `data` field is already stripped off in the parse step of the pipeline + "mintables": map[string]any{ + "5009297550715157269": map[string]any{ + "mintable": "10", + "block": 8, + }, + }, + "reserveInfo": map[string]any{ + "reserveAmount": "10332550000000000000000", + "timestamp": 1749483841486, + }, + "latestBlocks": map[string]any{ + "5009297550715157269": 23, + }, + }, + Error: nil, + }, + }, + } + + // Capture the 'ea_request' parameter from the pipeline run + var eaRequest any + runner.EXPECT().ExecuteRun(mock.Anything, mock.Anything, mock.Anything).Return(executedRun, results, nil).Run(func(_ context.Context, _ pipeline.Spec, vars pipeline.Vars) { + var err error + eaRequest, err = vars.Get("ea_request") + require.NoError(t, err) + }) + + payload, err := ea.GetPayload(ctx, securemint.Blocks{1234567890: 1234567890, 5009297550715157269: 10}) + require.NoError(t, err, "GetPayload should not return an error") + + // Validate the 'ea_request' parameter serialized to json + eaRequestJSON, err := json.Marshal(eaRequest) + require.NoError(t, err, "Failed to marshal ea_request to JSON") + assert.JSONEq(t, + `{ + "reserves": "platform", + "supplyChains": [ + "5009297550715157269" + ], + "supplyChainBlocks": [ + 10 + ], + "token": "eth" + }`, + string(eaRequestJSON), + ) + + // Validate the resulting payload + amount, ok := big.NewInt(10).SetString("10332550000000000000000", 10) + require.True(t, ok, "Failed to parse reserve amount from string") + expectedPayload := securemint.ExternalAdapterPayload{ + Mintables: securemint.Mintables{ + 5009297550715157269: { + Block: 8, + Mintable: big.NewInt(10), + }, + }, + ReserveInfo: securemint.ReserveInfo{ + ReserveAmount: amount, + Timestamp: time.UnixMilli(1749483841486), + }, + LatestBlocks: securemint.Blocks{ + 5009297550715157269: 23, + }, + } + assert.Equal(t, expectedPayload, payload) +} diff --git a/core/services/ocr3/securemint/ea/types.go b/core/services/ocr3/securemint/ea/types.go new file mode 100644 index 00000000000..aaa93e8a76d --- /dev/null +++ b/core/services/ocr3/securemint/ea/types.go @@ -0,0 +1,57 @@ +package ea + +// Request represents the request structure sent to the secure mint external adapter. +// Example (sent in the 'data' field): +// +// { +// "data": { +// "token": "eth", +// "reserves": "platform", +// "supplyChains": [ +// "5009297550715157269" +// ], +// "supplyChainBlocks": [ +// 0 +// ] +// } +// } +type Request struct { + Token string `json:"token"` + Reserves string `json:"reserves"` + SupplyChains []string `json:"supplyChains,omitempty"` + SupplyChainBlocks []uint64 `json:"supplyChainBlocks,omitempty"` +} + +// Response represents the response structure from the secure mint external adapter. +// Example: +// +// { +// "mintables": { +// "5009297550715157269": { +// "mintable": "5", +// "block": 22667990 +// } +// }, +// "reserveInfo": { +// "reserveAmount": "10332550000000000000000", +// "timestamp": 1749483841486 +// }, +// "latestBlocks": { +// "5009297550715157269": 22667990 +// } +// } +type Response struct { + Mintables map[string]MintableInfo `json:"mintables"` + ReserveInfo ReserveInfo `json:"reserveInfo"` + LatestBlocks map[string]uint64 `json:"latestBlocks"` +} + +type MintableInfo struct { + Mintable string `json:"mintable"` + Block uint64 `json:"block"` +} + +type ReserveInfo struct { + ReserveAmount string `json:"reserveAmount"` + Timestamp int64 `json:"timestamp"` +} diff --git a/core/services/ocr3/securemint/integrationtest/helpers_test.go b/core/services/ocr3/securemint/integrationtest/helpers_test.go new file mode 100644 index 00000000000..5f5d186e3eb --- /dev/null +++ b/core/services/ocr3/securemint/integrationtest/helpers_test.go @@ -0,0 +1,372 @@ +package integrationtest + +import ( + "crypto/ed25519" + "encoding/json" + "fmt" + "io" + "math/big" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + "go.uber.org/zap/zaptest/observer" + + commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config" + evmtypes "github.com/smartcontractkit/chainlink-evm/pkg/types" + "github.com/smartcontractkit/chainlink/v2/core/bridges" + "github.com/smartcontractkit/chainlink/v2/core/config/toml" + "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/keystest" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" + "github.com/smartcontractkit/chainlink/v2/core/services/job" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/chaintype" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/csakey" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ocr2key" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/validate" + sm_ea "github.com/smartcontractkit/chainlink/v2/core/services/ocr3/securemint/ea" + "github.com/smartcontractkit/chainlink/v2/core/services/ocrbootstrap" + "github.com/smartcontractkit/chainlink/v2/core/store/models" + "github.com/smartcontractkit/chainlink/v2/core/utils/testutils/heavyweight" + ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "github.com/smartcontractkit/wsrpc/credentials" +) + +type node struct { + app chainlink.Application + clientPubKey credentials.StaticSizedPublicKey + keyBundle ocr2key.KeyBundle + observedLogs *observer.ObservedLogs +} + +func (node *node) addBootstrapJob(t *testing.T, spec string) *job.Job { + job, err := ocrbootstrap.ValidatedBootstrapSpecToml(spec) + require.NoError(t, err) + err = node.app.AddJobV2(testutils.Context(t), &job) + require.NoError(t, err) + return &job +} + +func setupNode( + t *testing.T, + port int, + dbName string, + backend evmtypes.Backend, + csaKey csakey.KeyV2, + f func(*chainlink.Config), +) (app chainlink.Application, peerID string, clientPubKey credentials.StaticSizedPublicKey, ocr2kb ocr2key.KeyBundle, observedLogs *observer.ObservedLogs) { + k := big.NewInt(int64(port)) // keys unique to port + p2pKey := p2pkey.MustNewV2XXXTestingOnly(k) + rdr := keystest.NewRandReaderFromSeed(int64(port)) + ocr2kb = ocr2key.MustNewInsecure(rdr, chaintype.EVM) + + p2paddresses := []string{fmt.Sprintf("127.0.0.1:%d", port)} + + config, _ := heavyweight.FullTestDBV2(t, func(c *chainlink.Config, _ *chainlink.Secrets) { + // set finality depth to 1 so we don't have to wait for multiple blocks + c.EVM[0].FinalityDepth = ptr[uint32](1) + + // [JobPipeline] + c.JobPipeline.MaxSuccessfulRuns = ptr(uint64(1000)) + c.JobPipeline.VerboseLogging = ptr(true) + + // [Feature] + c.Feature.UICSAKeys = ptr(true) + c.Feature.LogPoller = ptr(true) + c.Feature.FeedsManager = ptr(false) + + // [OCR] + c.OCR.Enabled = ptr(false) + + // [OCR2] + c.OCR2.Enabled = ptr(true) + c.OCR2.ContractPollInterval = commonconfig.MustNewDuration(500 * time.Millisecond) + + // [P2P] + c.P2P.PeerID = ptr(p2pKey.PeerID()) + c.P2P.TraceLogging = ptr(true) + + // [P2P.V2] + c.P2P.V2.Enabled = ptr(true) + c.P2P.V2.AnnounceAddresses = &p2paddresses + c.P2P.V2.ListenAddresses = &p2paddresses + c.P2P.V2.DeltaDial = commonconfig.MustNewDuration(500 * time.Millisecond) + c.P2P.V2.DeltaReconcile = commonconfig.MustNewDuration(5 * time.Second) + + // [Log] + c.Log.Level = ptr(toml.LogLevel(zapcore.DebugLevel)) // generally speaking we want debug level for logs unless overridden + + // [EVM.Transactions] + for _, evmCfg := range c.EVM { + evmCfg.Transactions.Enabled = ptr(false) // don't need txmgr + } + + // Optional overrides + if f != nil { + f(c) + } + }) + + lggr, observedLogs := logger.TestLoggerObserved(t, config.Log().Level()) + + app = cltest.NewApplicationWithConfigV2AndKeyOnSimulatedBlockchain(t, config, backend, p2pKey, ocr2kb, csaKey, lggr.Named(dbName)) + err := app.Start(testutils.Context(t)) + require.NoError(t, err) + + t.Cleanup(func() { + assert.NoError(t, app.Stop()) + }) + + return app, p2pKey.PeerID().Raw(), csaKey.StaticSizedPublicKey(), ocr2kb, observedLogs +} + +func ptr[T any](t T) *T { return &t } + +func createSecureMintBootstrapJob(t *testing.T, bootstrapNode node, configuratorAddress common.Address, chainID, fromBlock string) *job.Job { + return bootstrapNode.addBootstrapJob(t, fmt.Sprintf(` + type = "bootstrap" + relay = "evm" + schemaVersion = 1 + name = "bootstrap-secure-mint" + contractID = "%s" + contractConfigTrackerPollInterval = "1s" + contractConfigConfirmations = 1 + + [relayConfig] + chainID = %s + fromBlock = %s + providerType = "securemint" + lloDonID = 1 + lloConfigMode = "bluegreen"`, // Using lloConfigMode 'bluegreen' since otherwise LLO config poller won't work + configuratorAddress.Hex(), + chainID, + fromBlock), + ) +} + +func addSecureMintOCRJobs( + t *testing.T, + nodes []node, + configuratorAddress common.Address, +) (jobIDs map[int]int32) { + // node idx => job id + jobIDs = make(map[int]int32) + + // Create one bridge and one SM Feed OCR job on each node + for i, node := range nodes { + name := "securemint-ea" + + bmBridge := createSecureMintBridge(t, name, i, node.app.BridgeORM()) + t.Logf("Created secure mint bridge %s on node %d", bmBridge, i) + + jobID := addSecureMintJob( + t, + node, + configuratorAddress, + bmBridge, + ) + jobIDs[i] = jobID + t.Logf("Added secure mint job with id %d on node %d", jobID, i) + } + return jobIDs +} + +func addSecureMintJob( + t *testing.T, + node node, + configuratorAddress common.Address, + bridgeName string, +) (id int32) { + + spec := getSecureMintJobSpec(t, configuratorAddress.Hex(), node.keyBundle.ID(), node.clientPubKey[:], bridgeName) + + c := node.app.GetConfig() + job, err := validate.ValidatedOracleSpecToml(testutils.Context(t), c.OCR2(), c.Insecure(), spec, nil) + require.NoError(t, err) + + err = node.app.AddJobV2(testutils.Context(t), &job) + require.NoError(t, err) + t.Logf("Added secure mint job spec %s", job.ExternalJobID) + + return job.ID +} + +func getSecureMintJobSpec(t *testing.T, ocrContractAddress, keyBundleID string, publicKey ed25519.PublicKey, bridgeName string) string { + + t.Logf("Using transmitter address %x for job", publicKey) + + return fmt.Sprintf(` + type = "offchainreporting2" + relay = "evm" + schemaVersion = 1 + pluginType = "securemint" + name = "secure mint spec" + contractID = "%s" + ocrKeyBundleID = "%s" + transmitterID = "%x" + contractConfigConfirmations = 1 + contractConfigTrackerPollInterval = "1s" + observationSource = """ + // data source 1 + ds1 [type=bridge name="%s" requestData=<{ "data": $(ea_request) }>]; + ds1_parse [type=jsonparse path="data"]; + + ds1 -> ds1_parse -> answer1; + + answer1 [type=any index=0]; + """ + + allowNoBootstrappers = false + + [relayConfig] + chainID = 1337 + fromBlock = 1 + providerType = "securemint" + lloDonID = 1 + lloConfigMode = "bluegreen" + + [pluginConfig] + maxChains = 5 + token = "btc" + reserves = "custom" + chainSelectors = ["8953668971247136127", "729797994450396300"] + `, // Using lloConfigMode 'bluegreen' since otherwise LLO config poller won't work + ocrContractAddress, // contract address + keyBundleID, // ocr key bundle id + publicKey, // transmitter id + bridgeName) // bridge name +} + +// Based on https://chainlink-core.slack.com/archives/C090PQH50M6/p1749483857095389?thread_ts=1749482941.061609&cid=C090PQH50M6 +func createSecureMintBridge(t *testing.T, name string, i int, borm bridges.ORM) (bridgeName string) { + ctx := testutils.Context(t) + + initialResponse := sm_ea.Response{ + Mintables: map[string]sm_ea.MintableInfo{}, + LatestBlocks: map[string]uint64{ + "8953668971247136127": 40, // "bitcoin-testnet-rootstock" + "729797994450396300": 5, // "telos-evm-testnet" + }, + ReserveInfo: sm_ea.ReserveInfo{ + ReserveAmount: "1000", + Timestamp: time.Now().UnixMilli(), + }, + } + jsonInitialResp, err := json.Marshal(initialResponse) + require.NoError(t, err) + + fullResponse := sm_ea.Response{ + Mintables: map[string]sm_ea.MintableInfo{ + "8953668971247136127": { // "bitcoin-testnet-rootstock" + Block: uint64(40), + Mintable: "10", + }, + "729797994450396300": { // "telos-evm-testnet" + Block: uint64(5), + Mintable: "25", + }, + }, + LatestBlocks: map[string]uint64{ + "8953668971247136127": 42, // "bitcoin-testnet-rootstock" + "729797994450396300": 7, // "telos-evm-testnet" + }, + ReserveInfo: sm_ea.ReserveInfo{ + ReserveAmount: "500", + Timestamp: time.Now().UnixMilli(), + }, + } + jsonFullResponse, err := json.Marshal(fullResponse) + require.NoError(t, err) + + //nolint:testifylint // allow require.NoError in the http server + bridge := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + body, err := io.ReadAll(req.Body) + defer req.Body.Close() + require.NoError(t, err) + t.Logf("Received request for secure mint bridge %s on node %d: path %s, request body %s", name, i, req.URL.String(), string(body)) + + // Parse the request body into a map to extract the 'data' field + var requestMap map[string]any + err = json.Unmarshal(body, &requestMap) + require.NoError(t, err, "Failed to parse request body as map for bridge %s on node %d", name, i) + + dataField, exists := requestMap["data"] + require.True(t, exists, "Request body should contain 'data' field for bridge %s on node %d", name, i) + + // Marshal the data field back to JSON and parse as ea.Request + dataBytes, err := json.Marshal(dataField) + require.NoError(t, err, "Failed to marshal data field for bridge %s on node %d", name, i) + var eaRequest sm_ea.Request + err = json.Unmarshal(dataBytes, &eaRequest) + require.NoError(t, err, "Failed to parse request body as ea.Request for bridge %s on node %d", name, i) + + // Validate the parsed ea.Request + assert.Equal(t, "btc", eaRequest.Token, "Token should be 'eth'") + assert.Equal(t, "custom", eaRequest.Reserves, "Reserves should be 'platform'") + + // Return initial EA response if empty request (first round) + if len(eaRequest.SupplyChains) == 0 && len(eaRequest.SupplyChainBlocks) == 0 { + t.Logf("Received empty supply chains for secure mint bridge %s on node %d, returning initial response", name, i) + res.WriteHeader(http.StatusOK) + _, err = res.Write(fmt.Appendf(nil, `{"data": %s}`, string(jsonInitialResp))) + require.NoError(t, err) + return + } + + // Validate non-empty request + assert.Contains(t, eaRequest.SupplyChains, "8953668971247136127", "Supply chains should contain bitcoin-testnet-rootstock") + assert.Contains(t, eaRequest.SupplyChains, "729797994450396300", "Supply chains should contain telos-evm-testnet") + assert.Len(t, eaRequest.SupplyChains, 2, "Should have exactly 2 supply chains") + + assert.Len(t, eaRequest.SupplyChainBlocks, 2, "Should have exactly 2 supply chain blocks") + assert.GreaterOrEqual(t, eaRequest.SupplyChainBlocks[0], uint64(0), "Supply chain block should be at least 0") + assert.GreaterOrEqual(t, eaRequest.SupplyChainBlocks[1], uint64(0), "Supply chain block should be at least 0") + + // Return full EA response with mintable amounts + res.WriteHeader(http.StatusOK) + resp := fmt.Sprintf(`{"data": %s}`, string(jsonFullResponse)) + t.Logf("Responding from secure mint bridge %s on node %d with: %s", name, i, resp) + _, err = res.Write([]byte(resp)) + require.NoError(t, err) + })) + t.Cleanup(func() { + t.Logf("Closing secure mint bridge %s on node %d with url %s", name, i, bridge.URL) + bridge.Close() + }) + t.Logf("Created secure mint bridge %s on node %d with URL %s", name, i, bridge.URL) + + u, _ := url.Parse(bridge.URL) + bridgeName = fmt.Sprintf("bridge-%s-%d", name, i) + require.NoError(t, borm.CreateBridgeType(ctx, &bridges.BridgeType{ + Name: bridges.BridgeName(bridgeName), + URL: models.WebURL(*u), + })) + + return bridgeName +} + +// secureMintReport mimics por.PorReport in the securemint plugin. +type secureMintReport struct { + ConfigDigest ocr2types.ConfigDigest `json:"configDigest"` + SeqNr uint64 `json:"seqNr"` + Block uint64 `json:"block"` + Mintable *big.Int `json:"mintable"` +} + +// secureMintOffchainConfig mimics por.PorOffchainConfig in the securemint plugin. +type secureMintOffchainConfig struct { + MaxChains uint32 // The maximum number of chains that can be tracked by the external adapter. +} + +func (c *secureMintOffchainConfig) Serialize() ([]byte, error) { + return json.Marshal(c) +} diff --git a/core/services/ocr3/securemint/integrationtest/integration_test.go b/core/services/ocr3/securemint/integrationtest/integration_test.go new file mode 100644 index 00000000000..8a9077509ef --- /dev/null +++ b/core/services/ocr3/securemint/integrationtest/integration_test.go @@ -0,0 +1,470 @@ +package integrationtest + +import ( + "crypto/ed25519" + "encoding/hex" + "encoding/json" + "fmt" + "math/big" + "strings" + "sync" + "testing" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + gethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/ethconfig" + "github.com/onsi/gomega" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/atomic" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + smtypes "github.com/smartcontractkit/chainlink-common/pkg/types/core/securemint" + datastreamsllo "github.com/smartcontractkit/chainlink-data-streams/llo" + "github.com/smartcontractkit/chainlink-evm/gethwrappers/data-feeds/generated/data_feeds_cache" + "github.com/smartcontractkit/chainlink-evm/gethwrappers/llo-feeds/generated/configurator" + "github.com/smartcontractkit/chainlink-evm/pkg/assets" + evmtestutils "github.com/smartcontractkit/chainlink-evm/pkg/testutils" + evmtypes "github.com/smartcontractkit/chainlink-evm/pkg/types" + "github.com/smartcontractkit/chainlink-protos/cre/go/values" + "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/csakey" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr3/securemint" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/llo" + "github.com/smartcontractkit/freeport" + "github.com/smartcontractkit/libocr/commontypes" + "github.com/smartcontractkit/libocr/offchainreporting2plus/confighelper" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3confighelper" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" + ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" +) + +var ( + fNodes = uint8(1) + nNodes = 4 // number of nodes (not including bootstrap) +) + +// TestIntegration_SecureMint_happy_path tests runs a small DON which runs the secure mint plugin +// and verifies that it can successfully create reports. +// +// Inspired by: +// * core/internal/features/ocr2/features_ocr2_test.go +// * core/services/ocr2/plugins/ocr2keeper/integration_21_test.go +func TestIntegration_SecureMint_happy_path(t *testing.T) { + const salt = 100 + + clientCSAKeys := make([]csakey.KeyV2, nNodes) + clientPubKeys := make([]ed25519.PublicKey, nNodes) + for i := range nNodes { + k := big.NewInt(int64(salt + i)) + key := csakey.MustNewV2XXXTestingOnly(k) + clientCSAKeys[i] = key + clientPubKeys[i] = key.PublicKey + } + + steve, backend := setupBlockchain(t) + fromBlock, err := backend.Client().BlockNumber(testutils.Context(t)) + require.NoError(t, err) + t.Logf("Starting from block: %d", fromBlock) + + // Setup bootstrap + bootstrapCSAKey := csakey.MustNewV2XXXTestingOnly(big.NewInt(salt - 1)) + bootstrapNodePort := freeport.GetOne(t) + appBootstrap, bootstrapPeerID, _, bootstrapKb, _ := setupNode(t, bootstrapNodePort, "bootstrap_securemint", backend, bootstrapCSAKey, nil) + bootstrapNode := node{app: appBootstrap, keyBundle: bootstrapKb} + + p2pV2Bootstrappers := []commontypes.BootstrapperLocator{ + // Supply the bootstrap IP and port as a V2 peer address + {PeerID: bootstrapPeerID, Addrs: []string{fmt.Sprintf("127.0.0.1:%d", bootstrapNodePort)}}, + } + + // Setup oracle nodes + oracles, nodes := setupNodes(t, nNodes, backend, clientCSAKeys, func(c *chainlink.Config) { + // inform node about bootstrap node + c.P2P.V2.DefaultBootstrappers = &p2pV2Bootstrappers + }) + + allowedSenders := make([]common.Address, len(nodes)) + for i, node := range nodes { + keys, err := node.app.GetKeyStore().Eth().EnabledKeysForChain(testutils.Context(t), testutils.SimulatedChainID) + require.NoError(t, err) + allowedSenders[i] = keys[0].Address // assuming the first key is the transmitter + } + + _, configuratorAddress, configDigest := setSecureMintOnchainConfigUsingOCR3Configurator(t, steve, backend, nodes, oracles) + + t.Logf("Creating bootstrap job with configurator address: %s", configuratorAddress.Hex()) + bootstrapJob := createSecureMintBootstrapJob(t, bootstrapNode, configuratorAddress, testutils.SimulatedChainID.String(), fmt.Sprintf("%d", fromBlock)) + t.Logf("Created bootstrap job: %s with id %d", bootstrapJob.Name.ValueOrZero(), bootstrapJob.ID) + + jobIDs := addSecureMintOCRJobs(t, nodes, configuratorAddress) + + t.Logf("jobIDs: %v", jobIDs) + validateJobsRunningSuccessfully(t, nodes, jobIDs, configDigest) +} + +func setupBlockchain(t *testing.T) ( + *bind.TransactOpts, + evmtypes.Backend, +) { + steve := evmtestutils.MustNewSimTransactor(t) // config contract deployer and owner + genesisData := gethtypes.GenesisAlloc{steve.From: {Balance: assets.Ether(1000).ToInt()}} + backend := cltest.NewSimulatedBackend(t, genesisData, ethconfig.Defaults.Miner.GasCeil) + backend.Commit() + backend.Commit() // ensure starting block number at least 1 + + return steve, backend +} + +func setupNodes(t *testing.T, nNodes int, backend evmtypes.Backend, clientCSAKeys []csakey.KeyV2, f func(*chainlink.Config)) (oracles []confighelper.OracleIdentityExtra, nodes []node) { + ports := freeport.GetN(t, nNodes) + for i := range nNodes { + app, peerID, transmitter, kb, observedLogs := setupNode(t, ports[i], fmt.Sprintf("oracle_securemint_%d", i), backend, clientCSAKeys[i], f) + + nodes = append(nodes, node{ + app: app, + clientPubKey: transmitter, + keyBundle: kb, + observedLogs: observedLogs, + }) + offchainPublicKey, err := hex.DecodeString(strings.TrimPrefix(kb.OnChainPublicKey(), "0x")) + require.NoError(t, err) + oracles = append(oracles, confighelper.OracleIdentityExtra{ + OracleIdentity: confighelper.OracleIdentity{ + OnchainPublicKey: offchainPublicKey, + TransmitAccount: ocr2types.Account(hex.EncodeToString(transmitter[:])), + OffchainPublicKey: kb.OffchainPublicKey(), + PeerID: peerID, + }, + ConfigEncryptionPublicKey: kb.ConfigEncryptionPublicKey(), + }) + } + return +} + +func validateJobsRunningSuccessfully(t *testing.T, nodes []node, jobIDs map[int]int32, expectedConfigDigest ocr2types.ConfigDigest) { + + // 0. Add ourselves as a subscriber to the secure mint trigger capability + var collectedEvents []capabilities.TriggerResponse + transmissions := atomic.NewInt32(0) + transmitter := securemint.XXX_SingletonTransmitter.Load().(capabilities.TriggerCapability) + triggerConfig, err := values.NewMap(map[string]any{ + "workflowID": "securemint-workflow", + "maxFrequencyMs": 1000, + }) + require.NoError(t, err) + registerCh, err := transmitter.RegisterTrigger(testutils.Context(t), capabilities.TriggerRegistrationRequest{ + TriggerID: "securemint-trigger", + Metadata: capabilities.RequestMetadata{ + WorkflowID: "securemint-workflow", + }, + Config: triggerConfig, + }) + require.NoError(t, err) + go func() { + for resp := range registerCh { + t.Logf("Received trigger response: %+v", resp) + collectedEvents = append(collectedEvents, resp) + outputs, err2 := resp.Event.Outputs.Unwrap() + require.NoError(t, err2) + t.Logf("Received trigger response outputs: %+v", outputs) + transmissions.Inc() + } + }() + + // 1. Assert no job spec errors + for i, node := range nodes { + jobs, _, err := node.app.JobORM().FindJobs(testutils.Context(t), 0, 1000) + require.NoErrorf(t, err, "assert error finding jobs for node %d", i) + t.Logf("%d jobs found for node %d", len(jobs), i) + for _, j := range jobs { + t.Logf("job %d on node %d oracle spec: %#v", j.ID, i, j.OCR2OracleSpec) + t.Logf("job %d on node %d pipeline spec: %#v", j.ID, i, j.PipelineSpec) + } + // No spec errors + for _, j := range jobs { + ignore := 0 + for _, jse := range j.JobSpecErrors { + // Non-fatal timing related error, ignore for testing. + if strings.Contains(jse.Description, "leader's phase conflicts tGrace timeout") { + ignore++ + } else { + t.Errorf("assert error: job spec error on node %d: %v", i, jse) + } + } + require.Lenf(t, j.JobSpecErrors, ignore, "assert error: job spec errors on node %d", i) + } + } + t.Logf("No job spec errors identified for any node") + + // 2. Assert that all the Secure Mint jobs get a run with valid values eventually + var wg sync.WaitGroup + for i, node := range nodes { + wg.Add(1) + go func() { + defer wg.Done() + + pr := cltest.WaitForPipelineComplete(t, i, jobIDs[i], 1, 0, node.app.JobORM(), 30*time.Second, 1*time.Second) + outputs, err2 := pr[0].Outputs.MarshalJSON() + if !assert.NoError(t, err2) { + t.Logf("assert error marshalling outputs for job %d: %v", jobIDs[i], err2) + return + } + + t.Logf("Pipeline itself is %+v", pr[0]) + t.Logf("Pipeline run outputs are %s", string(outputs)) + }() + } + t.Logf("waiting for pipeline runs to complete") + wg.Wait() + t.Logf("All pipeline runs completed successfully") + + // 3. Check that correct reports are transmitted to trigger subscribers + // Report data is based on mock EA data, see helpers_test.go#createSecureMintBridge() for more details + + // Make sure trigger events have been collected + gomega.NewWithT(t).Eventually(func() bool { + t.Logf("Current event count: %d", len(collectedEvents)) + return len(collectedEvents) >= 2 + }, 10*time.Second, 500*time.Millisecond).Should(gomega.BeTrue()) + + require.GreaterOrEqual(t, len(collectedEvents), 2, "Should have collected at least 2 events (1 per chain)") + + for i, event := range collectedEvents { + t.Logf("Event %d: ID=%s, TriggerType=%s", i, event.Event.ID, event.Event.TriggerType) + + // Verify the event structure + assert.Equal(t, "securemint-trigger@1.0.0", event.Event.TriggerType) + assert.Contains(t, event.Event.ID, "securemint_") + + // Extract and validate the report data + outputs := event.Event.Outputs + require.NotNil(t, outputs, "Event outputs should not be nil") + + // Check that we have the expected fields + _, hasReport := outputs.Underlying["report"] + _, hasSigs := outputs.Underlying["sigs"] + _, hasSeqNr := outputs.Underlying["seqNr"] + _, hasConfigDigest := outputs.Underlying["configDigest"] + + assert.True(t, hasReport, "Event should contain report field") + assert.True(t, hasSigs, "Event should contain sigs field") + assert.True(t, hasSeqNr, "Event should contain seqNr field") + assert.True(t, hasConfigDigest, "Event should contain configDigest field") + + // Extract the report data + var reportBytes []byte + err = outputs.Underlying["report"].UnwrapTo(&reportBytes) + require.NoError(t, err, "Failed to extract report bytes from event %d with event id %s", i, event.Event.ID) + + // Parse the OCR3 report + var ocr3Report ocr3types.ReportWithInfo[smtypes.ChainSelector] + err = json.Unmarshal(reportBytes, &ocr3Report) + require.NoError(t, err, "Failed to unmarshal OCR3 report from event %d", i) + + t.Logf("Event %d OCR3 report: %+v: %+v", i, ocr3Report.Info, string(ocr3Report.Report)) + + type report struct { + configDigest ocr2types.ConfigDigest + mintable *big.Int + block int64 + } + + expectedReports := map[string]report{ + "729797994450396300": { + configDigest: expectedConfigDigest, + mintable: big.NewInt(25), + block: 5, + }, + "8953668971247136127": { + configDigest: expectedConfigDigest, + mintable: big.NewInt(10), + block: 40, + }, + } + + var smReport secureMintReport + err = json.Unmarshal(ocr3Report.Report, &smReport) + require.NoError(t, err, "failed to unmarshal to secureMintReport: %+v", ocr3Report.Report) + + expectedReport, ok := expectedReports[fmt.Sprintf("%d", ocr3Report.Info)] + require.True(t, ok, "expected report not found for chain selector %s (report was %+v)", ocr3Report.Info, smReport) + + assert.Equal(t, expectedReport.configDigest, smReport.ConfigDigest, "configDigest mismatch") + assert.Equal(t, expectedReport.mintable, smReport.Mintable, "mintable mismatch") + assert.Equal(t, expectedReport.block, int64(smReport.Block), "block number mismatch") //nolint:gosec // disable G115 since we control the data we won't encounter an overflow here + assert.Positive(t, smReport.SeqNr, "sequence number should be greater than 0") + } +} + +func setSecureMintOnchainConfigUsingOCR3Configurator(t *testing.T, steve *bind.TransactOpts, backend evmtypes.Backend, nodes []node, oracles []confighelper.OracleIdentityExtra) (*configurator.Configurator, common.Address, ocr2types.ConfigDigest) { + + // 1. Deploy configurator contract + configuratorAddress, _, configurator, err := configurator.DeployConfigurator(steve, backend.Client()) + require.NoError(t, err) + backend.Commit() + + // Ensure we have finality depth worth of blocks to start. + for range 5 { + backend.Commit() + } + t.Logf("Deployed OCR3Configurator contract at: %s", configuratorAddress.Hex()) + + // 2. Get the oracle config + smPluginConfig := secureMintOffchainConfig{MaxChains: 5} + smPluginConfigBytes, err := smPluginConfig.Serialize() + require.NoError(t, err) + + // using the data streams llo codec for the validation about version and predecessor config digest in the Configurator contract: https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/llo-feeds/v0.5.0/configuration/Configurator.sol#L116-L124 + onchainConfig, err := (&datastreamsllo.EVMOnchainConfigCodec{}).Encode(datastreamsllo.OnchainConfig{ + Version: 1, + PredecessorConfigDigest: nil, + }) + require.NoError(t, err) + + signers, _, f, outOnchainConfig, offchainConfigVersion, offchainConfig, err := ocr3confighelper.ContractSetConfigArgsForTests( + 2*time.Second, // deltaProgress, + 20*time.Second, // deltaResend, + 400*time.Millisecond, // deltaInitial, + 500*time.Millisecond, // deltaRound, + 250*time.Millisecond, // deltaGrace, + 300*time.Millisecond, // deltaCertifiedCommitRequest, + 1*time.Minute, // deltaStage, + 100, // rMax, + []int{len(oracles)}, // s, + oracles, // oracles, + smPluginConfigBytes, // reportingPluginConfig, + nil, // maxDurationInitialization, + 250*time.Millisecond, // maxDurationQuery, + 1*time.Second, // maxDurationObservation, + 1*time.Second, // maxDurationShouldAcceptAttestedReport, + 1*time.Second, // maxDurationShouldTransmitAcceptedReport, + int(fNodes), // f, + onchainConfig, // onchainConfig (binary blob containing configuration passed through to the ReportingPlugin and also available to the contract. Unlike ReportingPluginConfig which is only available offchain.) + ) + require.NoError(t, err) + + // 3. Set config on the contract + signerKeys := make([][]byte, len(signers)) + for i, signer := range signers { + signerKeys[i] = signer + } + + // use csa keys as transmitters, similar to LLO + transmitters := make([][32]byte, nNodes) + for i := range nNodes { + transmitters[i] = nodes[i].clientPubKey + } + t.Logf("transmitters: %v", transmitters) + + configID := [32]byte{} + copy(configID[:], common.FromHex("0x0000000000000000000000000000000000000000000000000000000000000001")) + + _, err = configurator.SetProductionConfig(steve, configID, signerKeys, transmitters, f, outOnchainConfig, offchainConfigVersion, offchainConfig) + if err != nil { + t.Logf("Error: %s", err) + errString, err := rPCErrorFromError(err) + require.NoError(t, err) + t.Fatalf("Failed to configure contract: %s %s", errString, err) + } + + // make sure config is finalized + for range 5 { + backend.Commit() + } + + var topic common.Hash + topic = llo.ProductionConfigSet + + logs, err := backend.Client().FilterLogs(testutils.Context(t), ethereum.FilterQuery{Addresses: []common.Address{configuratorAddress}, Topics: [][]common.Hash{[]common.Hash{topic, configID}}}) + require.NoError(t, err) + require.GreaterOrEqual(t, len(logs), 1) + cfg, err := llo.DecodeProductionConfigSetLog(logs[len(logs)-1].Data) + require.NoError(t, err) + + t.Logf("Configurator config digest: 0x%x", cfg.ConfigDigest) + + return configurator, configuratorAddress, cfg.ConfigDigest +} + +func rPCErrorFromError(txError error) (string, error) { + errBytes, err := json.Marshal(txError) + if err != nil { + return "", err + } + var callErr struct { + Code int + Data string `json:"data"` + Message string `json:"message"` + } + err = json.Unmarshal(errBytes, &callErr) + if err != nil { + return "", err + } + // If the error data is blank + if len(callErr.Data) == 0 { + return callErr.Data, nil + } + // Some nodes prepend "Reverted " and we also remove the 0x + trimmed := strings.TrimPrefix(callErr.Data, "Reverted ")[2:] + data, err := hex.DecodeString(trimmed) + if err != nil { + return "", err + } + revert, err := abi.UnpackRevert(data) + // If we can't decode the revert reason, return the raw data + if err != nil { + return callErr.Data, nil + } + return revert, nil +} + +// Not used yet, in scope for chain writing +func setupDataFeedsCacheContract(t *testing.T, steve *bind.TransactOpts, backend evmtypes.Backend, allowedSenders []common.Address, workflowOwner, workflowName string) ( + common.Address, *data_feeds_cache.DataFeedsCache) { + + addr, _, dataFeedsCache, err := data_feeds_cache.DeployDataFeedsCache(steve, backend.Client()) + require.NoError(t, err) + backend.Commit() + + var nameBytes [10]byte + copy(nameBytes[:], workflowName) + + ownerAddr := common.HexToAddress(workflowOwner) + + _, err = dataFeedsCache.SetFeedAdmin(steve, ownerAddr, true) + require.NoError(t, err) + + backend.Commit() + + metadatas := make([]data_feeds_cache.DataFeedsCacheWorkflowMetadata, len(allowedSenders)) + for i, sender := range allowedSenders { + metadatas[i] = + data_feeds_cache.DataFeedsCacheWorkflowMetadata{ + AllowedSender: sender, + AllowedWorkflowOwner: ownerAddr, + AllowedWorkflowName: nameBytes, + } + } + + feedIDBytes := [16]byte{} + copy(feedIDBytes[:], common.FromHex("0xA1B2C3D4E5F600010203040506070809")) + + _, err = dataFeedsCache.SetDecimalFeedConfigs(steve, [][16]byte{feedIDBytes}, []string{"securemint"}, metadatas) + if err != nil { + errString, err := rPCErrorFromError(err) + require.NoError(t, err) + + t.Fatalf("Failed to configure contract: %s", errString) + } + + backend.Commit() + + return addr, dataFeedsCache +} diff --git a/core/services/ocr3/securemint/keyringadapter/README.md b/core/services/ocr3/securemint/keyringadapter/README.md new file mode 100644 index 00000000000..9406fc70954 --- /dev/null +++ b/core/services/ocr3/securemint/keyringadapter/README.md @@ -0,0 +1,111 @@ +# PoR OCR3 OnchainKeyring Adapter + +This file contains an adapter implementation that enables the use of existing OCR2 OnchainKeyring implementations with the OCR3 PoR (Proof of Reserve) plugin. + +## Overview + +The `OnchainKeyringAdapter` wraps an existing `types.OnchainKeyring` (OCR2) and adapts it to implement `ocr3types.OnchainKeyring[ChainSelector]` (OCR3) specifically for the PoR system. + +## Key Features + +- **Interface Adaptation**: Converts between OCR2 and OCR3 keyring interfaces +- **Parameter Conversion**: Automatically converts OCR3 parameters (config digest, sequence number, report with info) to OCR2 ReportContext format +- **Backward Compatibility**: Allows reuse of existing OCR2 keyring implementations +- **Type Safety**: Strongly typed for PoR ChainSelector + +## Usage Example + +```go +package main + +import ( + "github.com/smartcontractkit/por_mock_ocr3plugin/por" + "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ocr2key" +) + +func main() { + // Create an OCR2 keyring (example using EVM keyring) + ocr2Bundle, err := ocr2key.New(chaintype.EVM) + if err != nil { + panic(err) + } + + // Wrap the OCR2 keyring with the PoR adapter + porKeyring := por.NewOnchainKeyringAdapter(ocr2Bundle) + + // Now you can use porKeyring as an ocr3types.OnchainKeyring[por.ChainSelector] + // for the PoR OCR3 plugin + + // Example usage in OCR3 context: + configDigest := types.ConfigDigest([32]byte{1, 2, 3}) + seqNr := uint64(42) + reportWithInfo := ocr3types.ReportWithInfo[por.ChainSelector]{ + Report: []byte("example-report"), + Info: por.ChainSelector(1234), + } + + signature, err := porKeyring.Sign(configDigest, seqNr, reportWithInfo) + if err != nil { + panic(err) + } + + isValid := porKeyring.Verify( + porKeyring.PublicKey(), + configDigest, + seqNr, + reportWithInfo, + signature, + ) + + println("Signature valid:", isValid) +} +``` + +## Implementation Details + +### Interface Mapping + +The adapter maps OCR3 interface methods to OCR2 interface methods as follows: + +| OCR3 Method | OCR2 Method | Conversion | +|-------------|-------------|------------| +| `PublicKey()` | `PublicKey()` | Direct passthrough | +| `Sign(ConfigDigest, uint64, ReportWithInfo[ChainSelector])` | `Sign(ReportContext, Report)` | Converts parameters to ReportContext | +| `Verify(OnchainPublicKey, ConfigDigest, uint64, ReportWithInfo[ChainSelector], []byte)` | `Verify(OnchainPublicKey, ReportContext, Report, []byte)` | Converts parameters to ReportContext | +| `MaxSignatureLength()` | `MaxSignatureLength()` | Direct passthrough | + +### Parameter Conversion + +OCR3 parameters are converted to OCR2 ReportContext as follows: + +```go +reportContext := types.ReportContext{ + ReportTimestamp: types.ReportTimestamp{ + ConfigDigest: configDigest, // From OCR3 parameter + Epoch: uint32(seqNr), // OCR3 sequence number as epoch + Round: 0, // Fixed to 0 (OCR3 doesn't use rounds) + }, + ExtraHash: [32]byte{}, // Empty hash +} +``` + +## Benefits + +1. **Reusability**: Existing OCR2 keyrings can be used with OCR3 PoR without modification +2. **Simplicity**: Single adapter handles all necessary conversions +3. **Type Safety**: Generic implementation ensures compile-time type checking +4. **Testing**: Comprehensive test suite ensures correct parameter conversion + +## Related Files + +- `onchain_keyring_adapter.go` - Main adapter implementation +- `onchain_keyring_adapter_test.go` - Comprehensive test suite +- `types.go` - PoR-specific type definitions including ChainSelector +- `external_adapter_interface.go` - Original external adapter interface + +## See Also + +- Reference implementations in `/core/services/ocrcommon/adapters.go` +- OCR2 keyring implementations in `/core/services/keystore/keys/ocr2key/` +- OCR3 types in `libocr/offchainreporting2plus/ocr3types/` diff --git a/core/services/ocr3/securemint/keyringadapter/onchain_keyring_adapter.go b/core/services/ocr3/securemint/keyringadapter/onchain_keyring_adapter.go new file mode 100644 index 00000000000..0770ba70483 --- /dev/null +++ b/core/services/ocr3/securemint/keyringadapter/onchain_keyring_adapter.go @@ -0,0 +1,81 @@ +package keyringadapter + +import ( + "github.com/smartcontractkit/chainlink-common/pkg/types/core/securemint" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" + "github.com/smartcontractkit/libocr/offchainreporting2plus/types" +) + +// SecureMintOCR3OnchainKeyringAdapter adapts an OCR2 OnchainKeyring to implement ocr3types.OnchainKeyring[ChainSelector] +// This adapter enables the use of existing OCR2 keyrings with the OCR3 PoR plugin. +// Copied and adapted from core/services/ocrcommon/adapters.go +// Ideally we use ocrcommon.OCR3OnchainKeyringMultiChainAdapter instead? Problem is that that one is not typed, it assumes []byte as the Report type, while we use por.ChainSelector. +type SecureMintOCR3OnchainKeyringAdapter struct { + ocr2Keyring types.OnchainKeyring +} + +// Ensure OnchainKeyringAdapter implements the OCR3 OnchainKeyring interface for PoR ChainSelector +var _ ocr3types.OnchainKeyring[securemint.ChainSelector] = &SecureMintOCR3OnchainKeyringAdapter{} + +// NewSecureMintOCR3OnchainKeyringAdapter creates a new adapter that wraps an OCR2 OnchainKeyring +// to implement the OCR3 OnchainKeyring interface for PoR ChainSelector. +func NewSecureMintOCR3OnchainKeyringAdapter(keyring types.OnchainKeyring) *SecureMintOCR3OnchainKeyringAdapter { + return &SecureMintOCR3OnchainKeyringAdapter{ + ocr2Keyring: keyring, + } +} + +// PublicKey returns the public key of the underlying OCR2 keyring. +func (adapter *SecureMintOCR3OnchainKeyringAdapter) PublicKey() types.OnchainPublicKey { + return adapter.ocr2Keyring.PublicKey() +} + +// Sign creates a signature over the given report using the OCR2 keyring. +// It converts the OCR3 parameters (config digest, sequence number, and report with info) +// into the OCR2 ReportContext format expected by the underlying keyring. +func (adapter *SecureMintOCR3OnchainKeyringAdapter) Sign( + configDigest types.ConfigDigest, + seqNr uint64, + reportWithInfo ocr3types.ReportWithInfo[securemint.ChainSelector], +) (signature []byte, err error) { + // Convert OCR3 parameters to OCR2 ReportContext + // Note: seqNr is converted to uint32 for Epoch field, which may truncate for very large values + reportContext := types.ReportContext{ + ReportTimestamp: types.ReportTimestamp{ + ConfigDigest: configDigest, + Epoch: uint32(seqNr), //nolint:gosec // Intentional conversion, matches OCR protocol + Round: 0, // OCR3 doesn't use rounds in the same way as OCR2 + }, + ExtraHash: [32]byte{}, // Initialize with empty hash + } + + return adapter.ocr2Keyring.Sign(reportContext, reportWithInfo.Report) +} + +// Verify verifies a signature over the given report using the OCR2 keyring. +// It converts the OCR3 parameters into the OCR2 ReportContext format for verification. +func (adapter *SecureMintOCR3OnchainKeyringAdapter) Verify( + publicKey types.OnchainPublicKey, + configDigest types.ConfigDigest, + seqNr uint64, + reportWithInfo ocr3types.ReportWithInfo[securemint.ChainSelector], + signature []byte, +) bool { + // Convert OCR3 parameters to OCR2 ReportContext + // Note: seqNr is converted to uint32 for Epoch field, which may truncate for very large values + reportContext := types.ReportContext{ + ReportTimestamp: types.ReportTimestamp{ + ConfigDigest: configDigest, + Epoch: uint32(seqNr), //nolint:gosec // Intentional conversion, matches OCR protocol + Round: 0, // OCR3 doesn't use rounds in the same way as OCR2 + }, + ExtraHash: [32]byte{}, // Initialize with empty hash + } + + return adapter.ocr2Keyring.Verify(publicKey, reportContext, reportWithInfo.Report, signature) +} + +// MaxSignatureLength returns the maximum signature length from the underlying OCR2 keyring. +func (adapter *SecureMintOCR3OnchainKeyringAdapter) MaxSignatureLength() int { + return adapter.ocr2Keyring.MaxSignatureLength() +} diff --git a/core/services/ocr3/securemint/keyringadapter/onchain_keyring_adapter_test.go b/core/services/ocr3/securemint/keyringadapter/onchain_keyring_adapter_test.go new file mode 100644 index 00000000000..95caba0613b --- /dev/null +++ b/core/services/ocr3/securemint/keyringadapter/onchain_keyring_adapter_test.go @@ -0,0 +1,178 @@ +package keyringadapter + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/types/core/securemint" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" + "github.com/smartcontractkit/libocr/offchainreporting2plus/types" +) + +// mockOCR2OnchainKeyring is a mock implementation of types.OnchainKeyring for testing +type mockOCR2OnchainKeyring struct { + publicKey types.OnchainPublicKey + maxSignatureLength int + signFunc func(types.ReportContext, types.Report) ([]byte, error) + verifyFunc func(types.OnchainPublicKey, types.ReportContext, types.Report, []byte) bool +} + +func (m *mockOCR2OnchainKeyring) PublicKey() types.OnchainPublicKey { + return m.publicKey +} + +func (m *mockOCR2OnchainKeyring) Sign(ctx types.ReportContext, report types.Report) ([]byte, error) { + if m.signFunc != nil { + return m.signFunc(ctx, report) + } + return []byte("mock-signature"), nil +} + +func (m *mockOCR2OnchainKeyring) Verify( + pubKey types.OnchainPublicKey, + ctx types.ReportContext, + report types.Report, + signature []byte, +) bool { + if m.verifyFunc != nil { + return m.verifyFunc(pubKey, ctx, report, signature) + } + return true +} + +func (m *mockOCR2OnchainKeyring) MaxSignatureLength() int { + return m.maxSignatureLength +} + +func TestPorOnchainKeyringAdapter(t *testing.T) { + // Setup test data + testPublicKey := types.OnchainPublicKey("test-public-key") + testConfigDigest := types.ConfigDigest([32]byte{1, 2, 3, 4, 5}) + testSeqNr := uint64(42) + testReport := types.Report([]byte("test-report")) + testChainSelector := securemint.ChainSelector(1234) + testSignature := []byte("test-signature") + testMaxSigLen := 65 + + reportWithInfo := ocr3types.ReportWithInfo[securemint.ChainSelector]{ + Report: testReport, + Info: testChainSelector, + } + + t.Run("adapter implements the correct interface", func(t *testing.T) { + mockKeyring := &mockOCR2OnchainKeyring{ + publicKey: testPublicKey, + maxSignatureLength: testMaxSigLen, + } + + adapter := NewSecureMintOCR3OnchainKeyringAdapter(mockKeyring) + + // Verify that the adapter implements the OCR3 OnchainKeyring interface + var _ ocr3types.OnchainKeyring[securemint.ChainSelector] = adapter + }) + + t.Run("PublicKey returns the underlying keyring's public key", func(t *testing.T) { + mockKeyring := &mockOCR2OnchainKeyring{ + publicKey: testPublicKey, + maxSignatureLength: testMaxSigLen, + } + + adapter := NewSecureMintOCR3OnchainKeyringAdapter(mockKeyring) + assert.Equal(t, testPublicKey, adapter.PublicKey()) + }) + + t.Run("MaxSignatureLength returns the underlying keyring's max signature length", func(t *testing.T) { + mockKeyring := &mockOCR2OnchainKeyring{ + publicKey: testPublicKey, + maxSignatureLength: testMaxSigLen, + } + + adapter := NewSecureMintOCR3OnchainKeyringAdapter(mockKeyring) + assert.Equal(t, testMaxSigLen, adapter.MaxSignatureLength()) + }) + + t.Run("Sign correctly converts OCR3 parameters to OCR2 format", func(t *testing.T) { + var capturedReportContext types.ReportContext + var capturedReport types.Report + + mockKeyring := &mockOCR2OnchainKeyring{ + publicKey: testPublicKey, + maxSignatureLength: testMaxSigLen, + signFunc: func(ctx types.ReportContext, report types.Report) ([]byte, error) { + capturedReportContext = ctx + capturedReport = report + return testSignature, nil + }, + } + + adapter := NewSecureMintOCR3OnchainKeyringAdapter(mockKeyring) + signature, err := adapter.Sign(testConfigDigest, testSeqNr, reportWithInfo) + + require.NoError(t, err) + assert.Equal(t, testSignature, signature) + + // Verify the conversion from OCR3 to OCR2 format + assert.Equal(t, testConfigDigest, capturedReportContext.ReportTimestamp.ConfigDigest) + assert.Equal(t, uint32(testSeqNr), capturedReportContext.ReportTimestamp.Epoch) + assert.Equal(t, uint8(0), capturedReportContext.ReportTimestamp.Round) + assert.Equal(t, [32]byte{}, capturedReportContext.ExtraHash) + assert.Equal(t, testReport, capturedReport) + }) + + t.Run("Verify correctly converts OCR3 parameters to OCR2 format", func(t *testing.T) { + var capturedPublicKey types.OnchainPublicKey + var capturedReportContext types.ReportContext + var capturedReport types.Report + var capturedSignature []byte + + mockKeyring := &mockOCR2OnchainKeyring{ + publicKey: testPublicKey, + maxSignatureLength: testMaxSigLen, + verifyFunc: func( + pubKey types.OnchainPublicKey, + ctx types.ReportContext, + report types.Report, + signature []byte, + ) bool { + capturedPublicKey = pubKey + capturedReportContext = ctx + capturedReport = report + capturedSignature = signature + return true + }, + } + + adapter := NewSecureMintOCR3OnchainKeyringAdapter(mockKeyring) + result := adapter.Verify(testPublicKey, testConfigDigest, testSeqNr, reportWithInfo, testSignature) + + assert.True(t, result) + + // Verify the conversion from OCR3 to OCR2 format + assert.Equal(t, testPublicKey, capturedPublicKey) + assert.Equal(t, testConfigDigest, capturedReportContext.ReportTimestamp.ConfigDigest) + assert.Equal(t, uint32(testSeqNr), capturedReportContext.ReportTimestamp.Epoch) + assert.Equal(t, uint8(0), capturedReportContext.ReportTimestamp.Round) + assert.Equal(t, [32]byte{}, capturedReportContext.ExtraHash) + assert.Equal(t, testReport, capturedReport) + assert.Equal(t, testSignature, capturedSignature) + }) + + t.Run("Sign and Verify work together", func(t *testing.T) { + mockKeyring := &mockOCR2OnchainKeyring{ + publicKey: testPublicKey, + maxSignatureLength: testMaxSigLen, + } + + adapter := NewSecureMintOCR3OnchainKeyringAdapter(mockKeyring) + + // Sign a report + signature, err := adapter.Sign(testConfigDigest, testSeqNr, reportWithInfo) + require.NoError(t, err) + + // Verify the signature + isValid := adapter.Verify(testPublicKey, testConfigDigest, testSeqNr, reportWithInfo, signature) + assert.True(t, isValid) + }) +} diff --git a/core/services/ocr3/securemint/services.go b/core/services/ocr3/securemint/services.go new file mode 100644 index 00000000000..6fb5161c52c --- /dev/null +++ b/core/services/ocr3/securemint/services.go @@ -0,0 +1,204 @@ +package securemint + +import ( + "context" + "errors" + "fmt" + "sync/atomic" + + "github.com/smartcontractkit/chainlink-common/pkg/loop" + "github.com/smartcontractkit/chainlink-common/pkg/types" + coretypes "github.com/smartcontractkit/chainlink-common/pkg/types/core" + "github.com/smartcontractkit/chainlink-common/pkg/types/core/securemint" + "github.com/smartcontractkit/chainlink/v2/core/config/env" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services" + "github.com/smartcontractkit/chainlink/v2/core/services/job" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr3/promwrapper" + sm_plugin_config "github.com/smartcontractkit/chainlink/v2/core/services/ocr3/securemint/config" + sm_ea "github.com/smartcontractkit/chainlink/v2/core/services/ocr3/securemint/ea" + "github.com/smartcontractkit/chainlink/v2/core/services/ocrcommon" + "github.com/smartcontractkit/chainlink/v2/core/services/pipeline" + evm_types "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" + "github.com/smartcontractkit/chainlink/v2/plugins" + libocr "github.com/smartcontractkit/libocr/offchainreporting2plus" +) + +var _ JobConfig = (*smJobConfig)(nil) + +type JobConfig interface { + JobPipelineMaxSuccessfulRuns() uint64 + JobPipelineResultWriteQueueDepth() uint64 + plugins.RegistrarConfig +} + +// concrete implementation of JobConfig +type smJobConfig struct { + jobPipelineMaxSuccessfulRuns uint64 + jobPipelineResultWriteQueueDepth uint64 + plugins.RegistrarConfig +} + +func NewJobConfig(jobPipelineMaxSuccessfulRuns uint64, jobPipelineResultWriteQueueDepth uint64, pluginProcessCfg plugins.RegistrarConfig) JobConfig { + return &smJobConfig{ + jobPipelineMaxSuccessfulRuns: jobPipelineMaxSuccessfulRuns, + jobPipelineResultWriteQueueDepth: jobPipelineResultWriteQueueDepth, + RegistrarConfig: pluginProcessCfg, + } +} + +func (m *smJobConfig) JobPipelineMaxSuccessfulRuns() uint64 { + return m.jobPipelineMaxSuccessfulRuns +} + +func (m *smJobConfig) JobPipelineResultWriteQueueDepth() uint64 { + return m.jobPipelineResultWriteQueueDepth +} + +// XXX_SingletonTransmitter is a hack to allow the secure mint integration test to access the transmitter in order to verify the sent reports. +var XXX_SingletonTransmitter atomic.Value // capabilities.TriggerCapability + +// NewSecureMintServices creates all securemint plugin specific services. +func NewSecureMintServices(ctx context.Context, + jb job.Job, + isNewlyCreatedJob bool, + relayer loop.Relayer, + pipelineRunner pipeline.Runner, + lggr logger.Logger, + argsNoPlugin libocr.OCR3OracleArgs[securemint.ChainSelector], + cfg JobConfig, + capabilitiesRegistry coretypes.CapabilitiesRegistry, +) (srvs []job.ServiceCtx, err error) { + // Parse and validate the secure mint plugin configuration + secureMintPluginConfig, err := sm_plugin_config.Parse(jb.OCR2OracleSpec.PluginConfig.Bytes()) + if err != nil { + return nil, fmt.Errorf("failed to parse secure mint plugin config: %w", err) + } + + if err = secureMintPluginConfig.Validate(); err != nil { + return nil, fmt.Errorf("invalid secure mint plugin config: %#v, %w", secureMintPluginConfig, err) + } + + spec := jb.OCR2OracleSpec + + // Get relay config to extract don ID + relayConfig, err := evm_types.NewRelayOpts(types.RelayArgs{ + ExternalJobID: jb.ExternalJobID, + JobID: jb.ID, + ContractID: spec.ContractID, + New: isNewlyCreatedJob, + RelayConfig: spec.RelayConfig.Bytes(), + ProviderType: string(spec.PluginType), + }).RelayConfig() + if err != nil { + return nil, fmt.Errorf("failed to get relay config: %w", err) + } + + // Create result run saver for pipeline execution + runSaver := ocrcommon.NewResultRunSaver( + pipelineRunner, + lggr, + cfg.JobPipelineMaxSuccessfulRuns(), + cfg.JobPipelineResultWriteQueueDepth(), + ) + + configProvider, err := relayer.NewConfigProvider(ctx, types.RelayArgs{ + ExternalJobID: jb.ExternalJobID, + JobID: jb.ID, + OracleSpecID: *jb.OCR2OracleSpecID, + ContractID: spec.ContractID, + New: isNewlyCreatedJob, + RelayConfig: spec.RelayConfig.Bytes(), + ProviderType: string(spec.PluginType), + }) + if err != nil { + return nil, fmt.Errorf("failed to create config provider: %w", err) + } + srvs = append(srvs, configProvider) + + argsNoPlugin.ContractConfigTracker = configProvider.ContractConfigTracker() + argsNoPlugin.OffchainConfigDigester = configProvider.OffchainConfigDigester() + + // Create the new secure mint transmitter with trigger capabilities + transmitterConfig := TransmitterConfig{ + Logger: lggr, + CapabilitiesRegistry: capabilitiesRegistry, + DonID: relayConfig.LLODONID, + TriggerCapabilityName: secureMintPluginConfig.TriggerCapabilityName, + TriggerCapabilityVersion: secureMintPluginConfig.TriggerCapabilityVersion, + TriggerTickerMinResolutionMs: secureMintPluginConfig.TriggerTickerMinResolutionMs, + TriggerSendChannelBufferSize: secureMintPluginConfig.TriggerSendChannelBufferSize, + } + + transmitter, err := transmitterConfig.NewTransmitter(spec.TransmitterID.String) + if err != nil { + return nil, fmt.Errorf("failed to create secure mint transmitter: %w", err) + } + srvs = append(srvs, transmitter) + argsNoPlugin.ContractTransmitter = transmitter + XXX_SingletonTransmitter.Store(transmitter) + + abort := func() { + if cerr := services.MultiCloser(srvs).Close(); cerr != nil { + lggr.Errorw("Error closing services", "err", cerr) + } + } + + // Create the reporting plugin factory + cmdName := env.SecureMintPlugin.Cmd.Get() + if cmdName == "" { + abort() + return nil, errors.New("secure mint plugin loop is not configured, non-loopp mode is not supported for secure mint") + } + lggr.Infof("Configuration indicates loopp usage for secure mint") + + // use unique logger names so we can use it to register a loop + secureMintLggr := lggr.Named("SecureMint").Named(spec.ContractID).Named(spec.GetID()) + envVars, err := plugins.ParseEnvFile(env.SecureMintPlugin.Env.Get()) + if err != nil { + err = fmt.Errorf("failed to parse secure mint env file: %w", err) + abort() + return + } + cmdFn, telem, err := cfg.RegisterLOOP(plugins.CmdConfig{ + ID: secureMintLggr.Name(), + Cmd: cmdName, + Env: envVars, + }) + if err != nil { + err = fmt.Errorf("failed to register loop: %w", err) + abort() + return + } + + ea, err := sm_ea.NewExternalAdapter(secureMintPluginConfig, pipelineRunner, jb, *jb.PipelineSpec, runSaver, lggr) + if err != nil { + return nil, fmt.Errorf("failed to create secure mint external adapter: %w", err) + } + + secureMintPluginFactory := loop.NewPluginSecureMintService(lggr, telem, cmdFn, ea) + srvs = append(srvs, secureMintPluginFactory) + + // Wrap the factory with prometheus metrics monitoring + promPluginFactory := promwrapper.NewReportingPluginFactory( + secureMintPluginFactory, + lggr, + "", + spec.ChainID, + "secure-mint", + ) + argsNoPlugin.ReportingPluginFactory = promPluginFactory + + // Create the oracle + var oracle libocr.Oracle + oracle, err = libocr.NewOracle(argsNoPlugin) + if err != nil { + abort() + return nil, fmt.Errorf("failed to create oracle: %w", err) + } + + // Assemble all services + srvs = append(srvs, runSaver, job.NewServiceAdapter(oracle)) + + return srvs, nil +} diff --git a/core/services/ocr3/securemint/transmitter.go b/core/services/ocr3/securemint/transmitter.go new file mode 100644 index 00000000000..bce856b6690 --- /dev/null +++ b/core/services/ocr3/securemint/transmitter.go @@ -0,0 +1,261 @@ +package securemint + +import ( + "context" + "encoding/json" + "fmt" + "sync" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + coretypes "github.com/smartcontractkit/chainlink-common/pkg/types/core" + "github.com/smartcontractkit/chainlink-common/pkg/types/core/securemint" + "github.com/smartcontractkit/chainlink-protos/cre/go/values" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr3/securemint/config" + "github.com/smartcontractkit/libocr/offchainreporting2/types" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" + ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" +) + +// These constants are used to identify the secure mint trigger capability. +const ( + defaultCapabilityName = "securemint-trigger" + defaultCapabilityVersion = "1.0.0" + defaultTickerResolutionMs = 1000 + defaultSendChannelBufferSize = 1000 +) + +// Transmitter is a wrapper for ocr3types.ContractTransmitter[securemint.ChainSelector] to add the Service interface to it. +type Transmitter interface { + ocr3types.ContractTransmitter[securemint.ChainSelector] + services.Service +} + +// TransmitterConfig is the configuration for the secure mint transmitter capability. +type TransmitterConfig struct { + Logger logger.Logger `json:"-"` + CapabilitiesRegistry coretypes.CapabilitiesRegistry `json:"-"` + DonID uint32 `json:"-"` + + TriggerCapabilityName string `json:"triggerCapabilityName"` + TriggerCapabilityVersion string `json:"triggerCapabilityVersion"` + TriggerTickerMinResolutionMs int `json:"triggerTickerMinResolutionMs"` + TriggerSendChannelBufferSize int `json:"triggerSendChannelBufferSize"` +} + +// NewTransmitter creates a new secure mint transmitter. +func (c TransmitterConfig) NewTransmitter(transmitterID string) (*transmitter, error) { + c.Logger.Infow("Initializing SecureMintTransmitter", "triggerCapabilityName", c.TriggerCapabilityName, "triggerCapabilityVersion", c.TriggerCapabilityVersion) + + t := &transmitter{ + config: c, + fromAccount: ocr2types.Account(transmitterID), + registry: c.CapabilitiesRegistry, + subscribers: make(map[string]*subscriber), + } + + // set default values if not provided + if t.config.TriggerCapabilityName == "" { + t.config.TriggerCapabilityName = defaultCapabilityName + } + if t.config.TriggerCapabilityVersion == "" { + t.config.TriggerCapabilityVersion = defaultCapabilityVersion + } + if t.config.TriggerTickerMinResolutionMs == 0 { + t.config.TriggerTickerMinResolutionMs = defaultTickerResolutionMs + } + if t.config.TriggerSendChannelBufferSize == 0 { + t.config.TriggerSendChannelBufferSize = defaultSendChannelBufferSize + } + + capInfo, err := capabilities.NewCapabilityInfo( + t.config.TriggerCapabilityName+"@"+t.config.TriggerCapabilityVersion, + capabilities.CapabilityTypeTrigger, + "Secure Mint Trigger", + ) + if err != nil { + return nil, err + } + t.CapabilityInfo = capInfo + + t.Service, t.eng = services.Config{ + Name: "SecureMintTransmitter", + Start: t.start, + Close: t.close, + }.NewServiceEngine(c.Logger) + + t.eng.Infow("SecureMintTransmitter initialized", "triggerCapabilityName", c.TriggerCapabilityName, "triggerCapabilityVersion", c.TriggerCapabilityVersion) + return t, nil +} + +type transmitter struct { + services.Service + eng *services.Engine + capabilities.CapabilityInfo + + config TransmitterConfig + fromAccount ocr2types.Account + registry coretypes.CapabilitiesRegistry + + subscribers map[string]*subscriber + mu sync.Mutex +} + +var _ Transmitter = &transmitter{} +var _ capabilities.TriggerCapability = &transmitter{} + +func (t *transmitter) start(ctx context.Context) error { + t.eng.Infow("Starting SecureMintTransmitter", "triggerCapabilityName", t.config.TriggerCapabilityName, "triggerCapabilityVersion", t.config.TriggerCapabilityVersion) + err := t.registry.Add(ctx, t) + if err != nil { + return fmt.Errorf("failed to add transmitter to registry: %w", err) + } + return nil +} + +func (t *transmitter) close() error { + t.eng.Infow("Closing SecureMintTransmitter", "triggerCapabilityName", t.config.TriggerCapabilityName, "triggerCapabilityVersion", t.config.TriggerCapabilityVersion) + return t.registry.Remove(context.Background(), t.CapabilityInfo.ID) +} + +// FromAccount returns the CSA public key of this node. +func (t *transmitter) FromAccount(context.Context) (ocr2types.Account, error) { + t.eng.Debugw("FromAccount", "fromAccount", t.fromAccount) + return t.fromAccount, nil +} + +// Transmit processes the secure mint report and transmits it as a trigger event to any subscribed workflows. +func (t *transmitter) Transmit( + ctx context.Context, + cd ocr2types.ConfigDigest, + seqNr uint64, + ocr3Report ocr3types.ReportWithInfo[securemint.ChainSelector], + sigs []types.AttributedOnchainSignature, +) error { + t.eng.Debugw("Transmit called", "cd", cd, "seqNr", seqNr, "report", ocr3Report, "sigs", sigs) + + // convert the secure mint report to a trigger event + capSigs := make([]capabilities.OCRAttributedOnchainSignature, len(sigs)) + for i, sig := range sigs { + capSigs[i] = capabilities.OCRAttributedOnchainSignature{ + Signer: uint32(sig.Signer), + Signature: sig.Signature, + } + } + + jsonOcr3Report, err := json.Marshal(ocr3Report) + if err != nil { + return fmt.Errorf("failed to marshal ocr3 report: %w", err) + } + + outputs, err := values.NewMap(map[string]any{ + "report": jsonOcr3Report, + "sigs": capSigs, + "seqNr": seqNr, + "configDigest": cd, + }) + if err != nil { + return fmt.Errorf("failed to create outputs map: %w", err) + } + + // use the seqNr as eventID to make sure we have unique event ids per report + // and that nodes sending the same report use the same event id (to enable consensus in the Workflow DON to work properly). + eventID := fmt.Sprintf("securemint_%d", seqNr) + + ev := &capabilities.TriggerEvent{ + TriggerType: t.CapabilityInfo.ID, + ID: eventID, + Outputs: outputs, + } + return t.processNewEvent(ctx, ev) +} + +// processNewEvent sends the trigger event to any subscribed workflows. +func (t *transmitter) processNewEvent(ctx context.Context, event *capabilities.TriggerEvent) error { + t.mu.Lock() + defer t.mu.Unlock() + t.eng.Debugw("processNewEvent pushing event", "eventID", event.ID) + + capResponse := capabilities.TriggerResponse{ + Event: *event, + } + + numIncludedSubscribers := 0 + for _, sub := range t.subscribers { + // include all subscribers (no frequency limiting for now) + select { + case sub.ch <- capResponse: + case <-ctx.Done(): + t.eng.Errorw("context done, dropping event", "eventID", event.ID) + return ctx.Err() + default: + // drop event if channel is full - processNewEvent() should be non-blocking + t.eng.Errorw("subscriber channel full, dropping event", "eventID", event.ID, "workflowID", sub.workflowID) + } + numIncludedSubscribers++ + } + + t.eng.Debugw("ProcessReport done", "eventID", event.ID, "numIncludedSubscribers", numIncludedSubscribers) + return nil +} + +// RegisterTrigger registers a new subscription to the secure mint trigger capability. +// This means that the workflow will receive a trigger event for each secure mint report. +func (t *transmitter) RegisterTrigger(ctx context.Context, req capabilities.TriggerRegistrationRequest) (<-chan capabilities.TriggerResponse, error) { + t.eng.Debugw("RegisterTrigger", "triggerID", req.TriggerID, "metadata", req.Metadata) + + config, err := validateConfig(req.Config, &t.config) + if err != nil { + return nil, fmt.Errorf("invalid config: %w", err) + } + + t.mu.Lock() + defer t.mu.Unlock() + + if _, ok := t.subscribers[req.TriggerID]; ok { + return nil, fmt.Errorf("triggerId %s already registered", t.ID) + } + + ch := make(chan capabilities.TriggerResponse, defaultSendChannelBufferSize) + t.subscribers[req.TriggerID] = &subscriber{ + ch: ch, + workflowID: req.Metadata.WorkflowID, + config: *config, + } + return ch, nil +} + +func validateConfig(registerConfig *values.Map, capabilityConfig *TransmitterConfig) (*config.SecureMintTriggerConfig, error) { + cfg := &config.SecureMintTriggerConfig{} + if err := registerConfig.UnwrapTo(cfg); err != nil { + return nil, err + } + if int64(cfg.MaxFrequencyMs)%int64(capabilityConfig.TriggerTickerMinResolutionMs) != 0 { //nolint:gosec // disable G115 + return nil, fmt.Errorf("MaxFrequencyMs must be a multiple of %d", capabilityConfig.TriggerTickerMinResolutionMs) + } + return cfg, nil +} + +// UnregisterTrigger unregisters a subscription to the secure mint trigger capability. +// This means that the workflow will no longer receive a trigger event for each secure mint report. +func (t *transmitter) UnregisterTrigger(ctx context.Context, req capabilities.TriggerRegistrationRequest) error { + t.eng.Debugw("UnregisterTrigger", "triggerID", req.TriggerID) + t.mu.Lock() + defer t.mu.Unlock() + + subscriber, ok := t.subscribers[req.TriggerID] + if !ok { + return fmt.Errorf("triggerId %s not registered", t.ID) + } + close(subscriber.ch) + delete(t.subscribers, req.TriggerID) + return nil +} + +// subscriber contains the channel to send a trigger response to (normally a CRE workflow). +type subscriber struct { + ch chan<- capabilities.TriggerResponse + workflowID string + config config.SecureMintTriggerConfig +} diff --git a/core/services/ocr3/securemint/transmitter_test.go b/core/services/ocr3/securemint/transmitter_test.go new file mode 100644 index 00000000000..3af8735e332 --- /dev/null +++ b/core/services/ocr3/securemint/transmitter_test.go @@ -0,0 +1,323 @@ +package securemint + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/types/core/securemint" + "github.com/smartcontractkit/chainlink-protos/cre/go/values" + p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" + "github.com/smartcontractkit/libocr/offchainreporting2/types" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" + ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" +) + +func TestTransmitter_NewTransmitter(t *testing.T) { + lggr := logger.Test(t) + + // Create a mock capabilities registry + mockRegistry := &mockCapabilitiesRegistry{} + + config := TransmitterConfig{ + Logger: lggr, + CapabilitiesRegistry: mockRegistry, + DonID: 1, + TriggerCapabilityName: "test-trigger", + TriggerCapabilityVersion: "1.0.0", + TriggerTickerMinResolutionMs: 1000, + TriggerSendChannelBufferSize: 1000, + } + + transmitter, err := config.NewTransmitter("test-transmitter") + require.NoError(t, err) + require.NotNil(t, transmitter) + + // Verify it implements the required interfaces + assert.Implements(t, (*Transmitter)(nil), transmitter) + assert.Implements(t, (*capabilities.TriggerCapability)(nil), transmitter) + + // Test service lifecycle + err = transmitter.Start(context.Background()) + require.NoError(t, err) + + err = transmitter.Close() + require.NoError(t, err) +} + +func TestTransmitter_RegisterTrigger(t *testing.T) { + lggr := logger.Test(t) + mockRegistry := &mockCapabilitiesRegistry{} + + config := TransmitterConfig{ + Logger: lggr, + CapabilitiesRegistry: mockRegistry, + DonID: 1, + TriggerCapabilityName: "test-trigger", + TriggerCapabilityVersion: "1.0.0", + TriggerTickerMinResolutionMs: 1000, + TriggerSendChannelBufferSize: 1000, + } + + transmitter, err := config.NewTransmitter("test-transmitter") + require.NoError(t, err) + + err = transmitter.Start(context.Background()) + require.NoError(t, err) + defer transmitter.Close() + + // Create trigger config as values.Map + triggerConfig, err := values.NewMap(map[string]any{ + "maxFrequencyMs": uint64(2000), + }) + require.NoError(t, err) + + // Test trigger registration + req := capabilities.TriggerRegistrationRequest{ + TriggerID: "test-trigger-1", + Config: triggerConfig, + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-workflow", + }, + } + + ch, err := transmitter.RegisterTrigger(context.Background(), req) + require.NoError(t, err) + require.NotNil(t, ch) + + // Test duplicate registration + _, err = transmitter.RegisterTrigger(context.Background(), req) + require.Error(t, err) + assert.Contains(t, err.Error(), "already registered") + + // Test unregister + err = transmitter.UnregisterTrigger(context.Background(), req) + require.NoError(t, err) + + // Test unregister non-existent + err = transmitter.UnregisterTrigger(context.Background(), req) + require.Error(t, err) + assert.Contains(t, err.Error(), "not registered") +} + +func TestTransmitter_FromAccount(t *testing.T) { + lggr := logger.Test(t) + mockRegistry := &mockCapabilitiesRegistry{} + + config := TransmitterConfig{ + Logger: lggr, + CapabilitiesRegistry: mockRegistry, + DonID: 1, + TriggerCapabilityName: "test-trigger", + TriggerCapabilityVersion: "1.0.0", + TriggerTickerMinResolutionMs: 1000, + TriggerSendChannelBufferSize: 1000, + } + + transmitter, err := config.NewTransmitter("0x11234") + require.NoError(t, err) + + account, err := transmitter.FromAccount(context.Background()) + require.NoError(t, err) + require.NotEmpty(t, account) + + // Verify account format includes logger name and don ID + assert.Contains(t, string(account), lggr.Name()) + assert.Equal(t, "0x11234", string(account)) +} + +func TestTransmitter_Transmit(t *testing.T) { + lggr := logger.Test(t) + mockRegistry := &mockCapabilitiesRegistry{} + + config := TransmitterConfig{ + Logger: lggr, + CapabilitiesRegistry: mockRegistry, + DonID: 1, + TriggerCapabilityName: "test-trigger", + TriggerCapabilityVersion: "1.0.0", + TriggerTickerMinResolutionMs: 1000, + TriggerSendChannelBufferSize: 1000, + } + + transmitter, err := config.NewTransmitter("test-transmitter") + require.NoError(t, err) + + err = transmitter.Start(context.Background()) + require.NoError(t, err) + defer transmitter.Close() + + // Register a trigger to receive events + triggerConfig, err := values.NewMap(map[string]any{ + "maxFrequencyMs": uint64(2000), + }) + require.NoError(t, err) + + req := capabilities.TriggerRegistrationRequest{ + TriggerID: "test-trigger-1", + Config: triggerConfig, + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-workflow", + }, + } + + ch, err := transmitter.RegisterTrigger(context.Background(), req) + require.NoError(t, err) + require.NotNil(t, ch) + + t.Run("successful transmission", func(t *testing.T) { + // Create test data + cd := ocr2types.ConfigDigest{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32} + seqNr := uint64(123) + report := ocr3types.ReportWithInfo[securemint.ChainSelector]{ + Report: []byte("test report data"), + Info: securemint.ChainSelector(1), // Use ChainSelector as the Info type + } + sigs := []types.AttributedOnchainSignature{ + { + Signature: []byte("signature1"), + Signer: 1, + }, + { + Signature: []byte("signature2"), + Signer: 2, + }, + } + + // Transmit the report + err := transmitter.Transmit(context.Background(), cd, seqNr, report, sigs) + require.NoError(t, err) + + // Wait for the event to be processed and sent to the channel + select { + case response := <-ch: + // Verify the trigger response + assert.Equal(t, "securemint_123", response.Event.ID) + assert.Equal(t, transmitter.CapabilityInfo.ID, response.Event.TriggerType) + + // Verify outputs + outputs := response.Event.Outputs + assert.NotNil(t, outputs) + + // Check seqNr + var seqNrVal uint64 + err = outputs.Underlying["seqNr"].UnwrapTo(&seqNrVal) + require.NoError(t, err) + assert.Equal(t, uint64(123), seqNrVal) + + // Check configDigest + var cdVal ocr2types.ConfigDigest + err = outputs.Underlying["configDigest"].UnwrapTo(&cdVal) + require.NoError(t, err) + assert.Equal(t, cd, cdVal) + + // Check signatures + var capSigs []capabilities.OCRAttributedOnchainSignature + err = outputs.Underlying["sigs"].UnwrapTo(&capSigs) + require.NoError(t, err) + assert.Len(t, capSigs, 2) + assert.Equal(t, uint32(1), capSigs[0].Signer) + assert.Equal(t, []byte("signature1"), capSigs[0].Signature) + assert.Equal(t, uint32(2), capSigs[1].Signer) + assert.Equal(t, []byte("signature2"), capSigs[1].Signature) + + // Check report + var reportBytes []byte + err = outputs.Underlying["report"].UnwrapTo(&reportBytes) + require.NoError(t, err) + + // json umarshal bytes to string and check if it contains "test report data" + var report ocr3types.ReportWithInfo[securemint.ChainSelector] + err = json.Unmarshal(reportBytes, &report) + require.NoError(t, err) + assert.Equal(t, "test report data", string(report.Report)) + + case <-time.After(2 * time.Second): + t.Fatal("timeout waiting for trigger response") + } + }) +} + +func TestTransmitter_Transmit_NoSubscribers(t *testing.T) { + lggr := logger.Test(t) + mockRegistry := &mockCapabilitiesRegistry{} + + config := TransmitterConfig{ + Logger: lggr, + CapabilitiesRegistry: mockRegistry, + DonID: 1, + TriggerCapabilityName: "test-trigger", + TriggerCapabilityVersion: "1.0.0", + TriggerTickerMinResolutionMs: 1000, + TriggerSendChannelBufferSize: 1000, + } + + transmitter, err := config.NewTransmitter("test-transmitter") + require.NoError(t, err) + + err = transmitter.Start(context.Background()) + require.NoError(t, err) + defer transmitter.Close() + + // Test transmission without any registered triggers + cd := ocr2types.ConfigDigest{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32} + seqNr := uint64(127) + report := ocr3types.ReportWithInfo[securemint.ChainSelector]{ + Report: []byte("test report data no subscribers"), + Info: securemint.ChainSelector(1), // Use ChainSelector as the Info type + } + sigs := []types.AttributedOnchainSignature{} + + // This should succeed even without subscribers + err = transmitter.Transmit(context.Background(), cd, seqNr, report, sigs) + require.NoError(t, err) +} + +// Mock capabilities registry for testing +type mockCapabilitiesRegistry struct{} + +func (m *mockCapabilitiesRegistry) Add(ctx context.Context, c capabilities.BaseCapability) error { + return nil +} + +func (m *mockCapabilitiesRegistry) Remove(ctx context.Context, ID string) error { + return nil +} + +func (m *mockCapabilitiesRegistry) Get(ctx context.Context, ID string) (capabilities.BaseCapability, error) { + return nil, nil +} + +func (m *mockCapabilitiesRegistry) List(ctx context.Context) ([]capabilities.BaseCapability, error) { + return nil, nil +} + +func (m *mockCapabilitiesRegistry) GetExecutable(ctx context.Context, ID string) (capabilities.ExecutableCapability, error) { + return nil, nil +} + +func (m *mockCapabilitiesRegistry) ConfigForCapability(ctx context.Context, capabilityID string, donID uint32) (capabilities.CapabilityConfiguration, error) { + return capabilities.CapabilityConfiguration{}, nil +} + +func (m *mockCapabilitiesRegistry) LocalNode(ctx context.Context) (capabilities.Node, error) { + return capabilities.Node{}, nil +} + +func (m *mockCapabilitiesRegistry) GetTrigger(ctx context.Context, ID string) (capabilities.TriggerCapability, error) { + return nil, nil +} + +func (m *mockCapabilitiesRegistry) NodeByPeerID(ctx context.Context, peerID p2ptypes.PeerID) (capabilities.Node, error) { + return capabilities.Node{}, nil +} + +func (m *mockCapabilitiesRegistry) DONsForCapability(ctx context.Context, capabilityID string) ([]capabilities.DONWithNodes, error) { + return nil, nil +} diff --git a/core/services/relay/evm/evm.go b/core/services/relay/evm/evm.go index 8f7e5ec36b9..471153eef25 100644 --- a/core/services/relay/evm/evm.go +++ b/core/services/relay/evm/evm.go @@ -690,7 +690,7 @@ func (r *Relayer) NewFunctionsProvider(ctx context.Context, rargs commontypes.Re return NewFunctionsProvider(ctx, r.chain, rargs, pargs, lggr, r.evmKeystore, functions.FunctionsPlugin) } -// NewConfigProvider is called by bootstrap jobs +// NewConfigProvider is called by bootstrap jobs and by the secure mint plugin func (r *Relayer) NewConfigProvider(ctx context.Context, args commontypes.RelayArgs) (configProvider commontypes.ConfigProvider, err error) { lggr := r.lggr.Named(args.ExternalJobID.String()).Named("ConfigProvider") relayOpts := types.NewRelayOpts(args) @@ -727,6 +727,9 @@ func (r *Relayer) NewConfigProvider(ctx context.Context, args commontypes.RelayA configProvider, err = newLLOConfigProvider(ctx, lggr, r.chain, &retirement.NullRetirementReportCache{}, relayOpts) case "ocr3-capability": configProvider, err = NewOCR3CapabilityConfigProvider(ctx, lggr, r.chain, relayOpts) + case "securemint": + // secure mint uses the OCR3 Configurator contract for onchain config, the LLO config provider works with that out of the box + configProvider, err = newLLOConfigProvider(ctx, lggr, r.chain, &retirement.NullRetirementReportCache{}, relayOpts) default: return nil, fmt.Errorf("unrecognized provider type: %q", args.ProviderType) } diff --git a/core/services/relay/evm/target_strategy.go b/core/services/relay/evm/target_strategy.go index 7a39cfc20da..392f2ac7a88 100644 --- a/core/services/relay/evm/target_strategy.go +++ b/core/services/relay/evm/target_strategy.go @@ -97,7 +97,7 @@ func (t *evmTargetStrategy) QueryTransmissionState(ctx context.Context, reportID binary.BigEndian.PutUint16(b, reportID) if !t.bound.Load() { - t.lggr.Debugw("Binding to forwarder address") + t.lggr.Debugw("Binding to forwarder address", "forwarder", t.forwarder) err = t.cr.Bind(ctx, []commontypes.BoundContract{t.binding}) if err != nil { return nil, err diff --git a/core/services/relay/relay.go b/core/services/relay/relay.go index 3fcc0ff0461..db20133d850 100644 --- a/core/services/relay/relay.go +++ b/core/services/relay/relay.go @@ -65,6 +65,8 @@ func (r *ServerAdapter) NewPluginProvider(ctx context.Context, rargs types.Relay return r.Relayer.NewPluginProvider(ctx, rargs, pargs) case types.LLO: return nil, fmt.Errorf("provider type not supported: %s", rargs.ProviderType) + case types.SecureMint: + return nil, fmt.Errorf("provider type not supported: %s", rargs.ProviderType) } return nil, fmt.Errorf("provider type not recognized: %s", rargs.ProviderType) } diff --git a/deployment/go.mod b/deployment/go.mod index ca4c1c5b077..2236ae2a6ae 100644 --- a/deployment/go.mod +++ b/deployment/go.mod @@ -37,12 +37,12 @@ require ( github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20250825135846-84f0d5167f8f github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250805210128-7f8a0f403c3a github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250805210128-7f8a0f403c3a - github.com/smartcontractkit/chainlink-common v0.9.5-0.20250908082700-aa3f5927af8c + github.com/smartcontractkit/chainlink-common v0.9.5-0.20250909120425-33154edc7a38 github.com/smartcontractkit/chainlink-deployments-framework v0.42.0 github.com/smartcontractkit/chainlink-evm v0.3.3-0.20250903140346-aacd485a7dea github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20250827130336-5922343458be github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250729142306-508e798f6a5d - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250829155125-f4655b0b4605 + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250905211734-167560f092c1 github.com/smartcontractkit/chainlink-protos/job-distributor v0.13.1 github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 github.com/smartcontractkit/chainlink-solana v1.1.2-0.20250905170534-87e867e6cf31 diff --git a/deployment/go.sum b/deployment/go.sum index 9080ceefb53..7178f13494c 100644 --- a/deployment/go.sum +++ b/deployment/go.sum @@ -1272,8 +1272,8 @@ github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250805210128-7 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250805210128-7f8a0f403c3a/go.mod h1:Ve1xD71bl193YIZQEoJMmBqLGQJdNs29bwbuObwvbhQ= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250805210128-7f8a0f403c3a h1:38dAlTPRUQHZus5dCnBnQyf/V4oYn0p2svWlbPgHDQ4= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250805210128-7f8a0f403c3a/go.mod h1:xtZNi6pOKdC3sLvokDvXOhgHzT+cyBqH/gWwvxTxqrg= -github.com/smartcontractkit/chainlink-common v0.9.5-0.20250908082700-aa3f5927af8c h1:2cnAGt0nedGS/M0deXRCIJVsxTWi6CzYPXkTxqVwViY= -github.com/smartcontractkit/chainlink-common v0.9.5-0.20250908082700-aa3f5927af8c/go.mod h1:b5KI42+P0ZmUXuvOFzSH9uIB8K83wvXq1GNVoY+ePeg= +github.com/smartcontractkit/chainlink-common v0.9.5-0.20250909120425-33154edc7a38 h1:dCvWsUaZsEkX6iH6CwsmtDORH6rHoAHQ9Vi0H/Zb444= +github.com/smartcontractkit/chainlink-common v0.9.5-0.20250909120425-33154edc7a38/go.mod h1:1diMLMwfIACeqJFt7ySGaBrJIeUwHTLhVVYlb41EyKk= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.1 h1:ca2z5OXgnbBPQRxpwXwBLJsUA1+cAp5ncfW4Ssvd6eY= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.1/go.mod h1:NZv/qKYGFRnkjOYBouajnDfFoZ+WDa6H2KNmSf1dnKc= github.com/smartcontractkit/chainlink-common/pkg/monitoring v0.0.0-20250415235644-8703639403c7 h1:9wh1G+WbXwPVqf0cfSRSgwIcaXTQgvYezylEAfwmrbw= @@ -1298,8 +1298,8 @@ github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250729142306- github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250729142306-508e798f6a5d/go.mod h1:2JTBNp3FlRdO/nHc4dsc9bfxxMClMO1Qt8sLJgtreBY= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20250722225531-876fd6b94976 h1:mF3FiDUoV0QbJcks9R2y7ydqntNL1Z0VCPBJgx/Ms+0= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20250722225531-876fd6b94976/go.mod h1:HHGeDUpAsPa0pmOx7wrByCitjQ0mbUxf0R9v+g67uCA= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250829155125-f4655b0b4605 h1:yVH5tLDzW2ZBUpmkHF5nci1SRSXTcU3A1VZ8iS5qudA= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250829155125-f4655b0b4605/go.mod h1:jUC52kZzEnWF9tddHh85zolKybmLpbQ1oNA4FjOHt1Q= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250905211734-167560f092c1 h1:HZt/80mhcNw6/MlYBIRracxfHWNqFF0iZ5nZEVZBUgo= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250905211734-167560f092c1/go.mod h1:jUC52kZzEnWF9tddHh85zolKybmLpbQ1oNA4FjOHt1Q= github.com/smartcontractkit/chainlink-protos/job-distributor v0.13.1 h1:PWwLGimBt37eDzpbfZ9V/ZkW4oCjcwKjKiAwKlSfPc0= github.com/smartcontractkit/chainlink-protos/job-distributor v0.13.1/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE= github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 h1:0eroOyBwmdoGUwUdvMI0/J7m5wuzNnJDMglSOK1sfNY= diff --git a/go.mod b/go.mod index 3fd9afa4c4f..1f74162ee09 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/smartcontractkit/chainlink/v2 go 1.24.5 +toolchain go1.24.6 + require ( github.com/Depado/ginprom v1.8.0 github.com/Masterminds/semver/v3 v3.4.0 @@ -84,7 +86,7 @@ require ( github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20250825135846-84f0d5167f8f github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250805210128-7f8a0f403c3a github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250804184440-c0506474fc44 - github.com/smartcontractkit/chainlink-common v0.9.5-0.20250908082700-aa3f5927af8c + github.com/smartcontractkit/chainlink-common v0.9.5-0.20250909155641-2e763fb6ffe6 // https://github.com/smartcontractkit/chainlink-common/pull/1529 github.com/smartcontractkit/chainlink-data-streams v0.1.2 github.com/smartcontractkit/chainlink-evm v0.3.3-0.20250903140346-aacd485a7dea github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20250827130336-5922343458be @@ -93,7 +95,7 @@ require ( github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20250717121125-2350c82883e2 github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250729142306-508e798f6a5d github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20250722225531-876fd6b94976 - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250829155125-f4655b0b4605 + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250905211734-167560f092c1 github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 github.com/smartcontractkit/chainlink-protos/storage-service v0.3.0 github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20250822025801-598d3d86f873 diff --git a/go.sum b/go.sum index 542429e1931..bcf2db8b504 100644 --- a/go.sum +++ b/go.sum @@ -1110,8 +1110,8 @@ github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250805210128-7 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250805210128-7f8a0f403c3a/go.mod h1:Ve1xD71bl193YIZQEoJMmBqLGQJdNs29bwbuObwvbhQ= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250804184440-c0506474fc44 h1:S00lus9RPu5JuxKRtGEET+aIUfASahHpTRV5RgPARSI= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250804184440-c0506474fc44/go.mod h1:xtZNi6pOKdC3sLvokDvXOhgHzT+cyBqH/gWwvxTxqrg= -github.com/smartcontractkit/chainlink-common v0.9.5-0.20250908082700-aa3f5927af8c h1:2cnAGt0nedGS/M0deXRCIJVsxTWi6CzYPXkTxqVwViY= -github.com/smartcontractkit/chainlink-common v0.9.5-0.20250908082700-aa3f5927af8c/go.mod h1:b5KI42+P0ZmUXuvOFzSH9uIB8K83wvXq1GNVoY+ePeg= +github.com/smartcontractkit/chainlink-common v0.9.5-0.20250909155641-2e763fb6ffe6 h1:GQQmRpuhLnE9OQNI8qpgde4+Ys7hfmaRHNjY2RAV1Jc= +github.com/smartcontractkit/chainlink-common v0.9.5-0.20250909155641-2e763fb6ffe6/go.mod h1:1diMLMwfIACeqJFt7ySGaBrJIeUwHTLhVVYlb41EyKk= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.1 h1:ca2z5OXgnbBPQRxpwXwBLJsUA1+cAp5ncfW4Ssvd6eY= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.1/go.mod h1:NZv/qKYGFRnkjOYBouajnDfFoZ+WDa6H2KNmSf1dnKc= github.com/smartcontractkit/chainlink-common/pkg/monitoring v0.0.0-20250415235644-8703639403c7 h1:9wh1G+WbXwPVqf0cfSRSgwIcaXTQgvYezylEAfwmrbw= @@ -1134,8 +1134,8 @@ github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250729142306- github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250729142306-508e798f6a5d/go.mod h1:2JTBNp3FlRdO/nHc4dsc9bfxxMClMO1Qt8sLJgtreBY= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20250722225531-876fd6b94976 h1:mF3FiDUoV0QbJcks9R2y7ydqntNL1Z0VCPBJgx/Ms+0= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20250722225531-876fd6b94976/go.mod h1:HHGeDUpAsPa0pmOx7wrByCitjQ0mbUxf0R9v+g67uCA= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250829155125-f4655b0b4605 h1:yVH5tLDzW2ZBUpmkHF5nci1SRSXTcU3A1VZ8iS5qudA= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250829155125-f4655b0b4605/go.mod h1:jUC52kZzEnWF9tddHh85zolKybmLpbQ1oNA4FjOHt1Q= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250905211734-167560f092c1 h1:HZt/80mhcNw6/MlYBIRracxfHWNqFF0iZ5nZEVZBUgo= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250905211734-167560f092c1/go.mod h1:jUC52kZzEnWF9tddHh85zolKybmLpbQ1oNA4FjOHt1Q= github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 h1:0eroOyBwmdoGUwUdvMI0/J7m5wuzNnJDMglSOK1sfNY= github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0/go.mod h1:m/A3lqD7ms/RsQ9BT5P2uceYY0QX5mIt4KQxT2G6qEo= github.com/smartcontractkit/chainlink-protos/rmn/v1.6/go v0.0.0-20250131130834-15e0d4cde2a6 h1:L6KJ4kGv/yNNoCk8affk7Y1vAY0qglPMXC/hevV/IsA= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 5eb68ac3304..18654ad9cb2 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -50,7 +50,7 @@ require ( github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20250825135846-84f0d5167f8f github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250805210128-7f8a0f403c3a github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250805210128-7f8a0f403c3a - github.com/smartcontractkit/chainlink-common v0.9.5-0.20250908082700-aa3f5927af8c + github.com/smartcontractkit/chainlink-common v0.9.5-0.20250909120425-33154edc7a38 github.com/smartcontractkit/chainlink-deployments-framework v0.42.0 github.com/smartcontractkit/chainlink-evm v0.3.3-0.20250903140346-aacd485a7dea github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20250827130336-5922343458be @@ -485,7 +485,7 @@ require ( github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20250717121125-2350c82883e2 // indirect github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250729142306-508e798f6a5d // indirect github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20250722225531-876fd6b94976 // indirect - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250829155125-f4655b0b4605 // indirect + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250905211734-167560f092c1 // indirect github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 // indirect github.com/smartcontractkit/chainlink-protos/rmn/v1.6/go v0.0.0-20250131130834-15e0d4cde2a6 // indirect github.com/smartcontractkit/chainlink-protos/storage-service v0.3.0 // indirect diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 7e79bd89388..ab53d790348 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -1524,8 +1524,8 @@ github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250805210128-7 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250805210128-7f8a0f403c3a/go.mod h1:Ve1xD71bl193YIZQEoJMmBqLGQJdNs29bwbuObwvbhQ= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250805210128-7f8a0f403c3a h1:38dAlTPRUQHZus5dCnBnQyf/V4oYn0p2svWlbPgHDQ4= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250805210128-7f8a0f403c3a/go.mod h1:xtZNi6pOKdC3sLvokDvXOhgHzT+cyBqH/gWwvxTxqrg= -github.com/smartcontractkit/chainlink-common v0.9.5-0.20250908082700-aa3f5927af8c h1:2cnAGt0nedGS/M0deXRCIJVsxTWi6CzYPXkTxqVwViY= -github.com/smartcontractkit/chainlink-common v0.9.5-0.20250908082700-aa3f5927af8c/go.mod h1:b5KI42+P0ZmUXuvOFzSH9uIB8K83wvXq1GNVoY+ePeg= +github.com/smartcontractkit/chainlink-common v0.9.5-0.20250909120425-33154edc7a38 h1:dCvWsUaZsEkX6iH6CwsmtDORH6rHoAHQ9Vi0H/Zb444= +github.com/smartcontractkit/chainlink-common v0.9.5-0.20250909120425-33154edc7a38/go.mod h1:1diMLMwfIACeqJFt7ySGaBrJIeUwHTLhVVYlb41EyKk= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.1 h1:ca2z5OXgnbBPQRxpwXwBLJsUA1+cAp5ncfW4Ssvd6eY= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.1/go.mod h1:NZv/qKYGFRnkjOYBouajnDfFoZ+WDa6H2KNmSf1dnKc= github.com/smartcontractkit/chainlink-common/pkg/monitoring v0.0.0-20250415235644-8703639403c7 h1:9wh1G+WbXwPVqf0cfSRSgwIcaXTQgvYezylEAfwmrbw= @@ -1550,8 +1550,8 @@ github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250729142306- github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250729142306-508e798f6a5d/go.mod h1:2JTBNp3FlRdO/nHc4dsc9bfxxMClMO1Qt8sLJgtreBY= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20250722225531-876fd6b94976 h1:mF3FiDUoV0QbJcks9R2y7ydqntNL1Z0VCPBJgx/Ms+0= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20250722225531-876fd6b94976/go.mod h1:HHGeDUpAsPa0pmOx7wrByCitjQ0mbUxf0R9v+g67uCA= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250829155125-f4655b0b4605 h1:yVH5tLDzW2ZBUpmkHF5nci1SRSXTcU3A1VZ8iS5qudA= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250829155125-f4655b0b4605/go.mod h1:jUC52kZzEnWF9tddHh85zolKybmLpbQ1oNA4FjOHt1Q= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250905211734-167560f092c1 h1:HZt/80mhcNw6/MlYBIRracxfHWNqFF0iZ5nZEVZBUgo= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250905211734-167560f092c1/go.mod h1:jUC52kZzEnWF9tddHh85zolKybmLpbQ1oNA4FjOHt1Q= github.com/smartcontractkit/chainlink-protos/job-distributor v0.13.1 h1:PWwLGimBt37eDzpbfZ9V/ZkW4oCjcwKjKiAwKlSfPc0= github.com/smartcontractkit/chainlink-protos/job-distributor v0.13.1/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE= github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 h1:0eroOyBwmdoGUwUdvMI0/J7m5wuzNnJDMglSOK1sfNY= diff --git a/integration-tests/load/go.mod b/integration-tests/load/go.mod index c8deba9269a..3220a20ba79 100644 --- a/integration-tests/load/go.mod +++ b/integration-tests/load/go.mod @@ -32,7 +32,7 @@ require ( github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20250825135846-84f0d5167f8f github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250805210128-7f8a0f403c3a github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250805210128-7f8a0f403c3a - github.com/smartcontractkit/chainlink-common v0.9.5-0.20250908082700-aa3f5927af8c + github.com/smartcontractkit/chainlink-common v0.9.5-0.20250909120425-33154edc7a38 github.com/smartcontractkit/chainlink-deployments-framework v0.42.0 github.com/smartcontractkit/chainlink-evm v0.3.3-0.20250903140346-aacd485a7dea github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20250827130336-5922343458be @@ -474,7 +474,7 @@ require ( github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20250717121125-2350c82883e2 // indirect github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250729142306-508e798f6a5d // indirect github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20250722225531-876fd6b94976 // indirect - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250829155125-f4655b0b4605 // indirect + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250905211734-167560f092c1 // indirect github.com/smartcontractkit/chainlink-protos/job-distributor v0.13.1 // indirect github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 // indirect github.com/smartcontractkit/chainlink-protos/rmn/v1.6/go v0.0.0-20250131130834-15e0d4cde2a6 // indirect diff --git a/integration-tests/load/go.sum b/integration-tests/load/go.sum index 2fda3b4c4cf..a3bcdc44e3e 100644 --- a/integration-tests/load/go.sum +++ b/integration-tests/load/go.sum @@ -1500,8 +1500,8 @@ github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250805210128-7 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250805210128-7f8a0f403c3a/go.mod h1:Ve1xD71bl193YIZQEoJMmBqLGQJdNs29bwbuObwvbhQ= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250805210128-7f8a0f403c3a h1:38dAlTPRUQHZus5dCnBnQyf/V4oYn0p2svWlbPgHDQ4= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250805210128-7f8a0f403c3a/go.mod h1:xtZNi6pOKdC3sLvokDvXOhgHzT+cyBqH/gWwvxTxqrg= -github.com/smartcontractkit/chainlink-common v0.9.5-0.20250908082700-aa3f5927af8c h1:2cnAGt0nedGS/M0deXRCIJVsxTWi6CzYPXkTxqVwViY= -github.com/smartcontractkit/chainlink-common v0.9.5-0.20250908082700-aa3f5927af8c/go.mod h1:b5KI42+P0ZmUXuvOFzSH9uIB8K83wvXq1GNVoY+ePeg= +github.com/smartcontractkit/chainlink-common v0.9.5-0.20250909120425-33154edc7a38 h1:dCvWsUaZsEkX6iH6CwsmtDORH6rHoAHQ9Vi0H/Zb444= +github.com/smartcontractkit/chainlink-common v0.9.5-0.20250909120425-33154edc7a38/go.mod h1:1diMLMwfIACeqJFt7ySGaBrJIeUwHTLhVVYlb41EyKk= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.1 h1:ca2z5OXgnbBPQRxpwXwBLJsUA1+cAp5ncfW4Ssvd6eY= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.1/go.mod h1:NZv/qKYGFRnkjOYBouajnDfFoZ+WDa6H2KNmSf1dnKc= github.com/smartcontractkit/chainlink-common/pkg/monitoring v0.0.0-20250415235644-8703639403c7 h1:9wh1G+WbXwPVqf0cfSRSgwIcaXTQgvYezylEAfwmrbw= @@ -1526,8 +1526,8 @@ github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250729142306- github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250729142306-508e798f6a5d/go.mod h1:2JTBNp3FlRdO/nHc4dsc9bfxxMClMO1Qt8sLJgtreBY= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20250722225531-876fd6b94976 h1:mF3FiDUoV0QbJcks9R2y7ydqntNL1Z0VCPBJgx/Ms+0= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20250722225531-876fd6b94976/go.mod h1:HHGeDUpAsPa0pmOx7wrByCitjQ0mbUxf0R9v+g67uCA= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250829155125-f4655b0b4605 h1:yVH5tLDzW2ZBUpmkHF5nci1SRSXTcU3A1VZ8iS5qudA= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250829155125-f4655b0b4605/go.mod h1:jUC52kZzEnWF9tddHh85zolKybmLpbQ1oNA4FjOHt1Q= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250905211734-167560f092c1 h1:HZt/80mhcNw6/MlYBIRracxfHWNqFF0iZ5nZEVZBUgo= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250905211734-167560f092c1/go.mod h1:jUC52kZzEnWF9tddHh85zolKybmLpbQ1oNA4FjOHt1Q= github.com/smartcontractkit/chainlink-protos/job-distributor v0.13.1 h1:PWwLGimBt37eDzpbfZ9V/ZkW4oCjcwKjKiAwKlSfPc0= github.com/smartcontractkit/chainlink-protos/job-distributor v0.13.1/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE= github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 h1:0eroOyBwmdoGUwUdvMI0/J7m5wuzNnJDMglSOK1sfNY= diff --git a/system-tests/lib/go.mod b/system-tests/lib/go.mod index e29edefca84..064c6a21f35 100644 --- a/system-tests/lib/go.mod +++ b/system-tests/lib/go.mod @@ -33,11 +33,11 @@ require ( github.com/rs/zerolog v1.33.0 github.com/scylladb/go-reflectx v1.0.1 github.com/smartcontractkit/chain-selectors v1.0.67 - github.com/smartcontractkit/chainlink-common v0.9.5-0.20250908082700-aa3f5927af8c + github.com/smartcontractkit/chainlink-common v0.9.5-0.20250909120425-33154edc7a38 github.com/smartcontractkit/chainlink-deployments-framework v0.42.0 github.com/smartcontractkit/chainlink-evm v0.3.3-0.20250903140346-aacd485a7dea github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20250827130336-5922343458be - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250829155125-f4655b0b4605 + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250905211734-167560f092c1 github.com/smartcontractkit/chainlink-protos/job-distributor v0.13.1 github.com/smartcontractkit/chainlink-solana v1.1.2-0.20250905170534-87e867e6cf31 github.com/smartcontractkit/chainlink-testing-framework/framework v0.10.17 diff --git a/system-tests/lib/go.sum b/system-tests/lib/go.sum index 0ae1bb96d8c..41073e33fbb 100644 --- a/system-tests/lib/go.sum +++ b/system-tests/lib/go.sum @@ -1516,8 +1516,8 @@ github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250805210128-7 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250805210128-7f8a0f403c3a/go.mod h1:Ve1xD71bl193YIZQEoJMmBqLGQJdNs29bwbuObwvbhQ= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250805210128-7f8a0f403c3a h1:38dAlTPRUQHZus5dCnBnQyf/V4oYn0p2svWlbPgHDQ4= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250805210128-7f8a0f403c3a/go.mod h1:xtZNi6pOKdC3sLvokDvXOhgHzT+cyBqH/gWwvxTxqrg= -github.com/smartcontractkit/chainlink-common v0.9.5-0.20250908082700-aa3f5927af8c h1:2cnAGt0nedGS/M0deXRCIJVsxTWi6CzYPXkTxqVwViY= -github.com/smartcontractkit/chainlink-common v0.9.5-0.20250908082700-aa3f5927af8c/go.mod h1:b5KI42+P0ZmUXuvOFzSH9uIB8K83wvXq1GNVoY+ePeg= +github.com/smartcontractkit/chainlink-common v0.9.5-0.20250909120425-33154edc7a38 h1:dCvWsUaZsEkX6iH6CwsmtDORH6rHoAHQ9Vi0H/Zb444= +github.com/smartcontractkit/chainlink-common v0.9.5-0.20250909120425-33154edc7a38/go.mod h1:1diMLMwfIACeqJFt7ySGaBrJIeUwHTLhVVYlb41EyKk= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.1 h1:ca2z5OXgnbBPQRxpwXwBLJsUA1+cAp5ncfW4Ssvd6eY= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.1/go.mod h1:NZv/qKYGFRnkjOYBouajnDfFoZ+WDa6H2KNmSf1dnKc= github.com/smartcontractkit/chainlink-common/pkg/monitoring v0.0.0-20250415235644-8703639403c7 h1:9wh1G+WbXwPVqf0cfSRSgwIcaXTQgvYezylEAfwmrbw= @@ -1542,8 +1542,8 @@ github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250729142306- github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250729142306-508e798f6a5d/go.mod h1:2JTBNp3FlRdO/nHc4dsc9bfxxMClMO1Qt8sLJgtreBY= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20250722225531-876fd6b94976 h1:mF3FiDUoV0QbJcks9R2y7ydqntNL1Z0VCPBJgx/Ms+0= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20250722225531-876fd6b94976/go.mod h1:HHGeDUpAsPa0pmOx7wrByCitjQ0mbUxf0R9v+g67uCA= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250829155125-f4655b0b4605 h1:yVH5tLDzW2ZBUpmkHF5nci1SRSXTcU3A1VZ8iS5qudA= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250829155125-f4655b0b4605/go.mod h1:jUC52kZzEnWF9tddHh85zolKybmLpbQ1oNA4FjOHt1Q= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250905211734-167560f092c1 h1:HZt/80mhcNw6/MlYBIRracxfHWNqFF0iZ5nZEVZBUgo= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250905211734-167560f092c1/go.mod h1:jUC52kZzEnWF9tddHh85zolKybmLpbQ1oNA4FjOHt1Q= github.com/smartcontractkit/chainlink-protos/job-distributor v0.13.1 h1:PWwLGimBt37eDzpbfZ9V/ZkW4oCjcwKjKiAwKlSfPc0= github.com/smartcontractkit/chainlink-protos/job-distributor v0.13.1/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE= github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 h1:0eroOyBwmdoGUwUdvMI0/J7m5wuzNnJDMglSOK1sfNY= diff --git a/system-tests/tests/go.mod b/system-tests/tests/go.mod index b9c31df16c7..936c649b69c 100644 --- a/system-tests/tests/go.mod +++ b/system-tests/tests/go.mod @@ -25,11 +25,11 @@ require ( github.com/prometheus/common v0.65.0 github.com/rs/zerolog v1.33.0 github.com/shopspring/decimal v1.4.0 - github.com/smartcontractkit/chainlink-common v0.9.5-0.20250908082700-aa3f5927af8c + github.com/smartcontractkit/chainlink-common v0.9.5-0.20250909120425-33154edc7a38 github.com/smartcontractkit/chainlink-data-streams v0.1.2 github.com/smartcontractkit/chainlink-deployments-framework v0.42.0 github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20250827130336-5922343458be - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250829155125-f4655b0b4605 + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250905211734-167560f092c1 github.com/smartcontractkit/chainlink-protos/job-distributor v0.13.1 github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20250822025801-598d3d86f873 github.com/smartcontractkit/chainlink-testing-framework/framework v0.10.17 diff --git a/system-tests/tests/go.sum b/system-tests/tests/go.sum index ad9b0aa4317..645e4b4fe7a 100644 --- a/system-tests/tests/go.sum +++ b/system-tests/tests/go.sum @@ -1719,8 +1719,8 @@ github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250805210128-7 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250805210128-7f8a0f403c3a/go.mod h1:Ve1xD71bl193YIZQEoJMmBqLGQJdNs29bwbuObwvbhQ= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250805210128-7f8a0f403c3a h1:38dAlTPRUQHZus5dCnBnQyf/V4oYn0p2svWlbPgHDQ4= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250805210128-7f8a0f403c3a/go.mod h1:xtZNi6pOKdC3sLvokDvXOhgHzT+cyBqH/gWwvxTxqrg= -github.com/smartcontractkit/chainlink-common v0.9.5-0.20250908082700-aa3f5927af8c h1:2cnAGt0nedGS/M0deXRCIJVsxTWi6CzYPXkTxqVwViY= -github.com/smartcontractkit/chainlink-common v0.9.5-0.20250908082700-aa3f5927af8c/go.mod h1:b5KI42+P0ZmUXuvOFzSH9uIB8K83wvXq1GNVoY+ePeg= +github.com/smartcontractkit/chainlink-common v0.9.5-0.20250909120425-33154edc7a38 h1:dCvWsUaZsEkX6iH6CwsmtDORH6rHoAHQ9Vi0H/Zb444= +github.com/smartcontractkit/chainlink-common v0.9.5-0.20250909120425-33154edc7a38/go.mod h1:1diMLMwfIACeqJFt7ySGaBrJIeUwHTLhVVYlb41EyKk= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.1 h1:ca2z5OXgnbBPQRxpwXwBLJsUA1+cAp5ncfW4Ssvd6eY= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.1/go.mod h1:NZv/qKYGFRnkjOYBouajnDfFoZ+WDa6H2KNmSf1dnKc= github.com/smartcontractkit/chainlink-common/pkg/monitoring v0.0.0-20250415235644-8703639403c7 h1:9wh1G+WbXwPVqf0cfSRSgwIcaXTQgvYezylEAfwmrbw= @@ -1745,8 +1745,8 @@ github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250729142306- github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250729142306-508e798f6a5d/go.mod h1:2JTBNp3FlRdO/nHc4dsc9bfxxMClMO1Qt8sLJgtreBY= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20250722225531-876fd6b94976 h1:mF3FiDUoV0QbJcks9R2y7ydqntNL1Z0VCPBJgx/Ms+0= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20250722225531-876fd6b94976/go.mod h1:HHGeDUpAsPa0pmOx7wrByCitjQ0mbUxf0R9v+g67uCA= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250829155125-f4655b0b4605 h1:yVH5tLDzW2ZBUpmkHF5nci1SRSXTcU3A1VZ8iS5qudA= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250829155125-f4655b0b4605/go.mod h1:jUC52kZzEnWF9tddHh85zolKybmLpbQ1oNA4FjOHt1Q= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250905211734-167560f092c1 h1:HZt/80mhcNw6/MlYBIRracxfHWNqFF0iZ5nZEVZBUgo= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250905211734-167560f092c1/go.mod h1:jUC52kZzEnWF9tddHh85zolKybmLpbQ1oNA4FjOHt1Q= github.com/smartcontractkit/chainlink-protos/job-distributor v0.13.1 h1:PWwLGimBt37eDzpbfZ9V/ZkW4oCjcwKjKiAwKlSfPc0= github.com/smartcontractkit/chainlink-protos/job-distributor v0.13.1/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE= github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 h1:0eroOyBwmdoGUwUdvMI0/J7m5wuzNnJDMglSOK1sfNY=