From 3d700a8ca6146a8331c05a8589dda49d39eab972 Mon Sep 17 00:00:00 2001 From: Vyzaldy Sanchez Date: Mon, 3 Jun 2024 09:14:31 -0400 Subject: [PATCH] Implement multi-chain OCR3 Keybundle (#13108) * Adds `onchainSigningStrategy` job spec field support * Makes changeset internal * Fixes typo and remove unnecessary logs * Stores `onchain_signing_strategy` values in the DB * Fixes test error * Implements OCR3 multi-chain keybundle adapter WIP * Implements OCR3 multi-chain keybundle adapter WIP * Fixes merge conflicts * Fixes linter * Improves OCR3 key bundle adapter * Fixes import cycle * Fixes import cycle * Fixes lint * Fixes lint * Adds tests for new adapter * Updates go.mod * Fixes build * Fixes lint * Uses proper `proto` marshall/unmarshall * Uses correct proto package * Fixes gomod * Improves implementation * Fixes validation on optional setup --- .changeset/olive-knives-happen.md | 5 ++ core/services/job/models_test.go | 1 + core/services/ocr2/delegate.go | 44 ++++++++-- core/services/ocr2/validate/validate.go | 49 ++++++++--- core/services/ocr2/validate/validate_test.go | 33 +++++++ core/services/ocrcommon/adapters.go | 91 ++++++++++++++++++++ core/services/ocrcommon/adapters_test.go | 71 ++++++++++++++- 7 files changed, 271 insertions(+), 23 deletions(-) create mode 100644 .changeset/olive-knives-happen.md diff --git a/.changeset/olive-knives-happen.md b/.changeset/olive-knives-happen.md new file mode 100644 index 00000000000..7f522c96ff1 --- /dev/null +++ b/.changeset/olive-knives-happen.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +#internal Generic Plugin `onchainSigningStrategy` support diff --git a/core/services/job/models_test.go b/core/services/job/models_test.go index c177b3b81e1..04bbbdee0f0 100644 --- a/core/services/job/models_test.go +++ b/core/services/job/models_test.go @@ -12,6 +12,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-common/pkg/types" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" "github.com/smartcontractkit/chainlink/v2/core/store/models" ) diff --git a/core/services/ocr2/delegate.go b/core/services/ocr2/delegate.go index 350cbc8d593..1c70195dd43 100644 --- a/core/services/ocr2/delegate.go +++ b/core/services/ocr2/delegate.go @@ -26,21 +26,25 @@ import ( ocr2keepers20runner "github.com/smartcontractkit/chainlink-automation/pkg/v2/runner" ocr2keepers21config "github.com/smartcontractkit/chainlink-automation/pkg/v3/config" ocr2keepers21 "github.com/smartcontractkit/chainlink-automation/pkg/v3/plugin" + "github.com/smartcontractkit/chainlink-common/pkg/loop/reportingplugins/ocr3" + + "github.com/smartcontractkit/chainlink/v2/core/config/env" + + "github.com/smartcontractkit/chainlink-vrf/altbn_128" + dkgpkg "github.com/smartcontractkit/chainlink-vrf/dkg" + "github.com/smartcontractkit/chainlink-vrf/ocr2vrf" + "github.com/smartcontractkit/chainlink-common/pkg/loop" "github.com/smartcontractkit/chainlink-common/pkg/loop/reportingplugins" - "github.com/smartcontractkit/chainlink-common/pkg/loop/reportingplugins/ocr3" "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-common/pkg/types/core" llotypes "github.com/smartcontractkit/chainlink-common/pkg/types/llo" "github.com/smartcontractkit/chainlink-common/pkg/utils/mailbox" - "github.com/smartcontractkit/chainlink-vrf/altbn_128" - dkgpkg "github.com/smartcontractkit/chainlink-vrf/dkg" - "github.com/smartcontractkit/chainlink-vrf/ocr2vrf" + "github.com/smartcontractkit/chainlink/v2/core/bridges" "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm" coreconfig "github.com/smartcontractkit/chainlink/v2/core/config" - "github.com/smartcontractkit/chainlink/v2/core/config/env" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/job" "github.com/smartcontractkit/chainlink/v2/core/services/keystore" @@ -683,9 +687,9 @@ func (d *Delegate) newServicesGenericPlugin( } oracleArgs.ReportingPluginFactory = plugin srvs = append(srvs, plugin) - oracle, err := libocr2.NewOracle(oracleArgs) - if err != nil { - return nil, err + oracle, oracleErr := libocr2.NewOracle(oracleArgs) + if oracleErr != nil { + return nil, oracleErr } srvs = append(srvs, job.NewServiceAdapter(oracle)) @@ -714,6 +718,28 @@ func (d *Delegate) newServicesGenericPlugin( if ocr3Provider, ok := provider.(types.OCR3ContractTransmitter); ok { contractTransmitter = ocr3Provider.OCR3ContractTransmitter() } + var onchainKeyringAdapter ocr3types.OnchainKeyring[[]byte] + if onchainSigningStrategy.IsMultiChain() { + // We are extracting the config beforehand + keyBundles := map[string]ocr2key.KeyBundle{} + for name := range onchainSigningStrategy.ConfigCopy() { + kbID, ostErr := onchainSigningStrategy.KeyBundleID(name) + if ostErr != nil { + return nil, ostErr + } + os, ostErr := d.ks.Get(kbID) + if ostErr != nil { + return nil, ostErr + } + keyBundles[name] = os + } + onchainKeyringAdapter, err = ocrcommon.NewOCR3OnchainKeyringMultiChainAdapter(keyBundles, lggr) + if err != nil { + return nil, err + } + } else { + onchainKeyringAdapter = ocrcommon.NewOCR3OnchainKeyringAdapter(kb) + } oracleArgs := libocr2.OCR3OracleArgs[[]byte]{ BinaryNetworkEndpointFactory: d.peerWrapper.Peer2, V2Bootstrappers: bootstrapPeers, @@ -725,7 +751,7 @@ func (d *Delegate) newServicesGenericPlugin( MonitoringEndpoint: oracleEndpoint, OffchainConfigDigester: provider.OffchainConfigDigester(), OffchainKeyring: kb, - OnchainKeyring: ocrcommon.NewOCR3OnchainKeyringAdapter(kb), + OnchainKeyring: onchainKeyringAdapter, MetricsRegisterer: prometheus.WrapRegistererWith(map[string]string{"job_name": jb.Name.ValueOrZero()}, prometheus.DefaultRegisterer), } oracleArgs.ReportingPluginFactory = plugin diff --git a/core/services/ocr2/validate/validate.go b/core/services/ocr2/validate/validate.go index ca28f73c0de..00ddfec000d 100644 --- a/core/services/ocr2/validate/validate.go +++ b/core/services/ocr2/validate/validate.go @@ -200,11 +200,31 @@ func (o *OCR2OnchainSigningStrategy) PublicKey() (string, error) { if !ok { return "", nil } - name, ok := pk.(string) + pkString, ok := pk.(string) if !ok { return "", fmt.Errorf("expected string publicKey value, but got: %T", pk) } - return name, nil + return pkString, nil +} + +func (o *OCR2OnchainSigningStrategy) ConfigCopy() job.JSONConfig { + copiedConfig := make(job.JSONConfig) + for k, v := range o.Config { + copiedConfig[k] = v + } + return copiedConfig +} + +func (o *OCR2OnchainSigningStrategy) KeyBundleID(name string) (string, error) { + kbID, ok := o.Config[name] + if !ok { + return "", nil + } + kbIDString, ok := kbID.(string) + if !ok { + return "", fmt.Errorf("expected string %s value, but got: %T", name, kbID) + } + return kbIDString, nil } func validateGenericPluginSpec(ctx context.Context, spec *job.OCR2OracleSpec, rc plugins.RegistrarConfig) error { @@ -222,17 +242,20 @@ func validateGenericPluginSpec(ctx context.Context, spec *job.OCR2OracleSpec, rc return errors.New("generic config invalid: only OCR version 2 and 3 are supported") } - onchainSigningStrategy := OCR2OnchainSigningStrategy{} - err = json.Unmarshal(spec.OnchainSigningStrategy.Bytes(), &onchainSigningStrategy) - if err != nil { - return err - } - pk, err := onchainSigningStrategy.PublicKey() - if err != nil { - return err - } - if pk == "" { - return errors.New("generic config invalid: must provide public key for the onchain signing strategy") + // OnchainSigningStrategy is optional + if spec.OnchainSigningStrategy != nil && len(spec.OnchainSigningStrategy.Bytes()) > 0 { + onchainSigningStrategy := OCR2OnchainSigningStrategy{} + err = json.Unmarshal(spec.OnchainSigningStrategy.Bytes(), &onchainSigningStrategy) + if err != nil { + return err + } + pk, ossErr := onchainSigningStrategy.PublicKey() + if ossErr != nil { + return ossErr + } + if pk == "" { + return errors.New("generic config invalid: must provide public key for the onchain signing strategy") + } } plugEnv := env.NewPlugin(p.PluginName) diff --git a/core/services/ocr2/validate/validate_test.go b/core/services/ocr2/validate/validate_test.go index da896bf4a92..05a10caeaf5 100644 --- a/core/services/ocr2/validate/validate_test.go +++ b/core/services/ocr2/validate/validate_test.go @@ -958,3 +958,36 @@ spec = "a spec" assert.Equal(t, "median", pc.PluginName) assert.Equal(t, "median", pc.TelemetryType) } + +type envelope2 struct { + OnchainSigningStrategy *validate.OCR2OnchainSigningStrategy +} + +func TestOCR2OnchainSigningStrategy_Unmarshal(t *testing.T) { + payload := ` +[onchainSigningStrategy] +strategyName = "single-chain" +[onchainSigningStrategy.config] +evm = "08d14c6eed757414d72055d28de6caf06535806c6a14e450f3a2f1c854420e17" +publicKey = "0x1234567890123456789012345678901234567890" +` + oss := &envelope2{} + tree, err := toml.Load(payload) + require.NoError(t, err) + o := map[string]any{} + err = tree.Unmarshal(&o) + require.NoError(t, err) + b, err := json.Marshal(o) + require.NoError(t, err) + err = json.Unmarshal(b, oss) + require.NoError(t, err) + + pk, err := oss.OnchainSigningStrategy.PublicKey() + require.NoError(t, err) + kbID, err := oss.OnchainSigningStrategy.KeyBundleID("evm") + require.NoError(t, err) + + assert.False(t, oss.OnchainSigningStrategy.IsMultiChain()) + assert.Equal(t, "0x1234567890123456789012345678901234567890", pk) + assert.Equal(t, "08d14c6eed757414d72055d28de6caf06535806c6a14e450f3a2f1c854420e17", kbID) +} diff --git a/core/services/ocrcommon/adapters.go b/core/services/ocrcommon/adapters.go index 1eee437eb6b..372d9e37f15 100644 --- a/core/services/ocrcommon/adapters.go +++ b/core/services/ocrcommon/adapters.go @@ -2,9 +2,16 @@ package ocrcommon import ( "context" + "fmt" + "github.com/pkg/errors" "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ocr2key" ) var _ ocr3types.OnchainKeyring[[]byte] = (*OCR3OnchainKeyringAdapter)(nil) @@ -71,3 +78,87 @@ func (c *OCR3ContractTransmitterAdapter) Transmit(ctx context.Context, digest oc func (c *OCR3ContractTransmitterAdapter) FromAccount() (ocrtypes.Account, error) { return c.ct.FromAccount() } + +var _ ocr3types.OnchainKeyring[[]byte] = (*OCR3OnchainKeyringMultiChainAdapter)(nil) + +type OCR3OnchainKeyringMultiChainAdapter struct { + keyBundles map[string]ocr2key.KeyBundle + publicKey ocrtypes.OnchainPublicKey + lggr logger.Logger +} + +func NewOCR3OnchainKeyringMultiChainAdapter(ost map[string]ocr2key.KeyBundle, lggr logger.Logger) (*OCR3OnchainKeyringMultiChainAdapter, error) { + if len(ost) == 0 { + return nil, errors.New("no key bundles provided") + } + // We don't need to check for the existence of `publicKey` in the keyBundles map because it is required on validation on `validate/validate.go` + return &OCR3OnchainKeyringMultiChainAdapter{ost, ost["publicKey"].PublicKey(), lggr}, nil +} + +func (a *OCR3OnchainKeyringMultiChainAdapter) PublicKey() ocrtypes.OnchainPublicKey { + return a.publicKey +} + +func (a *OCR3OnchainKeyringMultiChainAdapter) getKeyBundleFromInfo(info []byte) (ocr2key.KeyBundle, error) { + unmarshalledInfo := new(structpb.Struct) + err := proto.Unmarshal(info, unmarshalledInfo) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal report info: %v", err) + } + infoMap := unmarshalledInfo.AsMap() + keyBundleName, ok := infoMap["keyBundleName"] + if !ok { + return nil, errors.New("keyBundleName not found in report info") + } + name, ok := keyBundleName.(string) + if !ok { + return nil, errors.New("keyBundleName is not a string") + } + kb, ok := a.keyBundles[name] + if !ok { + return nil, fmt.Errorf("keyBundle not found: %s", name) + } + return kb, nil +} + +func (a *OCR3OnchainKeyringMultiChainAdapter) Sign(digest ocrtypes.ConfigDigest, seqNr uint64, r ocr3types.ReportWithInfo[[]byte]) (signature []byte, err error) { + kb, err := a.getKeyBundleFromInfo(r.Info) + if err != nil { + return nil, fmt.Errorf("sign: failed to get key bundle from report info: %v", err) + } + return kb.Sign(ocrtypes.ReportContext{ + ReportTimestamp: ocrtypes.ReportTimestamp{ + ConfigDigest: digest, + Epoch: uint32(seqNr), + Round: 0, + }, + ExtraHash: [32]byte(make([]byte, 32)), + }, r.Report) +} + +func (a *OCR3OnchainKeyringMultiChainAdapter) Verify(opk ocrtypes.OnchainPublicKey, digest ocrtypes.ConfigDigest, seqNr uint64, ri ocr3types.ReportWithInfo[[]byte], signature []byte) bool { + kb, err := a.getKeyBundleFromInfo(ri.Info) + if err != nil { + a.lggr.Warnf("verify: failed to get key bundle from report info: %v", err) + return false + } + return kb.Verify(opk, ocrtypes.ReportContext{ + ReportTimestamp: ocrtypes.ReportTimestamp{ + ConfigDigest: digest, + Epoch: uint32(seqNr), + Round: 0, + }, + ExtraHash: [32]byte(make([]byte, 32)), + }, ri.Report, signature) +} + +func (a *OCR3OnchainKeyringMultiChainAdapter) MaxSignatureLength() int { + maxLength := -1 + for _, kb := range a.keyBundles { + l := kb.MaxSignatureLength() + if l > maxLength { + maxLength = l + } + } + return maxLength +} diff --git a/core/services/ocrcommon/adapters_test.go b/core/services/ocrcommon/adapters_test.go index 669e015e7bc..fed854b0b32 100644 --- a/core/services/ocrcommon/adapters_test.go +++ b/core/services/ocrcommon/adapters_test.go @@ -2,15 +2,25 @@ package ocrcommon_test import ( "context" + "encoding/json" "fmt" "reflect" "testing" + "github.com/pelletier/go-toml" "github.com/smartcontractkit/libocr/offchainreporting2/types" "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/keystest" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ocr2key" + keystoreMocks "github.com/smartcontractkit/chainlink/v2/core/services/keystore/mocks" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/validate" "github.com/smartcontractkit/chainlink/v2/core/services/ocrcommon" ) @@ -105,6 +115,65 @@ func TestOCR3OnchainKeyringAdapter(t *testing.T) { require.Equal(t, maxSignatureLength, kr.MaxSignatureLength()) } +type envelope struct { + OnchainSigningStrategy *validate.OCR2OnchainSigningStrategy +} + +func TestNewOCR3OnchainKeyringMultiChainAdapter(t *testing.T) { + payload := ` +[onchainSigningStrategy] +strategyName = "single-chain" +[onchainSigningStrategy.config] +evm = "08d14c6eed757414d72055d28de6caf06535806c6a14e450f3a2f1c854420e17" +publicKey = "pub-key" +` + oss := &envelope{} + tree, err := toml.Load(payload) + require.NoError(t, err) + o := map[string]any{} + err = tree.Unmarshal(&o) + require.NoError(t, err) + b, err := json.Marshal(o) + require.NoError(t, err) + err = json.Unmarshal(b, oss) + require.NoError(t, err) + reportInfo := ocr3types.ReportWithInfo[[]byte]{ + Report: []byte("multi-chain-report"), + } + info, err := structpb.NewStruct(map[string]interface{}{ + "keyBundleName": "evm", + }) + require.NoError(t, err) + infoB, err := proto.Marshal(info) + require.NoError(t, err) + reportInfo.Info = infoB + + ks := keystoreMocks.NewOCR2(t) + fakeKey := ocr2key.MustNewInsecure(keystest.NewRandReaderFromSeed(1), "evm") + pk := fakeKey.PublicKey() + ks.On("Get", "pub-key").Return(fakeKey, nil) + ks.On("Get", "08d14c6eed757414d72055d28de6caf06535806c6a14e450f3a2f1c854420e17").Return(fakeKey, nil) + keyBundles := map[string]ocr2key.KeyBundle{} + for name := range oss.OnchainSigningStrategy.ConfigCopy() { + kbID, ostErr := oss.OnchainSigningStrategy.KeyBundleID(name) + require.NoError(t, ostErr) + os, ostErr := ks.Get(kbID) + require.NoError(t, ostErr) + keyBundles[name] = os + } + + adapter, err := ocrcommon.NewOCR3OnchainKeyringMultiChainAdapter(keyBundles, logger.TestLogger(t)) + require.NoError(t, err) + _, err = ocrcommon.NewOCR3OnchainKeyringMultiChainAdapter(map[string]ocr2key.KeyBundle{}, logger.TestLogger(t)) + require.Error(t, err, "no key bundles provided") + + sig, err := adapter.Sign(configDigest, seqNr, reportInfo) + assert.NoError(t, err) + assert.True(t, adapter.Verify(pk, configDigest, seqNr, reportInfo, sig)) + assert.Equal(t, pk, adapter.PublicKey()) + assert.Equal(t, fakeKey.MaxSignatureLength(), adapter.MaxSignatureLength()) +} + var _ ocrtypes.ContractTransmitter = (*fakeContractTransmitter)(nil) type fakeContractTransmitter struct {