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
109 changes: 109 additions & 0 deletions core/capabilities/ccip/ccipevm/commitcodecv2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
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"
"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{}

func NewCommitPluginCodecV2() *CommitPluginCodecV2 {
return &CommitPluginCodecV2{}
}

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
}

cd, err := codec.NewCodec(commitCodecConfig)
if err != nil {
return nil, err
}
Copy link
Contributor

Choose a reason for hiding this comment

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

initialize in constructor


return cd.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{}
cd, err := codec.NewCodec(commitCodecConfig)
if err != nil {
return report, err
}

err = cd.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)
96 changes: 96 additions & 0 deletions core/capabilities/ccip/ccipevm/commitcodecv2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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 := NewCommitPluginCodecV2()
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 := NewCommitPluginCodecV2()
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 := NewCommitPluginCodecV2()
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
126 changes: 126 additions & 0 deletions core/capabilities/ccip/ccipevm/executecodecv2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package ccipevm

import (
"context"
"errors"
"fmt"

commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec"
"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{}

func NewExecutePluginCodecV2() *ExecutePluginCodecV2 {
return &ExecutePluginCodecV2{}
}

func validate(report cciptypes.ExecutePluginReport) error {
for _, chainReport := range report.ChainReports {
if chainReport.ProofFlagBits.IsEmpty() {
return fmt.Errorf("proof flag bits are empty")

Check failure on line 45 in core/capabilities/ccip/ccipevm/executecodecv2.go

View workflow job for this annotation

GitHub Actions / lint

fmt.Errorf can be replaced with errors.New (perfsprint)
}

evmProofs := make([][32]byte, 0, len(chainReport.Proofs))
for _, proof := range chainReport.Proofs {
evmProofs = append(evmProofs, proof)

Check failure on line 50 in core/capabilities/ccip/ccipevm/executecodecv2.go

View workflow job for this annotation

GitHub Actions / lint

SA4010: this result of append is never used, except maybe in other appends (staticcheck)
}

for _, message := range chainReport.Messages {
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
}

cd, err := codec.NewCodec(execCodecConfig)
if err != nil {
return nil, err
}

return cd.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{}
cd, err := codec.NewCodec(execCodecConfig)
if err != nil {
return report, err
}

err = cd.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