Skip to content

Commit

Permalink
Implement multi-chain OCR3 Keybundle (#13108)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
vyzaldysanchez committed Jun 3, 2024
1 parent 7a86103 commit 3d700a8
Show file tree
Hide file tree
Showing 7 changed files with 271 additions and 23 deletions.
5 changes: 5 additions & 0 deletions .changeset/olive-knives-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink": patch
---

#internal Generic Plugin `onchainSigningStrategy` support
1 change: 1 addition & 0 deletions core/services/job/models_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
44 changes: 35 additions & 9 deletions core/services/ocr2/delegate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
49 changes: 36 additions & 13 deletions core/services/ocr2/validate/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
33 changes: 33 additions & 0 deletions core/services/ocr2/validate/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
91 changes: 91 additions & 0 deletions core/services/ocrcommon/adapters.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
71 changes: 70 additions & 1 deletion core/services/ocrcommon/adapters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 3d700a8

Please sign in to comment.