Skip to content

Commit

Permalink
Refactor secondary transmission (#15358)
Browse files Browse the repository at this point in the history
* Refactor secondary transmission

* Add dual transmission ABI

* Update dual transmission ABI

* Update ABI
Add forwarder for secondary transmission

* Refactor dual transmitter
Add tests

* Add missing test file

* Add missing file

* Fix lint

* Fix lint
Fix test

* Pass txManagerOCR2 to ocr2FeedsDualTransmission

* Add dualTransmission meta validation

* Implement feedback

* Add ContractTransmitter helper function

* Add debug logging

* Add hint and refund validation

* rename file typo
  • Loading branch information
george-dorin authored Jan 13, 2025
1 parent 98adf6d commit 1251ee3
Show file tree
Hide file tree
Showing 11 changed files with 743 additions and 87 deletions.
93 changes: 92 additions & 1 deletion core/services/job/job_orm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2119,7 +2119,8 @@ func TestORM_CreateJob_OCR2_With_DualTransmission(t *testing.T) {
require.ErrorContains(t, jobORM.CreateJob(ctx, &jb), "invalid transmitter address in dual transmission config")

dtTransmitterAddress := cltest.MustGenerateRandomKey(t)
completeDualTransmissionSpec := fmt.Sprintf(`

metaNotSliceDualTransmissionSpec := fmt.Sprintf(`
enableDualTransmission=true
[relayConfig.dualTransmission]
contractAddress = '0x613a38AC1659769640aaE063C651F48E0250454C'
Expand All @@ -2130,6 +2131,96 @@ func TestORM_CreateJob_OCR2_With_DualTransmission(t *testing.T) {
`,
dtTransmitterAddress.Address.String())

jb, err = ocr2validate.ValidatedOracleSpecToml(testutils.Context(t), config.OCR2(), config.Insecure(), baseJobSpec+metaNotSliceDualTransmissionSpec, nil)
require.NoError(t, err)
require.ErrorContains(t, jobORM.CreateJob(ctx, &jb), "dual transmission meta value key1 is not a slice")

hintNotValidDualTransmissionSpec := fmt.Sprintf(`
enableDualTransmission=true
[relayConfig.dualTransmission]
contractAddress = '0x613a38AC1659769640aaE063C651F48E0250454C'
transmitterAddress = '%s'
[relayConfig.dualTransmission.meta]
hint = ['some-invalid-hint']
key2 = ['val2','val3']
`,
dtTransmitterAddress.Address.String())

jb, err = ocr2validate.ValidatedOracleSpecToml(testutils.Context(t), config.OCR2(), config.Insecure(), baseJobSpec+hintNotValidDualTransmissionSpec, nil)
require.NoError(t, err)
require.ErrorContains(t, jobORM.CreateJob(ctx, &jb), "dual transmission meta.hint value some-invalid-hint should be one of the following [contract_address function_selector logs calldata default_logs]")

invalidRefundFormatDualTransmissionSpec := fmt.Sprintf(`
enableDualTransmission=true
[relayConfig.dualTransmission]
contractAddress = '0x613a38AC1659769640aaE063C651F48E0250454C'
transmitterAddress = '%s'
[relayConfig.dualTransmission.meta]
hint = ['calldata','logs']
refund = ['0x00']
`,
dtTransmitterAddress.Address.String())

jb, err = ocr2validate.ValidatedOracleSpecToml(testutils.Context(t), config.OCR2(), config.Insecure(), baseJobSpec+invalidRefundFormatDualTransmissionSpec, nil)
require.NoError(t, err)
require.ErrorContains(t, jobORM.CreateJob(ctx, &jb), "invalid dual transmission refund, format should be <ADDRESS>:<PERCENT>")

invalidRefundAddressFormatDualTransmissionSpec := fmt.Sprintf(`
enableDualTransmission=true
[relayConfig.dualTransmission]
contractAddress = '0x613a38AC1659769640aaE063C651F48E0250454C'
transmitterAddress = '%s'
[relayConfig.dualTransmission.meta]
hint = ['calldata','logs']
refund = ['0x000:50']
`,
dtTransmitterAddress.Address.String())

jb, err = ocr2validate.ValidatedOracleSpecToml(testutils.Context(t), config.OCR2(), config.Insecure(), baseJobSpec+invalidRefundAddressFormatDualTransmissionSpec, nil)
require.NoError(t, err)
require.ErrorContains(t, jobORM.CreateJob(ctx, &jb), "invalid dual transmission refund address, 0x000 is not a valid address")

invalidRefundPercentFormatDualTransmissionSpec := fmt.Sprintf(`
enableDualTransmission=true
[relayConfig.dualTransmission]
contractAddress = '0x613a38AC1659769640aaE063C651F48E0250454C'
transmitterAddress = '%s'
[relayConfig.dualTransmission.meta]
hint = ['calldata','logs']
refund = ['0x0000000000000000000000000000000000000000:A']
`,
dtTransmitterAddress.Address.String())

jb, err = ocr2validate.ValidatedOracleSpecToml(testutils.Context(t), config.OCR2(), config.Insecure(), baseJobSpec+invalidRefundPercentFormatDualTransmissionSpec, nil)
require.NoError(t, err)
require.ErrorContains(t, jobORM.CreateJob(ctx, &jb), "invalid dual transmission refund percent, A is not a number")

invalidRefundPercentTotalFormatDualTransmissionSpec := fmt.Sprintf(`
enableDualTransmission=true
[relayConfig.dualTransmission]
contractAddress = '0x613a38AC1659769640aaE063C651F48E0250454C'
transmitterAddress = '%s'
[relayConfig.dualTransmission.meta]
hint = ['calldata','logs']
refund = ['0x0000000000000000000000000000000000000000:50','0x0000000000000000000000000000000000000001:50']
`,
dtTransmitterAddress.Address.String())

jb, err = ocr2validate.ValidatedOracleSpecToml(testutils.Context(t), config.OCR2(), config.Insecure(), baseJobSpec+invalidRefundPercentTotalFormatDualTransmissionSpec, nil)
require.NoError(t, err)
require.ErrorContains(t, jobORM.CreateJob(ctx, &jb), "invalid dual transmission refund percentages, total sum of percentages must be less than 100")

completeDualTransmissionSpec := fmt.Sprintf(`
enableDualTransmission=true
[relayConfig.dualTransmission]
contractAddress = '0x613a38AC1659769640aaE063C651F48E0250454C'
transmitterAddress = '%s'
[relayConfig.dualTransmission.meta]
key1 = ['val1']
key2 = ['val2','val3']
`,
dtTransmitterAddress.Address.String())

jb, err = ocr2validate.ValidatedOracleSpecToml(testutils.Context(t), config.OCR2(), config.Insecure(), baseJobSpec+completeDualTransmissionSpec, nil)
require.NoError(t, err)

Expand Down
76 changes: 76 additions & 0 deletions core/services/job/orm.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"fmt"
"reflect"
"slices"
"strconv"
"strings"
"time"

"github.com/ethereum/go-ethereum/common"
Expand Down Expand Up @@ -326,9 +328,19 @@ func (o *orm) CreateJob(ctx context.Context, jb *Job) error {
return errors.New("invalid transmitter address in dual transmission config")
}

rawMeta, ok := dualTransmissionConfig["meta"].(map[string]interface{})
if !ok {
return errors.New("invalid dual transmission meta")
}

if err = validateDualTransmissionMeta(rawMeta); err != nil {
return err
}

if err = validateKeyStoreMatchForRelay(ctx, jb.OCR2OracleSpec.Relay, tx.keyStore, dtTransmitterAddress); err != nil {
return errors.Wrap(err, "unknown dual transmission transmitterAddress")
}

}

specID, err := tx.insertOCR2OracleSpec(ctx, jb.OCR2OracleSpec)
Expand Down Expand Up @@ -1669,3 +1681,67 @@ func (r legacyGasStationServerSpecRow) toLegacyGasStationServerSpec() *LegacyGas
func (o *orm) loadJobSpecErrors(ctx context.Context, jb *Job) error {
return errors.Wrapf(o.ds.SelectContext(ctx, &jb.JobSpecErrors, `SELECT * FROM job_spec_errors WHERE job_id = $1`, jb.ID), "failed to load job spec errors for job %d", jb.ID)
}

func validateDualTransmissionHint(vals []interface{}) error {
accepted := []string{"contract_address", "function_selector", "logs", "calldata", "default_logs"}
for _, v := range vals {
valString, ok := v.(string)
if !ok {
return errors.Errorf("dual transmission meta value %v is not a string", v)
}
if !slices.Contains(accepted, valString) {
return errors.Errorf("dual transmission meta.hint value %s should be one of the following %s", valString, accepted)
}
}
return nil
}

func validateDualTransmissionRefund(vals []interface{}) error {
totalRefund := 0
for _, v := range vals {
valString, ok := v.(string)
if !ok {
return errors.Errorf("dual transmission meta value %v is not a string", v)
}

s := strings.Split(valString, ":")
if len(s) != 2 {
return errors.New("invalid dual transmission refund, format should be <ADDRESS>:<PERCENT>")
}
if !common.IsHexAddress(s[0]) {
return errors.Errorf("invalid dual transmission refund address, %s is not a valid address", s[0])
}
percent, err := strconv.Atoi(s[1])
if err != nil {
return errors.Errorf("invalid dual transmission refund percent, %s is not a number", s[1])
}
totalRefund += percent
}

if totalRefund >= 100 {
return errors.New("invalid dual transmission refund percentages, total sum of percentages must be less than 100")
}
return nil
}

func validateDualTransmissionMeta(meta map[string]interface{}) error {
for k, v := range meta {
metaFieldValues, ok := v.([]interface{})
if !ok {
return errors.Errorf("dual transmission meta value %s is not a slice", k)
}
if k == "hint" {
if err := validateDualTransmissionHint(metaFieldValues); err != nil {
return err
}
}

if k == "refund" {
if err := validateDualTransmissionRefund(metaFieldValues); err != nil {
return err
}
}
}

return nil
}
6 changes: 6 additions & 0 deletions core/services/ocr2/plugins/ccip/transmitter/transmitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type txManager interface {
type Transmitter interface {
CreateEthTransaction(ctx context.Context, toAddress common.Address, payload []byte, txMeta *txmgr.TxMeta) error
FromAddress(context.Context) common.Address

CreateSecondaryEthTransaction(context.Context, []byte, *txmgr.TxMeta) error
}

type transmitter struct {
Expand Down Expand Up @@ -141,3 +143,7 @@ func (t *transmitter) forwarderAddress() common.Address {
}
return t.effectiveTransmitterAddress
}

func (t *transmitter) CreateSecondaryEthTransaction(ctx context.Context, bytes []byte, meta *txmgr.TxMeta) error {
return errors.New("trying to send a secondary transmission on a non dual transmitter")
}
134 changes: 134 additions & 0 deletions core/services/ocrcommon/dual_transmitter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package ocrcommon

import (
"context"
"math/big"
"net/url"
"slices"

"github.com/ethereum/go-ethereum/common"
"github.com/pkg/errors"

"github.com/smartcontractkit/chainlink/v2/common/txmgr/types"
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/forwarders"
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr"
)

type ocr2FeedsDualTransmission struct {
txm txManager
primaryFromAddresses []common.Address
gasLimit uint64
primaryEffectiveTransmitterAddress common.Address
strategy types.TxStrategy
checker txmgr.TransmitCheckerSpec
chainID *big.Int
keystore roundRobinKeystore

ocr2Aggregator common.Address
txManagerOCR2

secondaryContractAddress common.Address
secondaryFromAddress common.Address
secondaryMeta map[string][]string
}

func (t *ocr2FeedsDualTransmission) forwarderAddress(ctx context.Context, eoa, ocr2Aggregator common.Address) (common.Address, error) {
// If effectiveTransmitterAddress is in fromAddresses, then forwarders aren't set.
if slices.Contains(t.primaryFromAddresses, t.primaryEffectiveTransmitterAddress) {
return common.Address{}, nil
}

forwarderAddress, err := t.GetForwarderForEOAOCR2Feeds(ctx, eoa, ocr2Aggregator)
if err != nil {
return common.Address{}, err
}

// if forwarder address is in fromAddresses, then none of the forwarders are valid
if slices.Contains(t.primaryFromAddresses, forwarderAddress) {
forwarderAddress = common.Address{}
}

return forwarderAddress, nil
}

func (t *ocr2FeedsDualTransmission) CreateEthTransaction(ctx context.Context, toAddress common.Address, payload []byte, txMeta *txmgr.TxMeta) error {
roundRobinFromAddress, err := t.keystore.GetRoundRobinAddress(ctx, t.chainID, t.primaryFromAddresses...)
if err != nil {
return errors.Wrap(err, "skipped OCR transmission, error getting round-robin address")
}

forwarderAddress, err := t.forwarderAddress(ctx, roundRobinFromAddress, toAddress)
if err != nil {
return err
}

_, err = t.txm.CreateTransaction(ctx, txmgr.TxRequest{
FromAddress: roundRobinFromAddress,
ToAddress: toAddress,
EncodedPayload: payload,
FeeLimit: t.gasLimit,
ForwarderAddress: forwarderAddress,
Strategy: t.strategy,
Checker: t.checker,
Meta: txMeta,
})

return errors.Wrap(err, "skipped OCR transmission: skipped primary transmission")
}

func (t *ocr2FeedsDualTransmission) CreateSecondaryEthTransaction(ctx context.Context, payload []byte, txMeta *txmgr.TxMeta) error {
forwarderAddress, err := t.forwarderAddress(ctx, t.secondaryFromAddress, t.secondaryContractAddress)
if err != nil {
return err
}

if txMeta == nil {
txMeta = &txmgr.TxMeta{}
}

dualBroadcast := true
dualBroadcastParams := t.urlParams()

txMeta.DualBroadcast = &dualBroadcast
txMeta.DualBroadcastParams = &dualBroadcastParams

_, err = t.txm.CreateTransaction(ctx, txmgr.TxRequest{
FromAddress: t.secondaryFromAddress,
ToAddress: t.secondaryContractAddress,
EncodedPayload: payload,
ForwarderAddress: forwarderAddress,
FeeLimit: t.gasLimit,
Strategy: t.strategy,
Checker: t.checker,
Meta: txMeta,
})

return errors.Wrap(err, "skipped secondary transmission")
}

func (t *ocr2FeedsDualTransmission) FromAddress(ctx context.Context) common.Address {
roundRobinFromAddress, err := t.keystore.GetRoundRobinAddress(ctx, t.chainID, t.primaryFromAddresses...)
if err != nil {
return t.primaryEffectiveTransmitterAddress
}

forwarderAddress, err := t.GetForwarderForEOAOCR2Feeds(ctx, roundRobinFromAddress, t.ocr2Aggregator)
if errors.Is(err, forwarders.ErrForwarderForEOANotFound) {
// if there are no valid forwarders try to fallback to eoa
return roundRobinFromAddress
} else if err != nil {
return t.primaryEffectiveTransmitterAddress
}

return forwarderAddress
}

func (t *ocr2FeedsDualTransmission) urlParams() string {
values := url.Values{}
for k, v := range t.secondaryMeta {
for _, p := range v {
values.Add(k, p)
}
}
return values.Encode()
}
Loading

0 comments on commit 1251ee3

Please sign in to comment.