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

Add generic codec for CCIP plugin #15454

Draft
wants to merge 12 commits into
base: develop
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changeset/serious-cups-yawn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink": minor
---

add new commit and exec codec using evm codec #added
107 changes: 107 additions & 0 deletions core/capabilities/ccip/ccipevm/commitcodecv2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package ccipevm

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

"github.com/ethereum/go-ethereum/common"
cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3"
commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec"
commontypes "github.com/smartcontractkit/chainlink-common/pkg/types"
"github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/codec"
"github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types"
)

const commitReportABI = `[{"components":[{"name":"chainSel","type":"uint64","internalType":"uint64"},{"name":"onRampAddress","type":"bytes","internalType":"bytes"},{"name":"seqNumsRange","type":"uint64[2]","internalType":"uint64[2]"},{"name":"merkleRoot","type":"bytes32","internalType":"bytes32"}],"name":"merkleRoots","type":"tuple[]","internalType":"struct MerkleRootChain[]"},{"components":[{"components":[{"name":"tokenID","type":"string","internalType":"string"},{"name":"price","type":"uint256","internalType":"uint256"}],"name":"tokenPriceUpdates","type":"tuple[]","internalType":"struct TokenPrice[]"},{"components":[{"name":"chainSel","type":"uint64","internalType":"uint64"},{"name":"gasPrice","type":"uint256","internalType":"uint256"}],"name":"gasPriceUpdates","type":"tuple[]","internalType":"struct GasPriceChain[]"}],"name":"priceUpdates","type":"tuple","internalType":"struct PriceUpdates"},{"components":[{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}],"name":"rmnSignatures","type":"tuple[]"}]`
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be generated from the contracts eventually?

Copy link
Contributor Author

@huangzhen1997 huangzhen1997 Dec 2, 2024

Choose a reason for hiding this comment

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

Yes, that would be ideal. The existing generated ABI seems match with the format from generated MerkleRoot, not with the original report format. See discussion from slack thread


var commitCodecConfig = types.CodecConfig{
Configs: map[string]types.ChainCodecConfig{
"CommitPluginReport": {
TypeABI: commitReportABI,
ModifierConfigs: commoncodec.ModifiersConfig{
&commoncodec.WrapperModifierConfig{Fields: map[string]string{
"PriceUpdates.TokenPriceUpdates.Price": "Int",
"PriceUpdates.GasPriceUpdates.GasPrice": "Int",
}},
},
},
},
}

// CommitPluginCodecV2 is a codec for encoding and decoding commit plugin reports using generic evm codec
type CommitPluginCodecV2 struct {
codec commontypes.RemoteCodec
}

func NewCommitPluginCodecV2() (*CommitPluginCodecV2, error) {
cd, err := codec.NewCodec(commitCodecConfig)
if err != nil {
return nil, err
}
return &CommitPluginCodecV2{codec: cd}, nil
}

func validateReport(report cciptypes.CommitPluginReport) error {
for _, update := range report.PriceUpdates.TokenPriceUpdates {
if !common.IsHexAddress(string(update.TokenID)) {
return fmt.Errorf("invalid token address: %s", update.TokenID)
}
if update.Price.IsEmpty() {
return fmt.Errorf("empty price for token: %s", update.TokenID)
}
}

for _, update := range report.PriceUpdates.GasPriceUpdates {
if update.GasPrice.IsEmpty() {
return fmt.Errorf("empty gas price for chain: %d", update.ChainSel)
}
}

return nil
}

func (c *CommitPluginCodecV2) Encode(ctx context.Context, report cciptypes.CommitPluginReport) ([]byte, error) {
if err := validateReport(report); err != nil {
return nil, err
}

return c.codec.Encode(ctx, report, "CommitPluginReport")
}

func commitPostProcess(report *cciptypes.CommitPluginReport) error {
for index, update := range report.PriceUpdates.TokenPriceUpdates {
if !common.IsHexAddress(string(update.TokenID)) {
return fmt.Errorf("invalid token address: %s", update.TokenID)
}
if update.Price.IsEmpty() || update.Price.Int64() == 0 {
report.PriceUpdates.TokenPriceUpdates[index].Price = cciptypes.NewBigInt(big.NewInt(0))
}
}

for idx, update := range report.PriceUpdates.GasPriceUpdates {
if update.GasPrice.IsEmpty() || update.GasPrice.Int64() == 0 {
report.PriceUpdates.GasPriceUpdates[idx].GasPrice = cciptypes.NewBigInt(big.NewInt(0))
}
}

return nil
}

func (c *CommitPluginCodecV2) Decode(ctx context.Context, bytes []byte) (cciptypes.CommitPluginReport, error) {
report := cciptypes.CommitPluginReport{}

err := c.codec.Decode(ctx, bytes, &report, "CommitPluginReport")
if err != nil {
return report, err
}

if err = commitPostProcess(&report); err != nil {
return report, err
}

return report, nil
}

// Ensure CommitPluginCodec implements the CommitPluginCodec interface
var _ cciptypes.CommitPluginCodec = (*CommitPluginCodecV2)(nil)
99 changes: 99 additions & 0 deletions core/capabilities/ccip/ccipevm/commitcodecv2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package ccipevm

import (
"math/big"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3"
"github.com/smartcontractkit/chainlink/v2/core/internal/testutils"
)

func TestCommitPluginCodecV2(t *testing.T) {
testCases := []struct {
name string
report func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport
expErr bool
}{
{
name: "base report",
report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport {
return report
},
},
{
name: "empty token address",
report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport {
report.PriceUpdates.TokenPriceUpdates[0].TokenID = ""
return report
},
expErr: true,
},
{
name: "empty merkle root",
report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport {
report.MerkleRoots[0].MerkleRoot = cciptypes.Bytes32{}
return report
},
},
{
name: "zero token price",
report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport {
report.PriceUpdates.TokenPriceUpdates[0].Price = cciptypes.NewBigInt(big.NewInt(0))
return report
},
},
{
name: "zero gas price",
report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport {
report.PriceUpdates.GasPriceUpdates[0].GasPrice = cciptypes.NewBigInt(big.NewInt(0))
return report
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
report := tc.report(randomCommitReport())
commitCodec, err := NewCommitPluginCodecV2()
require.NoError(t, err)
ctx := testutils.Context(t)
encodedReport, err := commitCodec.Encode(ctx, report)
if tc.expErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
decodedReport, err := commitCodec.Decode(ctx, encodedReport)
require.NoError(t, err)
require.Equal(t, report, decodedReport)
})
}
}

func BenchmarkCommitPluginCodecV2_Encode(b *testing.B) {
commitCodec, err := NewCommitPluginCodecV2()
require.NoError(b, err)
ctx := testutils.Context(b)

rep := randomCommitReport()
for i := 0; i < b.N; i++ {
_, err := commitCodec.Encode(ctx, rep)
require.NoError(b, err)
}
}

func BenchmarkCommitPluginCodecV2_Decode(b *testing.B) {
commitCodec, err := NewCommitPluginCodecV2()
require.NoError(b, err)
ctx := testutils.Context(b)
encodedReport, err := commitCodec.Encode(ctx, randomCommitReport())
require.NoError(b, err)

for i := 0; i < b.N; i++ {
_, err := commitCodec.Decode(ctx, encodedReport)
require.NoError(b, err)
}
}
3 changes: 2 additions & 1 deletion core/capabilities/ccip/ccipevm/executecodec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"

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

"github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets"
Expand Down Expand Up @@ -68,6 +67,7 @@ var randomExecuteReport = func(t *testing.T, d *testSetupData) cciptypes.Execute
ExtraArgs: extraArgs,
FeeToken: utils.RandomAddress().Bytes(),
FeeTokenAmount: cciptypes.NewBigInt(utils.RandUint256()),
FeeValueJuels: cciptypes.NewBigInt(utils.RandUint256()),
TokenAmounts: tokenAmounts,
}
}
Expand Down Expand Up @@ -156,6 +156,7 @@ func TestExecutePluginCodecV1(t *testing.T) {
report.ChainReports[i].Messages[j].FeeToken = cciptypes.UnknownAddress{}
report.ChainReports[i].Messages[j].ExtraArgs = cciptypes.Bytes{}
report.ChainReports[i].Messages[j].FeeTokenAmount = cciptypes.BigInt{}
report.ChainReports[i].Messages[j].FeeValueJuels = cciptypes.BigInt{}
}
}

Expand Down
137 changes: 137 additions & 0 deletions core/capabilities/ccip/ccipevm/executecodecv2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package ccipevm

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

commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec"
commontypes "github.com/smartcontractkit/chainlink-common/pkg/types"
"github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/codec"
"github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types"

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

const execReportABI = `[{"name":"chainReports","type":"tuple[]","components":[{"name":"sourceChainSelector","type":"uint64","internalType":"uint64"},{"name":"messages","type":"tuple[]","components":[{"name":"header","type":"tuple","components":[{"name":"messageID","type":"bytes32","internalType":"bytes32"},{"name":"sourceChainSelector","type":"uint64","internalType":"uint64"},{"name":"destChainSelector","type":"uint64","internalType":"uint64"},{"name":"sequenceNumber","type":"uint64","internalType":"uint64"},{"name":"nonce","type":"uint64","internalType":"uint64"},{"name":"msgHash","type":"bytes32","internalType":"bytes32"},{"name":"onRamp","type":"bytes","internalType":"bytes"}]},{"name":"data","type":"bytes","internalType":"bytes"},{"name":"sender","type":"bytes","internalType":"bytes"},{"name":"receiver","type":"bytes","internalType":"bytes"},{"name":"extraArgs","type":"bytes","internalType":"bytes"},{"name":"feeToken","type":"bytes","internalType":"bytes"},{"name":"feeTokenAmount","type":"uint256","internalType":"uint256"},{"name":"feeValueJuels","type":"uint256","internalType":"uint256"},{"name":"tokenAmounts","type":"tuple[]","components":[{"name":"sourcePoolAddress","type":"bytes","internalType":"bytes"},{"name":"destTokenAddress","type":"bytes","internalType":"bytes"},{"name":"extraData","type":"bytes","internalType":"bytes"},{"name":"amount","type":"uint256","internalType":"uint256"},{"name":"destExecData","type":"bytes","internalType":"bytes"}]}]},{"name":"offchainTokenData","type":"bytes[][]","internalType":"bytes[][]"},{"name":"proofs","type":"bytes32[]","internalType":"bytes32[]"},{"name":"proofFlagBits","type":"uint256","internalType":"uint256"}]}]`

var execCodecConfig = types.CodecConfig{
Configs: map[string]types.ChainCodecConfig{
"ExecPluginReport": {
TypeABI: execReportABI,
ModifierConfigs: commoncodec.ModifiersConfig{
&commoncodec.WrapperModifierConfig{
Fields: map[string]string{
"ChainReports.Messages.FeeTokenAmount": "Int",
"ChainReports.Messages.FeeValueJuels": "Int",
"ChainReports.Messages.TokenAmounts.Amount": "Int",
"ChainReports.ProofFlagBits": "Int",
},
},
},
},
},
}

// ExecutePluginCodecV2 is a codec for encoding and decoding execute plugin reports with generic codec
type ExecutePluginCodecV2 struct {
codec commontypes.RemoteCodec
}

func NewExecutePluginCodecV2() (*ExecutePluginCodecV2, error) {
cd, err := codec.NewCodec(execCodecConfig)
if err != nil {
return nil, err
}
return &ExecutePluginCodecV2{codec: cd}, nil
}

func validate(report cciptypes.ExecutePluginReport) error {
for i, chainReport := range report.ChainReports {
if chainReport.ProofFlagBits.IsEmpty() {
return errors.New("proof flag bits are empty")
}

for j, message := range chainReport.Messages {
// optional fields
if message.FeeToken == nil {
report.ChainReports[i].Messages[j].FeeToken = []byte{}
}

if message.FeeValueJuels.IsEmpty() {
report.ChainReports[i].Messages[j].FeeValueJuels = cciptypes.NewBigInt(big.NewInt(0))
}

if message.FeeTokenAmount.IsEmpty() {
report.ChainReports[i].Messages[j].FeeTokenAmount = cciptypes.NewBigInt(big.NewInt(0))
}

// required fields
if message.Sender == nil {
return errors.New("message sender is nil")
}

for _, tokenAmount := range message.TokenAmounts {
if tokenAmount.Amount.IsEmpty() {
return fmt.Errorf("empty amount for token: %s", tokenAmount.DestTokenAddress)
}

_, err := abiDecodeUint32(tokenAmount.DestExecData)
if err != nil {
return fmt.Errorf("decode dest gas amount: %w", err)
}
}

_, err := decodeExtraArgsV1V2(message.ExtraArgs)
if err != nil {
return fmt.Errorf("decode extra args to get gas limit: %w", err)
}
}
}

return nil
}

func (e *ExecutePluginCodecV2) Encode(ctx context.Context, report cciptypes.ExecutePluginReport) ([]byte, error) {
if err := validate(report); err != nil {
return nil, err
}

return e.codec.Encode(ctx, report, "ExecPluginReport")
}

func execPostProcess(report *cciptypes.ExecutePluginReport) error {
if len(report.ChainReports) == 0 {
return errors.New("chain reports is empty")
}

for i, evmChainReport := range report.ChainReports {
for j := range evmChainReport.Messages {
report.ChainReports[i].Messages[j].Header.MsgHash = cciptypes.Bytes32{}
report.ChainReports[i].Messages[j].Header.OnRamp = cciptypes.UnknownAddress{}
report.ChainReports[i].Messages[j].ExtraArgs = cciptypes.Bytes{}
report.ChainReports[i].Messages[j].FeeToken = cciptypes.UnknownAddress{}
report.ChainReports[i].Messages[j].FeeTokenAmount = cciptypes.BigInt{}
}
}

return nil
}

func (e *ExecutePluginCodecV2) Decode(ctx context.Context, encodedReport []byte) (cciptypes.ExecutePluginReport, error) {
report := cciptypes.ExecutePluginReport{}
err := e.codec.Decode(ctx, encodedReport, &report, "ExecPluginReport")
if err != nil {
return report, err
}

if err = execPostProcess(&report); err != nil {
return report, err
}

return report, err
}

// Ensure ExecutePluginCodec implements the ExecutePluginCodec interface
var _ cciptypes.ExecutePluginCodec = (*ExecutePluginCodecV2)(nil)
Loading
Loading