Skip to content

Commit

Permalink
support Solana->EVM
Browse files Browse the repository at this point in the history
  • Loading branch information
huangzhen1997 committed Feb 14, 2025
1 parent 817ff37 commit f1faab0
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 47 deletions.
19 changes: 16 additions & 3 deletions core/capabilities/ccip/ccipevm/executecodec.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
ccipcommon "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common"

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

Expand All @@ -19,9 +20,10 @@ import (
// - "OffRamp 1.6.0"
type ExecutePluginCodecV1 struct {
executeReportMethodInputs abi.Arguments
extraDataCodec ccipcommon.ExtraDataCodec
}

func NewExecutePluginCodecV1() *ExecutePluginCodecV1 {
func NewExecutePluginCodecV1(extraDataCodec ccipcommon.ExtraDataCodec) *ExecutePluginCodecV1 {
abiParsed, err := abi.JSON(strings.NewReader(offramp.OffRampABI))
if err != nil {
panic(fmt.Errorf("parse multi offramp abi: %s", err))
Expand All @@ -33,6 +35,7 @@ func NewExecutePluginCodecV1() *ExecutePluginCodecV1 {

return &ExecutePluginCodecV1{
executeReportMethodInputs: methodInputs[:1],
extraDataCodec: extraDataCodec,
}
}

Expand All @@ -59,7 +62,12 @@ func (e *ExecutePluginCodecV1) Encode(ctx context.Context, report cciptypes.Exec
return nil, fmt.Errorf("empty amount for token: %s", tokenAmount.DestTokenAddress)
}

destGasAmount, err := abiDecodeUint32(tokenAmount.DestExecData)
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, fmt.Errorf("decode dest gas amount: %w", err)
}
Expand All @@ -82,7 +90,12 @@ func (e *ExecutePluginCodecV1) Encode(ctx context.Context, report cciptypes.Exec
})
}

gasLimit, err := decodeExtraArgsV1V2(message.ExtraArgs)
decodedExtraArgsMap, err := e.extraDataCodec.DecodeExtraArgs(message.ExtraArgs, chainReport.SourceChainSelector)
if err != nil {
return nil, err
}

gasLimit, err := parseExtraDataMap(decodedExtraArgsMap)
if err != nil {
return nil, fmt.Errorf("decode extra args to get gas limit: %w", err)
}
Expand Down
34 changes: 21 additions & 13 deletions core/capabilities/ccip/ccipevm/executecodec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccipsolana"
ccipcommon "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

Expand All @@ -25,7 +27,7 @@ import (
"github.com/smartcontractkit/chainlink/v2/core/internal/testutils"
)

var randomExecuteReport = func(t *testing.T, d *testSetupData) cciptypes.ExecutePluginReport {
var randomExecuteReport = func(t *testing.T, d *testSetupData, chainSelector uint64) cciptypes.ExecutePluginReport {
const numChainReports = 10
const msgsPerReport = 10
const numTokensPerMsg = 3
Expand Down Expand Up @@ -82,7 +84,7 @@ var randomExecuteReport = func(t *testing.T, d *testSetupData) cciptypes.Execute
}

chainReports[i] = cciptypes.ExecutePluginReportSingleChain{
SourceChainSelector: cciptypes.ChainSelector(rand.Uint64()),
SourceChainSelector: cciptypes.ChainSelector(chainSelector),
Messages: reportMessages,
OffchainTokenData: tokenData,
Proofs: []cciptypes.Bytes32{utils.RandomBytes32(), utils.RandomBytes32()},
Expand All @@ -95,16 +97,19 @@ var randomExecuteReport = func(t *testing.T, d *testSetupData) cciptypes.Execute

func TestExecutePluginCodecV1(t *testing.T) {
d := testSetup(t)
ExtraData := ccipcommon.NewExtraDataCodec(ccipcommon.NewExtraDataCodecParams(ExtraDataDecoder{}, ccipsolana.ExtraDataDecoder{}))

testCases := []struct {
name string
report func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport
expErr bool
name string
report func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport
expErr bool
chainSelector uint64
}{
{
name: "base report",
report: func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport { return report },
expErr: false,
name: "base report",
report: func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport { return report },
expErr: false,
chainSelector: 5009297550715157269, // ETH mainnet chain selector
},
{
name: "reports have empty msgs",
Expand All @@ -113,7 +118,8 @@ func TestExecutePluginCodecV1(t *testing.T) {
report.ChainReports[4].Messages = []cciptypes.Message{}
return report
},
expErr: false,
expErr: false,
chainSelector: 5009297550715157269, // ETH mainnet chain selector
},
{
name: "reports have empty offchain token data",
Expand All @@ -122,7 +128,8 @@ func TestExecutePluginCodecV1(t *testing.T) {
report.ChainReports[4].OffchainTokenData[1] = [][]byte{}
return report
},
expErr: false,
expErr: false,
chainSelector: 5009297550715157269, // ETH mainnet chain selector
},
}

Expand All @@ -141,8 +148,8 @@ func TestExecutePluginCodecV1(t *testing.T) {

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
codec := NewExecutePluginCodecV1()
report := tc.report(randomExecuteReport(t, d))
codec := NewExecutePluginCodecV1(ExtraData)
report := tc.report(randomExecuteReport(t, d, tc.chainSelector))
bytes, err := codec.Encode(ctx, report)
if tc.expErr {
assert.Error(t, err)
Expand Down Expand Up @@ -183,6 +190,7 @@ func TestExecutePluginCodecV1(t *testing.T) {
}

func Test_DecodeReport(t *testing.T) {
ExtraDataCodec := ccipcommon.NewExtraDataCodec(ccipcommon.NewExtraDataCodecParams(ExtraDataDecoder{}, ccipsolana.ExtraDataDecoder{}))
offRampABI, err := offramp.OffRampMetaData.GetAbi()
require.NoError(t, err)

Expand All @@ -201,7 +209,7 @@ func Test_DecodeReport(t *testing.T) {

rawReport := *abi.ConvertType(executeInputs[1], new([]byte)).(*[]byte)

codec := NewExecutePluginCodecV1()
codec := NewExecutePluginCodecV1(ExtraDataCodec)
decoded, err := codec.Decode(tests.Context(t), rawReport)
require.NoError(t, err)

Expand Down
61 changes: 56 additions & 5 deletions core/capabilities/ccip/ccipevm/msghasher.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ package ccipevm

import (
"context"
"errors"
"fmt"
"math/big"
"strings"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
ccipcommon "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common"

"github.com/smartcontractkit/chainlink-ccip/pkg/logutil"
cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3"
Expand Down Expand Up @@ -40,12 +44,14 @@ var (
// Compatible with:
// - "OnRamp 1.6.0"
type MessageHasherV1 struct {
lggr logger.Logger
lggr logger.Logger
extraDataCodec ccipcommon.ExtraDataCodec
}

func NewMessageHasherV1(lggr logger.Logger) *MessageHasherV1 {
func NewMessageHasherV1(lggr logger.Logger, extraDataCodec ccipcommon.ExtraDataCodec) *MessageHasherV1 {
return &MessageHasherV1{
lggr: lggr,
lggr: lggr,
extraDataCodec: extraDataCodec,
}
}

Expand Down Expand Up @@ -154,9 +160,15 @@ func (h *MessageHasherV1) Hash(ctx context.Context, msg cciptypes.Message) (ccip
// TODO: we assume that extra args is always abi-encoded for now, but we need
// to decode according to source chain selector family. We should add a family
// lookup API to the chain-selectors library.
gasLimit, err := decodeExtraArgsV1V2(msg.ExtraArgs)

decodedExtraArgsMap, err := h.extraDataCodec.DecodeExtraArgs(msg.ExtraArgs, msg.Header.SourceChainSelector)
if err != nil {
return [32]byte{}, err
}

gasLimit, err := parseExtraDataMap(decodedExtraArgsMap)
if err != nil {
return [32]byte{}, fmt.Errorf("decode extra args: %w", err)
return [32]byte{}, fmt.Errorf("decode extra args to get gas limit: %w", err)
}

lggr.Debugw("decoded msg gas limit", "gasLimit", gasLimit)
Expand Down Expand Up @@ -242,5 +254,44 @@ func abiDecodeAddress(data []byte) (common.Address, error) {
return val, nil
}

func parseExtraDataMap(input map[string]any) (*big.Int, error) {
var outputGas *big.Int
for fieldName, fieldValue := range input {
lowercase := strings.ToLower(fieldName)
switch lowercase {
case "gaslimit":
// Expect [][32]byte
if val, ok := fieldValue.(*big.Int); ok {
outputGas = val
return outputGas, nil
} else {
return nil, fmt.Errorf("unexpected type for gas limit: %T", fieldValue)
}
default:
// no error here, as we only need the keys to gasLimit, other keys can be skipped without like AllowOutOfOrderExecution etc.
}
}
return outputGas, errors.New("gas limit not found in extra data map")
}

func extractDestGasAmountFromMap(input map[string]any) (uint32, error) {
// Iterate through the expected fields in the struct
for fieldName, fieldValue := range input {
lowercase := strings.ToLower(fieldName)
switch lowercase {
case "destgasamount":
// Expect uint32
if val, ok := fieldValue.(uint32); ok {
return val, nil
} else {
return 0, errors.New("invalid type for destgasamount, expected uint32")
}
default:
}
}

return 0, errors.New("invalid token message, dest gas amount not found in the DestExecDataDecoded map")
}

// Interface compliance check
var _ cciptypes.MessageHasher = (*MessageHasherV1)(nil)
35 changes: 20 additions & 15 deletions core/capabilities/ccip/ccipevm/msghasher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccipsolana"
ccipcommon "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink-common/pkg/logger"
Expand All @@ -31,26 +33,28 @@ import (
cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3"
)

var ExtraDataCodec = ccipcommon.NewExtraDataCodec(ccipcommon.NewExtraDataCodecParams(ExtraDataDecoder{}, ccipsolana.ExtraDataDecoder{}))

// NOTE: these test cases are only EVM <-> EVM.
// Update these cases once we have non-EVM examples.
func TestMessageHasher_EVM2EVM(t *testing.T) {
ctx := testutils.Context(t)
d := testSetup(t)

testCases := []evmExtraArgs{
{version: "v1", gasLimit: big.NewInt(rand.Int63())},
{version: "v2", gasLimit: big.NewInt(rand.Int63()), allowOOO: false},
{version: "v2", gasLimit: big.NewInt(rand.Int63()), allowOOO: true},
{version: "v1", gasLimit: big.NewInt(rand.Int63()), chainSelector: 5009297550715157269}, // ETH mainnet chain selector
{version: "v2", gasLimit: big.NewInt(rand.Int63()), allowOOO: false, chainSelector: 5009297550715157269}, // ETH mainnet chain selector
{version: "v2", gasLimit: big.NewInt(rand.Int63()), allowOOO: true, chainSelector: 5009297550715157269}, // ETH mainnet chain selector
}
for i, tc := range testCases {
t.Run(fmt.Sprintf("tc_%d", i), func(tt *testing.T) {
testHasherEVM2EVM(ctx, tt, d, tc)
testHasherEVM2EVM(ctx, tt, d, tc, tc.chainSelector)
})
}
}

func testHasherEVM2EVM(ctx context.Context, t *testing.T, d *testSetupData, evmExtraArgs evmExtraArgs) {
ccipMsg := createEVM2EVMMessage(t, d.contract, evmExtraArgs)
func testHasherEVM2EVM(ctx context.Context, t *testing.T, d *testSetupData, evmExtraArgs evmExtraArgs, sourceChainSelector uint64) {
ccipMsg := createEVM2EVMMessage(t, d.contract, evmExtraArgs, sourceChainSelector)

var tokenAmounts []message_hasher.InternalAny2EVMTokenTransfer
for _, rta := range ccipMsg.TokenAmounts {
Expand Down Expand Up @@ -83,27 +87,28 @@ func testHasherEVM2EVM(ctx context.Context, t *testing.T, d *testSetupData, evmE
expectedHash, err := d.contract.Hash(&bind.CallOpts{Context: ctx}, evmMsg, ccipMsg.Header.OnRamp)
require.NoError(t, err)

evmMsgHasher := NewMessageHasherV1(logger.Test(t))
evmMsgHasher := NewMessageHasherV1(logger.Test(t), ExtraDataCodec)
actualHash, err := evmMsgHasher.Hash(ctx, ccipMsg)
require.NoError(t, err)

require.Equal(t, fmt.Sprintf("%x", expectedHash), strings.TrimPrefix(actualHash.String(), "0x"))
}

type evmExtraArgs struct {
version string
gasLimit *big.Int
allowOOO bool
version string
gasLimit *big.Int
allowOOO bool
chainSelector uint64
}

func createEVM2EVMMessage(t *testing.T, messageHasher *message_hasher.MessageHasher, evmExtraArgs evmExtraArgs) cciptypes.Message {
func createEVM2EVMMessage(t *testing.T, messageHasher *message_hasher.MessageHasher, evmExtraArgs evmExtraArgs, sourceChainSelector uint64) cciptypes.Message {
messageID := utils.RandomBytes32()

sourceTokenData := make([]byte, rand.Intn(2048))
_, err := cryptorand.Read(sourceTokenData)
require.NoError(t, err)

sourceChain := rand.Uint64()
sourceChain := sourceChainSelector
seqNum := rand.Uint64()
nonce := rand.Uint64()
destChain := rand.Uint64()
Expand Down Expand Up @@ -257,7 +262,7 @@ func TestMessagerHasher_againstRmnSharedVector(t *testing.T) {
}, any2EVMMessage, common.LeftPadBytes(msg.Header.OnRamp, 32))
require.NoError(t, err)

h := NewMessageHasherV1(logger.Test(t))
h := NewMessageHasherV1(logger.Test(t), ExtraDataCodec)
msgH, err := h.Hash(tests.Context(t), msg)
require.NoError(t, err)
require.Equal(t, expectedMsgHash, msgH.String())
Expand Down Expand Up @@ -330,7 +335,7 @@ func TestMessagerHasher_againstRmnSharedVector(t *testing.T) {
rmnMsgHash = "0xb6ea678f918293745bfb8db05d79dcf08986c7da3e302ac5f6782618a6f11967"
)

h := NewMessageHasherV1(logger.Test(t))
h := NewMessageHasherV1(logger.Test(t), ExtraDataCodec)
msgH, err := h.Hash(tests.Context(t), msg)
require.NoError(t, err)

Expand All @@ -355,7 +360,7 @@ func TestMessagerHasher_againstRmnSharedVector(t *testing.T) {
err = json.Unmarshal(data, &msgs)
require.NoError(t, err)

msgHasher := NewMessageHasherV1(logger.Test(t))
msgHasher := NewMessageHasherV1(logger.Test(t), ExtraDataCodec)

for _, msg := range msgs {
any2EVMMessage := ccipMsgToAny2EVMMessage(t, msg)
Expand Down
12 changes: 5 additions & 7 deletions core/capabilities/ccip/ccipsolana/executecodec.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,25 +206,23 @@ func (e *ExecutePluginCodecV1) Decode(ctx context.Context, encodedReport []byte)
}

func extractDestGasAmountFromMap(input map[string]any) (uint32, error) {
var out uint32

// Iterate through the expected fields in the struct
// Search for the gas fields
for fieldName, fieldValue := range input {
lowercase := strings.ToLower(fieldName)
switch lowercase {
case "destgasamount":
// Expect uint32
if v, ok := fieldValue.(uint32); ok {
out = v
return v, nil
} else {
return out, errors.New("invalid type for destgasamount, expected uint32")
return 0, errors.New("invalid type for destgasamount, expected uint32")
}
default:
return out, errors.New("invalid token message, dest gas amount not found in the DestExecDataDecoded map")

}
}

return out, nil
return 0, errors.New("invalid token message, dest gas amount not found in the DestExecDataDecoded map")
}

// Ensure ExecutePluginCodec implements the ExecutePluginCodec interface
Expand Down
Loading

0 comments on commit f1faab0

Please sign in to comment.