Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor extradata codec to unblock ccip ocr message optimization using protobuf #16402

Merged
5 changes: 5 additions & 0 deletions .changeset/four-hats-mate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink": minor
---

refactor extradata codec logic and unblock ccip msg optimization using protobuf #added
3 changes: 3 additions & 0 deletions .mockery.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ packages:
interfaces:
CCIPOracle:
OracleCreator:
github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common:
interfaces:
ExtraDataCodec:
github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/types:
interfaces:
Dispatcher:
Expand Down
8 changes: 5 additions & 3 deletions core/capabilities/ccip/ccipevm/extradatadecoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import (
cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3"
)

func DecodeDestExecDataToMap(DestExecData cciptypes.Bytes) (map[string]interface{}, error) {
destGasAmount, err := abiDecodeUint32(DestExecData)
type ExtraDataDecoder struct{}

func (d ExtraDataDecoder) DecodeDestExecDataToMap(destExecData cciptypes.Bytes) (map[string]interface{}, error) {
destGasAmount, err := abiDecodeUint32(destExecData)
if err != nil {
return nil, fmt.Errorf("decode dest gas amount: %w", err)
}
Expand All @@ -17,7 +19,7 @@ func DecodeDestExecDataToMap(DestExecData cciptypes.Bytes) (map[string]interface
}, nil
}

func DecodeExtraArgsToMap(extraArgs []byte) (map[string]any, error) {
func (d ExtraDataDecoder) DecodeExtraArgsToMap(extraArgs cciptypes.Bytes) (map[string]any, error) {
if len(extraArgs) < 4 {
return nil, fmt.Errorf("extra args too short: %d, should be at least 4 (i.e the extraArgs tag)", len(extraArgs))
}
Expand Down
22 changes: 18 additions & 4 deletions core/capabilities/ccip/ccipsolana/executecodec.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

agbinary "github.com/gagliardetto/binary"
"github.com/gagliardetto/solana-go"
"github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common"

"github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_offramp"
cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3"
Expand All @@ -19,10 +20,13 @@ import (
// Compatible with:
// - "OffRamp 1.6.0-dev"
type ExecutePluginCodecV1 struct {
extraDataCodec common.ExtraDataCodec
}

func NewExecutePluginCodecV1() *ExecutePluginCodecV1 {
return &ExecutePluginCodecV1{}
func NewExecutePluginCodecV1(extraDataCodec common.ExtraDataCodec) *ExecutePluginCodecV1 {
return &ExecutePluginCodecV1{
extraDataCodec: extraDataCodec,
}
}

func (e *ExecutePluginCodecV1) Encode(ctx context.Context, report cciptypes.ExecutePluginReport) ([]byte, error) {
Expand Down Expand Up @@ -50,7 +54,12 @@ func (e *ExecutePluginCodecV1) Encode(ctx context.Context, report cciptypes.Exec
return nil, fmt.Errorf("invalid destTokenAddress address: %v", tokenAmount.DestTokenAddress)
}

destGasAmount, err := extractDestGasAmountFromMap(tokenAmount.DestExecDataDecoded)
destExecDataDecodedMap, err := e.extraDataCodec.DecodeTokenAmountDestExecData(tokenAmount.DestExecData, chainReport.SourceChainSelector)
if err != nil {
return nil, fmt.Errorf("failed to decode dest exec data: %w", err)
}

destGasAmount, err := extractDestGasAmountFromMap(destExecDataDecodedMap)
if err != nil {
return nil, err
}
Expand All @@ -64,8 +73,13 @@ func (e *ExecutePluginCodecV1) Encode(ctx context.Context, report cciptypes.Exec
})
}

extraDataDecodecMap, err := e.extraDataCodec.DecodeExtraArgs(msg.ExtraArgs, chainReport.SourceChainSelector)
if err != nil {
return nil, fmt.Errorf("failed to decode extra args: %w", err)
}

var extraArgs ccip_offramp.Any2SVMRampExtraArgs
extraArgs, _, err := parseExtraArgsMapWithAccounts(msg.ExtraArgsDecoded)
extraArgs, _, err = parseExtraArgsMapWithAccounts(extraDataDecodecMap)
if err != nil {
return nil, fmt.Errorf("invalid extra args map: %w", err)
}
Expand Down
26 changes: 23 additions & 3 deletions core/capabilities/ccip/ccipsolana/executecodec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import (

agbinary "github.com/gagliardetto/binary"
solanago "github.com/gagliardetto/solana-go"
"github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common/mocks"
"github.com/stretchr/testify/mock"

"github.com/smartcontractkit/chainlink-ccip/mocks/pkg/types/ccipocr3"

"github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_offramp"

Expand Down Expand Up @@ -146,10 +150,18 @@ func TestExecutePluginCodecV1(t *testing.T) {
}

ctx := testutils.Context(t)
mockExtraDataCodec := &mocks.ExtraDataCodec{}
mockExtraDataCodec.On("DecodeTokenAmountDestExecData", mock.Anything, mock.Anything).Return(map[string]any{
"destGasAmount": uint32(10),
}, nil)
mockExtraDataCodec.On("DecodeExtraArgs", mock.Anything, mock.Anything).Return(map[string]any{
"ComputeUnits": uint32(1000),
"accountIsWritableBitmap": uint64(2),
}, nil)

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cd := NewExecutePluginCodecV1()
cd := NewExecutePluginCodecV1(mockExtraDataCodec)
report := tc.report(randomExecuteReport(t, tc.chainSelector))
bytes, err := cd.Encode(ctx, report)
if tc.expErr {
Expand Down Expand Up @@ -181,6 +193,14 @@ func TestExecutePluginCodecV1(t *testing.T) {
}

func Test_DecodingExecuteReport(t *testing.T) {
mockExtraDataCodec := ccipocr3.NewMockExtraDataCodec(t)
mockExtraDataCodec.On("DecodeTokenAmountDestExecData", mock.Anything, mock.Anything).Return(map[string]any{
"destGasAmount": uint32(10),
}, nil)
mockExtraDataCodec.On("DecodeExtraArgs", mock.Anything, mock.Anything).Return(map[string]any{
"ComputeUnits": uint32(1000),
"accountIsWritableBitmap": uint64(2),
}, nil)
t.Run("decode on-chain execute report", func(t *testing.T) {
chainSel := cciptypes.ChainSelector(rand.Uint64())
onRampAddr, err := solanago.NewRandomPrivateKey()
Expand Down Expand Up @@ -222,7 +242,7 @@ func Test_DecodingExecuteReport(t *testing.T) {
err = onChainReport.MarshalWithEncoder(encoder)
require.NoError(t, err)

executeCodec := NewExecutePluginCodecV1()
executeCodec := NewExecutePluginCodecV1(mockExtraDataCodec)
decode, err := executeCodec.Decode(testutils.Context(t), buf.Bytes())
require.NoError(t, err)

Expand All @@ -238,7 +258,7 @@ func Test_DecodingExecuteReport(t *testing.T) {

t.Run("decode Borsh encoded execute report", func(t *testing.T) {
ocrReport := randomExecuteReport(t, 124615329519749607)
cd := NewExecutePluginCodecV1()
cd := NewExecutePluginCodecV1(mockExtraDataCodec)
encodedReport, err := cd.Encode(testutils.Context(t), ocrReport)
require.NoError(t, err)

Expand Down
7 changes: 5 additions & 2 deletions core/capabilities/ccip/ccipsolana/extradatadecoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/ethereum/go-ethereum/common/hexutil"
agbinary "github.com/gagliardetto/binary"
cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3"

"github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/fee_quoter"
)
Expand All @@ -26,8 +27,10 @@ var (
evmExtraArgsV2Tag = hexutil.MustDecode("0x181dcf10")
)

type ExtraDataDecoder struct{}

// DecodeExtraArgsToMap is a helper function for converting Borsh encoded extra args bytes into map[string]any, which will be saved in ocr report.message.ExtraArgsDecoded
func DecodeExtraArgsToMap(extraArgs []byte) (map[string]any, error) {
func (d ExtraDataDecoder) DecodeExtraArgsToMap(extraArgs cciptypes.Bytes) (map[string]any, error) {
if len(extraArgs) < 4 {
return nil, fmt.Errorf("extra args too short: %d, should be at least 4 (i.e the extraArgs tag)", len(extraArgs))
}
Expand Down Expand Up @@ -67,7 +70,7 @@ func DecodeExtraArgsToMap(extraArgs []byte) (map[string]any, error) {
return outputMap, nil
}

func DecodeDestExecDataToMap(destExecData []byte) (map[string]any, error) {
func (d ExtraDataDecoder) DecodeDestExecDataToMap(destExecData cciptypes.Bytes) (map[string]any, error) {
return map[string]interface{}{
svmDestExecDataKey: bytesToUint32LE(destExecData),
}, nil
Expand Down
7 changes: 4 additions & 3 deletions core/capabilities/ccip/ccipsolana/extradatadecoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ import (
)

func Test_decodeExtraArgs(t *testing.T) {
extraDataDecoder := &ExtraDataDecoder{}
t.Run("decode dest exec data into map svm", func(t *testing.T) {
destGasAmount := uint32(10000)
encoded := make([]byte, 4)
binary.LittleEndian.PutUint32(encoded, destGasAmount)
output, err := DecodeDestExecDataToMap(encoded)
output, err := extraDataDecoder.DecodeDestExecDataToMap(encoded)
require.NoError(t, err)

decoded, exist := output[svmDestExecDataKey]
Expand All @@ -46,7 +47,7 @@ func Test_decodeExtraArgs(t *testing.T) {
encoder := agbinary.NewBorshEncoder(&buf)
err := extraArgs.MarshalWithEncoder(encoder)
require.NoError(t, err)
output, err := DecodeExtraArgsToMap(append(svmExtraArgsV1Tag, buf.Bytes()...))
output, err := extraDataDecoder.DecodeExtraArgsToMap(append(svmExtraArgsV1Tag, buf.Bytes()...))
require.NoError(t, err)
require.Len(t, output, 5)

Expand Down Expand Up @@ -74,7 +75,7 @@ func Test_decodeExtraArgs(t *testing.T) {
err := extraArgs.MarshalWithEncoder(encoder)
require.NoError(t, err)

output, err := DecodeExtraArgsToMap(append(evmExtraArgsV2Tag, buf.Bytes()...))
output, err := extraDataDecoder.DecodeExtraArgsToMap(append(evmExtraArgsV2Tag, buf.Bytes()...))
require.NoError(t, err)
require.Len(t, output, 2)

Expand Down
24 changes: 18 additions & 6 deletions core/capabilities/ccip/ccipsolana/msghasher.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

"github.com/gagliardetto/solana-go"
"github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common"

"github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_offramp"
"github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/ccip"
Expand All @@ -20,12 +21,14 @@ import (
// Compatible with:
// - "OnRamp 1.6.0-dev"
type MessageHasherV1 struct {
lggr logger.Logger
lggr logger.Logger
extraDataCodec common.ExtraDataCodec
}

func NewMessageHasherV1(lggr logger.Logger) *MessageHasherV1 {
func NewMessageHasherV1(lggr logger.Logger, extraDataCodec cciptypes.ExtraDataCodec) *MessageHasherV1 {
return &MessageHasherV1{
Comment on lines +28 to 29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure we need to pass this in, how about initializing it automatically

Suggested change
func NewMessageHasherV1(lggr logger.Logger, extraDataCodec cciptypes.ExtraDataCodec) *MessageHasherV1 {
return &MessageHasherV1{
func NewMessageHasherV1(lggr logger.Logger) *MessageHasherV1 {
extraDataCodec := ccipcommon.NewExtraDataCodec()
return &MessageHasherV1{

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does make mocking easier

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need to mock the encoder? It looks like the test is doing the full borsh encoding of the extra args, so the real one might work

Copy link
Contributor Author

@huangzhen1997 huangzhen1997 Feb 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's EVM->Solana, we would need ABI encoding, which will introduce some evm related logic in under the ccipsolana.

lggr: lggr,
lggr: lggr,
extraDataCodec: extraDataCodec,
}
}

Expand All @@ -45,7 +48,12 @@ func (h *MessageHasherV1) Hash(_ context.Context, msg cciptypes.Message) (ccipty
anyToSolanaMessage.Sender = msg.Sender
anyToSolanaMessage.Data = msg.Data
for _, ta := range msg.TokenAmounts {
destGasAmount, err := extractDestGasAmountFromMap(ta.DestExecDataDecoded)
destExecDataDecodedMap, err := h.extraDataCodec.DecodeTokenAmountDestExecData(ta.DestExecData, msg.Header.SourceChainSelector)
if err != nil {
return [32]byte{}, fmt.Errorf("failed to decode dest exec data: %w", err)
}

destGasAmount, err := extractDestGasAmountFromMap(destExecDataDecodedMap)
if err != nil {
return [32]byte{}, err
}
Expand All @@ -59,9 +67,13 @@ func (h *MessageHasherV1) Hash(_ context.Context, msg cciptypes.Message) (ccipty
})
}

var err error
extraDataDecodecMap, err := h.extraDataCodec.DecodeExtraArgs(msg.ExtraArgs, msg.Header.SourceChainSelector)
if err != nil {
return [32]byte{}, fmt.Errorf("failed to decode extra args: %w", err)
}

var msgAccounts []solana.PublicKey
anyToSolanaMessage.ExtraArgs, msgAccounts, err = parseExtraArgsMapWithAccounts(msg.ExtraArgsDecoded)
anyToSolanaMessage.ExtraArgs, msgAccounts, err = parseExtraArgsMapWithAccounts(extraDataDecodecMap)
if err != nil {
return [32]byte{}, fmt.Errorf("failed to decode ExtraArgs: %w", err)
}
Expand Down
17 changes: 16 additions & 1 deletion core/capabilities/ccip/ccipsolana/msghasher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (

agbinary "github.com/gagliardetto/binary"
"github.com/gagliardetto/solana-go"
"github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common/mocks"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/config"
Expand All @@ -25,7 +27,20 @@ import (

func TestMessageHasher_Any2Solana(t *testing.T) {
any2AnyMsg, any2SolanaMsg, msgAccounts := createAny2SolanaMessages(t)
msgHasher := NewMessageHasherV1(logger.Test(t))
mockExtraDataCodec := &mocks.ExtraDataCodec{}
mockExtraDataCodec.On("DecodeTokenAmountDestExecData", mock.Anything, mock.Anything).Return(map[string]any{
"destGasAmount": uint32(10),
}, nil)
mockExtraDataCodec.On("DecodeExtraArgs", mock.Anything, mock.Anything).Return(map[string]any{
"ComputeUnits": uint32(1000),
"AccountIsWritableBitmap": uint64(10),
"Accounts": [][32]byte{
[32]byte(config.CcipLogicReceiver.Bytes()),
[32]byte(config.ReceiverTargetAccountPDA.Bytes()),
[32]byte(solana.SystemProgramID.Bytes()),
},
}, nil)
msgHasher := NewMessageHasherV1(logger.Test(t), mockExtraDataCodec)
actualHash, err := msgHasher.Hash(testutils.Context(t), any2AnyMsg)
require.NoError(t, err)
expectedHash, err := ccip.HashAnyToSVMMessage(any2SolanaMsg, any2AnyMsg.Header.OnRamp, msgAccounts)
Expand Down
39 changes: 27 additions & 12 deletions core/capabilities/ccip/common/extradatacodec.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,34 @@ import (
"fmt"

chainsel "github.com/smartcontractkit/chain-selectors"

cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3"
"github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccipevm"
"github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccipsolana"
)

type ExtraDataCodec struct{}
type ExtraDataCodec interface {
// DecodeExtraArgs reformat bytes into a chain agnostic map[string]any representation for extra args
DecodeExtraArgs(extraArgs cciptypes.Bytes, sourceChainSelector cciptypes.ChainSelector) (map[string]any, error)
// DecodeTokenAmountDestExecData reformat bytes to chain-agnostic map[string]any for tokenAmount DestExecData field
DecodeTokenAmountDestExecData(destExecData cciptypes.Bytes, sourceChainSelector cciptypes.ChainSelector) (map[string]any, error)
}

type ExtraDataDecoder interface {
DecodeExtraArgsToMap(extraArgs cciptypes.Bytes) (map[string]any, error)
DecodeDestExecDataToMap(destExecData cciptypes.Bytes) (map[string]any, error)
}

func NewExtraDataCodec() ExtraDataCodec {
return ExtraDataCodec{}
type RealExtraDataCodec struct {
EVMExtraDataDecoder ExtraDataDecoder
SolanaExtraDataDecoder ExtraDataDecoder
}

func NewExtraDataCodec(evmDecoder ExtraDataDecoder, solanaDecoder ExtraDataDecoder) RealExtraDataCodec {
return RealExtraDataCodec{
EVMExtraDataDecoder: evmDecoder,
SolanaExtraDataDecoder: solanaDecoder,
}
}

func (c ExtraDataCodec) DecodeExtraArgs(extraArgs cciptypes.Bytes, sourceChainSelector cciptypes.ChainSelector) (map[string]any, error) {
func (c RealExtraDataCodec) DecodeExtraArgs(extraArgs cciptypes.Bytes, sourceChainSelector cciptypes.ChainSelector) (map[string]any, error) {
if len(extraArgs) == 0 {
// return empty map if extraArgs is empty
return nil, nil
Expand All @@ -29,17 +44,17 @@ func (c ExtraDataCodec) DecodeExtraArgs(extraArgs cciptypes.Bytes, sourceChainSe

switch family {
case chainsel.FamilyEVM:
return ccipevm.DecodeExtraArgsToMap(extraArgs)
return c.EVMExtraDataDecoder.DecodeExtraArgsToMap(extraArgs)

case chainsel.FamilySolana:
return ccipsolana.DecodeExtraArgsToMap(extraArgs)
return c.SolanaExtraDataDecoder.DecodeExtraArgsToMap(extraArgs)

default:
return nil, fmt.Errorf("unsupported family for extra args type %s", family)
}
}

func (c ExtraDataCodec) DecodeTokenAmountDestExecData(destExecData cciptypes.Bytes, sourceChainSelector cciptypes.ChainSelector) (map[string]any, error) {
func (c RealExtraDataCodec) DecodeTokenAmountDestExecData(destExecData cciptypes.Bytes, sourceChainSelector cciptypes.ChainSelector) (map[string]any, error) {
if len(destExecData) == 0 {
// return empty map if destExecData is empty
return nil, nil
Expand All @@ -52,10 +67,10 @@ func (c ExtraDataCodec) DecodeTokenAmountDestExecData(destExecData cciptypes.Byt

switch family {
case chainsel.FamilyEVM:
return ccipevm.DecodeDestExecDataToMap(destExecData)
return c.EVMExtraDataDecoder.DecodeDestExecDataToMap(destExecData)

case chainsel.FamilySolana:
return ccipsolana.DecodeDestExecDataToMap(destExecData)
return c.SolanaExtraDataDecoder.DecodeDestExecDataToMap(destExecData)

default:
return nil, fmt.Errorf("unsupported family for extra args type %s", family)
Expand Down
Loading
Loading