From 166556a496b1421032e85bf8a80f017c374b22bc Mon Sep 17 00:00:00 2001 From: Will Winder Date: Tue, 8 Oct 2024 12:14:54 -0400 Subject: [PATCH 1/2] Direct v2 copy of types. --- pkg/types/ccipocr3/v2/common_types.go | 148 ++++++++++ pkg/types/ccipocr3/v2/common_types_test.go | 136 ++++++++++ pkg/types/ccipocr3/v2/generic_types.go | 192 +++++++++++++ pkg/types/ccipocr3/v2/generic_types_test.go | 253 ++++++++++++++++++ pkg/types/ccipocr3/v2/interfaces.go | 40 +++ pkg/types/ccipocr3/v2/plugin_commit_types.go | 73 +++++ .../ccipocr3/v2/plugin_commit_types_test.go | 140 ++++++++++ pkg/types/ccipocr3/v2/plugin_execute_types.go | 13 + pkg/types/ccipocr3/v2/rmn_types.go | 30 +++ pkg/types/ccipocr3/v2/v2/common_types.go | 148 ++++++++++ pkg/types/ccipocr3/v2/v2/common_types_test.go | 136 ++++++++++ pkg/types/ccipocr3/v2/v2/generic_types.go | 192 +++++++++++++ .../ccipocr3/v2/v2/generic_types_test.go | 253 ++++++++++++++++++ pkg/types/ccipocr3/v2/v2/interfaces.go | 40 +++ .../ccipocr3/v2/v2/plugin_commit_types.go | 73 +++++ .../v2/v2/plugin_commit_types_test.go | 140 ++++++++++ .../ccipocr3/v2/v2/plugin_execute_types.go | 13 + pkg/types/ccipocr3/v2/v2/rmn_types.go | 30 +++ 18 files changed, 2050 insertions(+) create mode 100644 pkg/types/ccipocr3/v2/common_types.go create mode 100644 pkg/types/ccipocr3/v2/common_types_test.go create mode 100644 pkg/types/ccipocr3/v2/generic_types.go create mode 100644 pkg/types/ccipocr3/v2/generic_types_test.go create mode 100644 pkg/types/ccipocr3/v2/interfaces.go create mode 100644 pkg/types/ccipocr3/v2/plugin_commit_types.go create mode 100644 pkg/types/ccipocr3/v2/plugin_commit_types_test.go create mode 100644 pkg/types/ccipocr3/v2/plugin_execute_types.go create mode 100644 pkg/types/ccipocr3/v2/rmn_types.go create mode 100644 pkg/types/ccipocr3/v2/v2/common_types.go create mode 100644 pkg/types/ccipocr3/v2/v2/common_types_test.go create mode 100644 pkg/types/ccipocr3/v2/v2/generic_types.go create mode 100644 pkg/types/ccipocr3/v2/v2/generic_types_test.go create mode 100644 pkg/types/ccipocr3/v2/v2/interfaces.go create mode 100644 pkg/types/ccipocr3/v2/v2/plugin_commit_types.go create mode 100644 pkg/types/ccipocr3/v2/v2/plugin_commit_types_test.go create mode 100644 pkg/types/ccipocr3/v2/v2/plugin_execute_types.go create mode 100644 pkg/types/ccipocr3/v2/v2/rmn_types.go diff --git a/pkg/types/ccipocr3/v2/common_types.go b/pkg/types/ccipocr3/v2/common_types.go new file mode 100644 index 000000000..bc1048959 --- /dev/null +++ b/pkg/types/ccipocr3/v2/common_types.go @@ -0,0 +1,148 @@ +package ccipocr3 + +import ( + "encoding/hex" + "fmt" + "math/big" + "strings" +) + +type Bytes []byte + +func NewBytesFromString(s string) (Bytes, error) { + if len(s) < 2 { + return nil, fmt.Errorf("Bytes must be of at least length 2 (i.e, '0x' prefix): %s", s) + } + + if !strings.HasPrefix(s, "0x") { + return nil, fmt.Errorf("Bytes must start with '0x' prefix: %s", s) + } + + b, err := hex.DecodeString(s[2:]) + if err != nil { + return nil, fmt.Errorf("failed to decode hex: %w", err) + } + + return Bytes(b), nil +} + +func (b Bytes) String() string { + return "0x" + hex.EncodeToString(b) +} + +func (b Bytes) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf(`"%s"`, b.String())), nil +} + +func (b *Bytes) UnmarshalJSON(data []byte) error { + v := string(data) + if len(v) < 2 { + return fmt.Errorf("Bytes must be of at least length 2 (i.e, '0x' prefix): %s", v) + } + + // trim the start and end double quotes + v = v[1 : len(v)-1] + + if !strings.HasPrefix(v, "0x") { + return fmt.Errorf("Bytes must start with '0x' prefix: %s", v) + } + + // Decode everything after the '0x' prefix. + bs, err := hex.DecodeString(v[2:]) + if err != nil { + return fmt.Errorf("failed to decode hex: %w", err) + } + + *b = bs + return nil +} + +type Bytes32 [32]byte + +func NewBytes32FromString(s string) (Bytes32, error) { + if len(s) < 2 { + return Bytes32{}, fmt.Errorf("Bytes32 must be of at least length 2 (i.e, '0x' prefix): %s", s) + } + + if !strings.HasPrefix(s, "0x") { + return Bytes32{}, fmt.Errorf("Bytes32 must start with '0x' prefix: %s", s) + } + + b, err := hex.DecodeString(s[2:]) + if err != nil { + return Bytes32{}, fmt.Errorf("failed to decode hex: %w", err) + } + + var res Bytes32 + copy(res[:], b) + return res, nil +} + +func (b Bytes32) String() string { + return "0x" + hex.EncodeToString(b[:]) +} + +func (b Bytes32) IsEmpty() bool { + return b == Bytes32{} +} + +func (b Bytes32) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf(`"%s"`, b.String())), nil +} + +func (b *Bytes32) UnmarshalJSON(data []byte) error { + v := string(data) + if len(v) < 4 { + return fmt.Errorf("invalid MerkleRoot: %s", v) + } + + bCp, err := hex.DecodeString(v[1 : len(v)-1][2:]) + if err != nil { + return err + } + + copy(b[:], bCp) + return nil +} + +type BigInt struct { + *big.Int +} + +func NewBigInt(i *big.Int) BigInt { + return BigInt{Int: i} +} + +func NewBigIntFromInt64(i int64) BigInt { + return BigInt{Int: big.NewInt(i)} +} + +func (b BigInt) MarshalJSON() ([]byte, error) { + if b.Int == nil { + return []byte("null"), nil + } + return []byte(fmt.Sprintf(`"%s"`, b.String())), nil +} + +func (b *BigInt) UnmarshalJSON(p []byte) error { + if string(p) == "null" { + return nil + } + + if len(p) < 2 { + return fmt.Errorf("invalid BigInt: %s", p) + } + p = p[1 : len(p)-1] + + z := big.NewInt(0) + _, ok := z.SetString(string(p), 10) + if !ok { + return fmt.Errorf("not a valid big integer: %s", p) + } + b.Int = z + return nil +} + +func (b BigInt) IsEmpty() bool { + return b.Int == nil +} diff --git a/pkg/types/ccipocr3/v2/common_types_test.go b/pkg/types/ccipocr3/v2/common_types_test.go new file mode 100644 index 000000000..9403a6093 --- /dev/null +++ b/pkg/types/ccipocr3/v2/common_types_test.go @@ -0,0 +1,136 @@ +package ccipocr3 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewBytes32FromString(t *testing.T) { + testCases := []struct { + name string + input string + expected Bytes32 + expErr bool + }{ + { + name: "valid input", + input: "0x200000000000000000000000", + expected: Bytes32{0x20, 0}, + expErr: false, + }, + { + name: "invalid hex characters", + input: "lrfv", + expected: Bytes32{}, + expErr: true, + }, + { + name: "invalid input, no 0x prefix", + input: "200000000000000000000000", + expected: Bytes32{}, + expErr: true, + }, + { + name: "invalid input, not enough hex chars", + input: "0x2", + expected: Bytes32{}, + expErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual, err := NewBytes32FromString(tc.input) + if tc.expErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tc.expected, actual) + }) + } +} + +func TestBytes32_IsEmpty(t *testing.T) { + testCases := []struct { + name string + input Bytes32 + expected bool + }{ + { + name: "empty", + input: Bytes32{}, + expected: true, + }, + { + name: "not empty", + input: Bytes32{0x20, 0}, + expected: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, tc.input.IsEmpty()) + }) + } +} + +func TestNewBytesFromString(t *testing.T) { + tests := []struct { + name string + arg string + want Bytes + wantErr bool + }{ + { + "valid input", + "0x20", + Bytes{0x20}, + false, + }, + { + "valid long input", + "0x2010201020", + Bytes{0x20, 0x10, 0x20, 0x10, 0x20}, + false, + }, + { + "invalid input", + "0", + nil, + true, + }, + { + "invalid input, not enough hex chars", + "0x2", + nil, + true, + }, + { + "invalid input, no 0x prefix", + "20", + nil, + true, + }, + { + "invalid hex characters", + "0x2g", + nil, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewBytesFromString(tt.arg) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + }) + } +} diff --git a/pkg/types/ccipocr3/v2/generic_types.go b/pkg/types/ccipocr3/v2/generic_types.go new file mode 100644 index 000000000..dcf37f73d --- /dev/null +++ b/pkg/types/ccipocr3/v2/generic_types.go @@ -0,0 +1,192 @@ +package ccipocr3 + +import ( + "encoding/json" + "fmt" + "math/big" + "strconv" + + "github.com/smartcontractkit/libocr/offchainreporting2plus/types" +) + +type TokenPrice struct { + TokenID types.Account `json:"tokenID"` + Price BigInt `json:"price"` +} + +func NewTokenPrice(tokenID types.Account, price *big.Int) TokenPrice { + return TokenPrice{ + TokenID: tokenID, + Price: BigInt{price}, + } +} + +type GasPriceChain struct { + GasPrice BigInt `json:"gasPrice"` + ChainSel ChainSelector `json:"chainSel"` +} + +func NewGasPriceChain(gasPrice *big.Int, chainSel ChainSelector) GasPriceChain { + return GasPriceChain{ + GasPrice: NewBigInt(gasPrice), + ChainSel: chainSel, + } +} + +type SeqNum uint64 + +func (s SeqNum) String() string { + return strconv.FormatUint(uint64(s), 10) +} + +func NewSeqNumRange(start, end SeqNum) SeqNumRange { + return SeqNumRange{start, end} +} + +// SeqNumRange defines an inclusive range of sequence numbers. +type SeqNumRange [2]SeqNum + +func (s SeqNumRange) Start() SeqNum { + return s[0] +} + +func (s SeqNumRange) End() SeqNum { + return s[1] +} + +func (s *SeqNumRange) SetStart(v SeqNum) { + s[0] = v +} + +func (s *SeqNumRange) SetEnd(v SeqNum) { + s[1] = v +} + +// Limit returns a range limited up to n elements by truncating the end if necessary. +// Example: [1 -> 10].Limit(5) => [1 -> 5] +func (s *SeqNumRange) Limit(n uint64) SeqNumRange { + limitedRange := NewSeqNumRange(s.Start(), s.End()) + + numElems := s.End() - s.Start() + 1 + if numElems <= 0 { + return limitedRange + } + + if uint64(numElems) > n { + newEnd := limitedRange.Start() + SeqNum(n) - 1 + if newEnd > limitedRange.End() { // overflow - do nothing + return limitedRange + } + limitedRange.SetEnd(newEnd) + } + + return limitedRange +} + +// Overlaps returns true if the two ranges overlap. +func (s SeqNumRange) Overlaps(other SeqNumRange) bool { + return s.Start() <= other.End() && other.Start() <= s.End() +} + +// Contains returns true if the range contains the given sequence number. +func (s SeqNumRange) Contains(seq SeqNum) bool { + return s.Start() <= seq && seq <= s.End() +} + +func (s SeqNumRange) String() string { + return fmt.Sprintf("[%d -> %d]", s[0], s[1]) +} + +type ChainSelector uint64 + +func (c ChainSelector) String() string { + return fmt.Sprintf("ChainSelector(%d)", c) +} + +// Message is the generic Any2Any message type for CCIP messages. +// It represents, in particular, a message emitted by a CCIP onramp. +// The message is expected to be consumed by a CCIP offramp after +// translating it into the appropriate format for the destination chain. +type Message struct { + // Header is the family-agnostic header for OnRamp and OffRamp messages. + // This is always set on all CCIP messages. + Header RampMessageHeader `json:"header"` + // Sender address on the source chain. + // i.e if the source chain is EVM, this is an abi-encoded EVM address. + Sender Bytes `json:"sender"` + // Data is the arbitrary data payload supplied by the message sender. + Data Bytes `json:"data"` + // Receiver is the receiver address on the destination chain. + // This is encoded in the destination chain family specific encoding. + // i.e if the destination is EVM, this is abi.encode(receiver). + Receiver Bytes `json:"receiver"` + // ExtraArgs is destination-chain specific extra args, such as the gasLimit for EVM chains. + ExtraArgs Bytes `json:"extraArgs"` + // FeeToken is the fee token address. + // i.e if the source chain is EVM, len(FeeToken) == 20 (i.e, is not abi-encoded). + FeeToken Bytes `json:"feeToken"` + // FeeTokenAmount is the amount of fee tokens paid. + FeeTokenAmount BigInt `json:"feeTokenAmount"` + // FeeValueJuels is the fee amount in Juels + FeeValueJuels BigInt `json:"feeValueJuels"` + // TokenAmounts is the array of tokens and amounts to transfer. + TokenAmounts []RampTokenAmount `json:"tokenAmounts"` +} + +func (c Message) String() string { + js, _ := json.Marshal(c) + return string(js) +} + +// RampMessageHeader is the family-agnostic header for OnRamp and OffRamp messages. +// The MessageID is not expected to match MsgHash, since it may originate from a different +// ramp family. +type RampMessageHeader struct { + // MessageID is a unique identifier for the message, it should be unique across all chains. + // It is generated on the chain that the CCIP send is requested (i.e. the source chain of a message). + MessageID Bytes32 `json:"messageId"` + // SourceChainSelector is the chain selector of the chain that the message originated from. + SourceChainSelector ChainSelector `json:"sourceChainSelector,string"` + // DestChainSelector is the chain selector of the chain that the message is destined for. + DestChainSelector ChainSelector `json:"destChainSelector,string"` + // SequenceNumber is an auto-incrementing sequence number for the message. + // Not unique across lanes. + SequenceNumber SeqNum `json:"seqNum,string"` + // Nonce is the nonce for this lane for this sender, not unique across senders/lanes + Nonce uint64 `json:"nonce"` + + // MsgHash is the hash of all the message fields. + // NOTE: The field is expected to be empty, and will be populated by the plugin using the MsgHasher interface. + MsgHash Bytes32 `json:"msgHash"` // populated + + // OnRamp is the address of the onramp that sent the message. + // NOTE: This is populated by the ccip reader. Not emitted explicitly onchain. + OnRamp Bytes `json:"onRamp"` +} + +// RampTokenAmount represents the family-agnostic token amounts used for both OnRamp & OffRamp messages. +type RampTokenAmount struct { + // SourcePoolAddress is the source pool address, encoded according to source family native encoding scheme. + // This value is trusted as it was obtained through the onRamp. It can be relied upon by the destination + // pool to validate the source pool. + SourcePoolAddress Bytes `json:"sourcePoolAddress"` + + // DestTokenAddress is the address of the destination token, abi encoded in the case of EVM chains. + // This value is UNTRUSTED as any pool owner can return whatever value they want. + DestTokenAddress Bytes `json:"destTokenAddress"` + + // ExtraData is optional pool data to be transferred to the destination chain. Be default this is capped at + // CCIP_LOCK_OR_BURN_V1_RET_BYTES bytes. If more data is required, the TokenTransferFeeConfig.destBytesOverhead + // has to be set for the specific token. + ExtraData Bytes `json:"extraData"` + + // Amount is the amount of tokens to be transferred. + Amount BigInt `json:"amount"` + + // DestExecData is destination chain specific execution data encoded in bytes. + // For an EVM destination, it consists of the amount of gas available for the releaseOrMint + // and transfer calls made by the offRamp. + // NOTE: this must be decoded before providing it as an execution input to the destination chain + // or hashing it. See Internal._hash(Any2EVMRampMessage) for more details as an example. + DestExecData Bytes `json:"destExecData"` +} diff --git a/pkg/types/ccipocr3/v2/generic_types_test.go b/pkg/types/ccipocr3/v2/generic_types_test.go new file mode 100644 index 000000000..af94cca92 --- /dev/null +++ b/pkg/types/ccipocr3/v2/generic_types_test.go @@ -0,0 +1,253 @@ +package ccipocr3 + +import ( + "encoding/json" + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSeqNumRange(t *testing.T) { + t.Run("base", func(t *testing.T) { + rng := NewSeqNumRange(1, 2) + assert.Equal(t, SeqNum(1), rng.Start()) + assert.Equal(t, SeqNum(2), rng.End()) + }) + + t.Run("empty", func(t *testing.T) { + rng := SeqNumRange{} + assert.Equal(t, SeqNum(0), rng.Start()) + assert.Equal(t, SeqNum(0), rng.End()) + }) + + t.Run("override start and end", func(t *testing.T) { + rng := NewSeqNumRange(1, 2) + rng.SetStart(10) + rng.SetEnd(20) + assert.Equal(t, SeqNum(10), rng.Start()) + assert.Equal(t, SeqNum(20), rng.End()) + }) + + t.Run("string", func(t *testing.T) { + assert.Equal(t, "[1 -> 2]", NewSeqNumRange(1, 2).String()) + assert.Equal(t, "[0 -> 0]", SeqNumRange{}.String()) + }) +} + +func TestSeqNumRange_Overlap(t *testing.T) { + testCases := []struct { + name string + r1 SeqNumRange + r2 SeqNumRange + exp bool + }{ + {"OverlapMiddle", SeqNumRange{5, 10}, SeqNumRange{8, 12}, true}, + {"OverlapStart", SeqNumRange{5, 10}, SeqNumRange{10, 15}, true}, + {"OverlapEnd", SeqNumRange{5, 10}, SeqNumRange{0, 5}, true}, + {"NoOverlapBefore", SeqNumRange{5, 10}, SeqNumRange{0, 4}, false}, + {"NoOverlapAfter", SeqNumRange{5, 10}, SeqNumRange{11, 15}, false}, + {"SameRange", SeqNumRange{5, 10}, SeqNumRange{5, 10}, true}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.exp, tc.r1.Overlaps(tc.r2)) + }) + } +} + +func TestSeqNumRange_Contains(t *testing.T) { + tests := []struct { + name string + r SeqNumRange + seq SeqNum + expected bool + }{ + {"ContainsMiddle", SeqNumRange{5, 10}, SeqNum(7), true}, + {"ContainsStart", SeqNumRange{5, 10}, SeqNum(5), true}, + {"ContainsEnd", SeqNumRange{5, 10}, SeqNum(10), true}, + {"BeforeRange", SeqNumRange{5, 10}, SeqNum(4), false}, + {"AfterRange", SeqNumRange{5, 10}, SeqNum(11), false}, + {"EmptyRange", SeqNumRange{5, 5}, SeqNum(5), true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.r.Contains(tt.seq)) + }) + } +} + +func TestSeqNumRangeLimit(t *testing.T) { + testCases := []struct { + name string + rng SeqNumRange + n uint64 + want SeqNumRange + }{ + { + name: "no truncation", + rng: NewSeqNumRange(0, 10), + n: 11, + want: NewSeqNumRange(0, 10), + }, + { + name: "no truncation 2", + rng: NewSeqNumRange(100, 110), + n: 11, + want: NewSeqNumRange(100, 110), + }, + { + name: "truncation", + rng: NewSeqNumRange(0, 10), + n: 10, + want: NewSeqNumRange(0, 9), + }, + { + name: "truncation 2", + rng: NewSeqNumRange(100, 110), + n: 10, + want: NewSeqNumRange(100, 109), + }, + { + name: "empty", + rng: NewSeqNumRange(0, 0), + n: 0, + want: NewSeqNumRange(0, 0), + }, + { + name: "wrong range", + rng: NewSeqNumRange(20, 15), + n: 3, + want: NewSeqNumRange(20, 15), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := tc.rng.Limit(tc.n) + if got != tc.want { + t.Errorf("SeqNumRangeLimit(%v, %v) = %v; want %v", tc.rng, tc.n, got, tc.want) + } + }) + } +} + +func TestCCIPMsg_String(t *testing.T) { + tests := []struct { + name string + c Message + expected string + }{ + { + "base", + Message{ + Header: RampMessageHeader{ + MessageID: mustNewBytes32(t, "0x01"), + SourceChainSelector: ChainSelector(1), + DestChainSelector: ChainSelector(2), + SequenceNumber: 2, + Nonce: 1, + + MsgHash: mustNewBytes32(t, "0x23"), + OnRamp: mustNewBytes(t, "0x04D4cC5972ad487F71b85654d48b27D32b13a22F"), + }, + }, + `{"header":{"messageId":"0x0100000000000000000000000000000000000000000000000000000000000000","sourceChainSelector":"1","destChainSelector":"2","seqNum":"2","nonce":1,"msgHash":"0x2300000000000000000000000000000000000000000000000000000000000000","onRamp":"0x04d4cc5972ad487f71b85654d48b27d32b13a22f"},"sender":"0x","data":"0x","receiver":"0x","extraArgs":"0x","feeToken":"0x","feeTokenAmount":null,"feeValueJuels":null,"tokenAmounts":null}`, + }, + { + "with evm ramp message", + Message{ + Header: RampMessageHeader{ + MessageID: mustNewBytes32(t, "0x01"), + SourceChainSelector: ChainSelector(1), + DestChainSelector: ChainSelector(2), + SequenceNumber: 2, + Nonce: 1, + + MsgHash: mustNewBytes32(t, "0x23"), + OnRamp: mustNewBytes(t, "0x04D4cC5972ad487F71b85654d48b27D32b13a22F"), + }, + Sender: mustNewBytes(t, "0x04D4cC5972ad487F71b85654d48b27D32b13a22F"), + Receiver: mustNewBytes(t, "0x101112131415"), // simulate a non-evm receiver + Data: []byte("some data"), + ExtraArgs: []byte("extra args"), + FeeToken: mustNewBytes(t, "0xB5fCC870d2aC8745054b4ba99B1f176B93382162"), + FeeTokenAmount: BigInt{Int: big.NewInt(1000)}, + FeeValueJuels: BigInt{Int: big.NewInt(287)}, + TokenAmounts: []RampTokenAmount{ + { + SourcePoolAddress: mustNewBytes(t, "0x3E8456720B88A1DAdce8E2808C9Bf73dfFFd807c"), + DestTokenAddress: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, // simulate a non-evm token + ExtraData: []byte("extra token data"), + Amount: BigInt{Int: big.NewInt(2000)}, + DestExecData: []byte("extra token data"), + }, + }, + }, + `{"header":{"messageId":"0x0100000000000000000000000000000000000000000000000000000000000000","sourceChainSelector":"1","destChainSelector":"2","seqNum":"2","nonce":1,"msgHash":"0x2300000000000000000000000000000000000000000000000000000000000000","onRamp":"0x04d4cc5972ad487f71b85654d48b27d32b13a22f"},"sender":"0x04d4cc5972ad487f71b85654d48b27d32b13a22f","data":"0x736f6d652064617461","receiver":"0x101112131415","extraArgs":"0x65787472612061726773","feeToken":"0xb5fcc870d2ac8745054b4ba99b1f176b93382162","feeTokenAmount":"1000","feeValueJuels":"287","tokenAmounts":[{"sourcePoolAddress":"0x3e8456720b88a1dadce8e2808c9bf73dfffd807c","destTokenAddress":"0x0102030405060708090a","extraData":"0x657874726120746f6b656e2064617461","amount":"2000","destExecData":"0x657874726120746f6b656e2064617461"}]}`, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, test.c.String()) + }) + } +} + +func TestNewTokenPrice(t *testing.T) { + t.Run("base", func(t *testing.T) { + tp := NewTokenPrice("link", big.NewInt(1000)) + assert.Equal(t, "link", string(tp.TokenID)) + assert.Equal(t, uint64(1000), tp.Price.Int.Uint64()) + }) +} + +func TestNewGasPriceChain(t *testing.T) { + t.Run("base", func(t *testing.T) { + gpc := NewGasPriceChain(big.NewInt(1000), ChainSelector(1)) + assert.Equal(t, uint64(1000), (gpc.GasPrice).Uint64()) + assert.Equal(t, ChainSelector(1), gpc.ChainSel) + }) +} + +func TestMerkleRoot(t *testing.T) { + t.Run("str", func(t *testing.T) { + mr := Bytes32([32]byte{1}) + assert.Equal(t, "0x0100000000000000000000000000000000000000000000000000000000000000", mr.String()) + }) + + t.Run("json", func(t *testing.T) { + mr := Bytes32([32]byte{1}) + b, err := json.Marshal(mr) + assert.NoError(t, err) + assert.Equal(t, `"0x0100000000000000000000000000000000000000000000000000000000000000"`, string(b)) + + mr2 := Bytes32{} + err = json.Unmarshal(b, &mr2) + assert.NoError(t, err) + assert.Equal(t, mr, mr2) + + mr3 := Bytes32{} + err = json.Unmarshal([]byte(`"123"`), &mr3) + assert.Error(t, err) + + err = json.Unmarshal([]byte(`""`), &mr3) + assert.Error(t, err) + }) +} + +func mustNewBytes32(t *testing.T, s string) Bytes32 { + b32, err := NewBytes32FromString(s) + require.NoError(t, err) + return b32 +} + +func mustNewBytes(t *testing.T, s string) Bytes { + b, err := NewBytesFromString(s) + require.NoError(t, err) + return b +} diff --git a/pkg/types/ccipocr3/v2/interfaces.go b/pkg/types/ccipocr3/v2/interfaces.go new file mode 100644 index 000000000..3a263666e --- /dev/null +++ b/pkg/types/ccipocr3/v2/interfaces.go @@ -0,0 +1,40 @@ +package ccipocr3 + +import ( + "context" +) + +type CommitPluginCodec interface { + Encode(context.Context, CommitPluginReport) ([]byte, error) + Decode(context.Context, []byte) (CommitPluginReport, error) +} + +type ExecutePluginCodec interface { + Encode(context.Context, ExecutePluginReport) ([]byte, error) + Decode(context.Context, []byte) (ExecutePluginReport, error) +} + +type MessageHasher interface { + Hash(context.Context, Message) (Bytes32, error) +} + +// RMNCrypto provides a chain-agnostic interface for verifying RMN signatures. +// For example, on EVM, RMN reports are abi-encoded prior to being signed. +// On Solana, they would be borsh encoded instead, etc. +type RMNCrypto interface { + // VerifyReportSignatures verifies each provided signature against the provided report and the signer addresses. + // If any signature is invalid (no matching signer address is found), an error is returned immediately. + VerifyReportSignatures( + ctx context.Context, + sigs []RMNECDSASignature, + report RMNReport, + signerAddresses []Bytes, + ) error +} + +// TokenDataEncoder is a generic interface for encoding offchain token data for different chain families. +// Right now it supports only USDC/CCTP, but every new token that requires offchain data processsing +// should be added to that interface and implemented in the downstream repositories (e.g. chainlink-ccip, chainlink). +type TokenDataEncoder interface { + EncodeUSDC(ctx context.Context, message Bytes, attestation Bytes) (Bytes, error) +} diff --git a/pkg/types/ccipocr3/v2/plugin_commit_types.go b/pkg/types/ccipocr3/v2/plugin_commit_types.go new file mode 100644 index 000000000..665821faf --- /dev/null +++ b/pkg/types/ccipocr3/v2/plugin_commit_types.go @@ -0,0 +1,73 @@ +package ccipocr3 + +import "bytes" + +// CommitPluginReport contains the necessary information to commit CCIP +// messages from potentially many source chains, to a single destination chain. +// +// It must consist of either: +// +// 1. a non-empty MerkleRoots array, or +// 2. a non-empty PriceUpdates array +// +// If neither of the above is provided the report is considered empty and should +// not be transmitted on-chain. +// +// In the event the MerkleRoots array is non-empty, it may also contain +// RMNSignatures, if RMN is configured for some lanes involved in the commitment. +// A report with RMN signatures but without merkle roots is invalid. +type CommitPluginReport struct { + MerkleRoots []MerkleRootChain `json:"merkleRoots"` + PriceUpdates PriceUpdates `json:"priceUpdates"` + + // RMNSignatures are the ECDSA signatures from the RMN signing nodes on the RMNReport structure. + // For more details see the contract here: https://github.com/smartcontractkit/chainlink/blob/7ba0f37134a618375542079ff1805fe2224d7916/contracts/src/v0.8/ccip/interfaces/IRMNV2.sol#L8-L12 + RMNSignatures []RMNECDSASignature `json:"rmnSignatures"` + + // RMNRawVs is a 256-bit bitmap. A bit is set if the v value of the signature is 28, + // and unset if the v value is 27. + // Note that this implies that the maximum number of RMN signatures is 256. + RMNRawVs BigInt `json:"rmnRawVs"` +} + +// IsEmpty returns true if the CommitPluginReport is empty +func (r CommitPluginReport) IsEmpty() bool { + return len(r.MerkleRoots) == 0 && + len(r.PriceUpdates.TokenPriceUpdates) == 0 && + len(r.PriceUpdates.GasPriceUpdates) == 0 && + len(r.RMNSignatures) == 0 +} + +// MerkleRootChain Mirroring https://github.com/smartcontractkit/chainlink/blob/cd5c78959575f593b27fd83d8766086d0c678487/contracts/src/v0.8/ccip/libraries/Internal.sol#L356-L362 +type MerkleRootChain struct { + ChainSel ChainSelector `json:"chain"` + OnRampAddress Bytes `json:"onRampAddress"` + SeqNumsRange SeqNumRange `json:"seqNumsRange"` + MerkleRoot Bytes32 `json:"merkleRoot"` +} + +func (m MerkleRootChain) Equals(other MerkleRootChain) bool { + return m.ChainSel == other.ChainSel && + bytes.Equal(m.OnRampAddress, other.OnRampAddress) && + m.SeqNumsRange == other.SeqNumsRange && + m.MerkleRoot == other.MerkleRoot +} + +func NewMerkleRootChain( + chainSel ChainSelector, + onRampAddress Bytes, + seqNumsRange SeqNumRange, + merkleRoot Bytes32, +) MerkleRootChain { + return MerkleRootChain{ + ChainSel: chainSel, + OnRampAddress: onRampAddress, + SeqNumsRange: seqNumsRange, + MerkleRoot: merkleRoot, + } +} + +type PriceUpdates struct { + TokenPriceUpdates []TokenPrice `json:"tokenPriceUpdates"` + GasPriceUpdates []GasPriceChain `json:"gasPriceUpdates"` +} diff --git a/pkg/types/ccipocr3/v2/plugin_commit_types_test.go b/pkg/types/ccipocr3/v2/plugin_commit_types_test.go new file mode 100644 index 000000000..f49b9ef87 --- /dev/null +++ b/pkg/types/ccipocr3/v2/plugin_commit_types_test.go @@ -0,0 +1,140 @@ +package ccipocr3 + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCommitPluginReport(t *testing.T) { + t.Run("is empty", func(t *testing.T) { + r := CommitPluginReport{} + assert.True(t, r.IsEmpty()) + }) + + t.Run("is not empty", func(t *testing.T) { + r := CommitPluginReport{ + MerkleRoots: make([]MerkleRootChain, 1), + } + assert.False(t, r.IsEmpty()) + + r = CommitPluginReport{ + PriceUpdates: PriceUpdates{ + TokenPriceUpdates: make([]TokenPrice, 1), + GasPriceUpdates: make([]GasPriceChain, 1), + }, + } + assert.False(t, r.IsEmpty()) + + r = CommitPluginReport{ + RMNSignatures: make([]RMNECDSASignature, 1), + } + assert.False(t, r.IsEmpty()) + + r = CommitPluginReport{ + MerkleRoots: make([]MerkleRootChain, 1), + PriceUpdates: PriceUpdates{ + TokenPriceUpdates: make([]TokenPrice, 1), + GasPriceUpdates: make([]GasPriceChain, 1), + }, + RMNSignatures: make([]RMNECDSASignature, 1), + } + assert.False(t, r.IsEmpty()) + }) +} + +func TestMerkleRootChain_Equals_Structs(t *testing.T) { + tests := []struct { + name string + m1 MerkleRootChain + m2 MerkleRootChain + expected bool + }{ + { + name: "equal MerkleRootChains", + m1: MerkleRootChain{ + ChainSel: ChainSelector(1), + OnRampAddress: []byte{0x01, 0x02}, + SeqNumsRange: SeqNumRange{1, 10}, + MerkleRoot: Bytes32{0x01, 0x02, 0x03}, + }, + m2: MerkleRootChain{ + ChainSel: ChainSelector(1), + OnRampAddress: []byte{0x01, 0x02}, + SeqNumsRange: SeqNumRange{1, 10}, + MerkleRoot: Bytes32{0x01, 0x02, 0x03}, + }, + expected: true, + }, + { + name: "different ChainSel", + m1: MerkleRootChain{ + ChainSel: ChainSelector(1), + OnRampAddress: []byte{0x01, 0x02}, + SeqNumsRange: SeqNumRange{1, 10}, + MerkleRoot: Bytes32{0x01, 0x02, 0x03}, + }, + m2: MerkleRootChain{ + ChainSel: ChainSelector(2), + OnRampAddress: []byte{0x01, 0x02}, + SeqNumsRange: SeqNumRange{1, 10}, + MerkleRoot: Bytes32{0x01, 0x02, 0x03}, + }, + expected: false, + }, + { + name: "different OnRampAddress", + m1: MerkleRootChain{ + ChainSel: ChainSelector(1), + OnRampAddress: []byte{0x01, 0x02}, + SeqNumsRange: SeqNumRange{1, 10}, + MerkleRoot: Bytes32{0x01, 0x02, 0x03}, + }, + m2: MerkleRootChain{ + ChainSel: ChainSelector(1), + OnRampAddress: []byte{0x03, 0x04}, + SeqNumsRange: SeqNumRange{1, 10}, + MerkleRoot: Bytes32{0x01, 0x02, 0x03}, + }, + expected: false, + }, + { + name: "different SeqNumsRange", + m1: MerkleRootChain{ + ChainSel: ChainSelector(1), + OnRampAddress: []byte{0x01, 0x02}, + SeqNumsRange: SeqNumRange{1, 10}, + MerkleRoot: Bytes32{0x01, 0x02, 0x03}, + }, + m2: MerkleRootChain{ + ChainSel: ChainSelector(1), + OnRampAddress: []byte{0x01, 0x02}, + SeqNumsRange: SeqNumRange{2, 20}, + MerkleRoot: Bytes32{0x01, 0x02, 0x03}, + }, + expected: false, + }, + { + name: "different MerkleRoot", + m1: MerkleRootChain{ + ChainSel: ChainSelector(1), + OnRampAddress: []byte{0x01, 0x02}, + SeqNumsRange: SeqNumRange{1, 10}, + MerkleRoot: Bytes32{0x01, 0x02, 0x03}, + }, + m2: MerkleRootChain{ + ChainSel: ChainSelector(1), + OnRampAddress: []byte{0x01, 0x02}, + SeqNumsRange: SeqNumRange{1, 10}, + MerkleRoot: Bytes32{0x04, 0x05, 0x06}, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.m1.Equals(tt.m2)) + }) + } +} diff --git a/pkg/types/ccipocr3/v2/plugin_execute_types.go b/pkg/types/ccipocr3/v2/plugin_execute_types.go new file mode 100644 index 000000000..61e813226 --- /dev/null +++ b/pkg/types/ccipocr3/v2/plugin_execute_types.go @@ -0,0 +1,13 @@ +package ccipocr3 + +type ExecutePluginReport struct { + ChainReports []ExecutePluginReportSingleChain `json:"chainReports"` +} + +type ExecutePluginReportSingleChain struct { + SourceChainSelector ChainSelector `json:"sourceChainSelector"` + Messages []Message `json:"messages"` + OffchainTokenData [][][]byte `json:"offchainTokenData"` + Proofs []Bytes32 `json:"proofs"` + ProofFlagBits BigInt `json:"proofFlagBits"` +} diff --git a/pkg/types/ccipocr3/v2/rmn_types.go b/pkg/types/ccipocr3/v2/rmn_types.go new file mode 100644 index 000000000..8d8839495 --- /dev/null +++ b/pkg/types/ccipocr3/v2/rmn_types.go @@ -0,0 +1,30 @@ +package ccipocr3 + +// RMNReport is the payload that is signed by the RMN nodes, transmitted and verified onchain. +type RMNReport struct { + ReportVersion string // e.g. "RMN_V1_6_ANY2EVM_REPORT". + DestChainID BigInt // If applies, a chain specific id, e.g. evm chain id otherwise empty. + DestChainSelector ChainSelector + RmnRemoteContractAddress Bytes + OfframpAddress Bytes + RmnHomeContractConfigDigest Bytes32 + LaneUpdates []RMNLaneUpdate +} + +// RMNLaneUpdate represents an interval that has been observed by an RMN node. +// It is part of the payload that is signed and transmitted onchain. +type RMNLaneUpdate struct { + SourceChainSelector ChainSelector + OnRampAddress Bytes // (for EVM should be abi-encoded) + MinSeqNr SeqNum + MaxSeqNr SeqNum + MerkleRoot Bytes32 +} + +// RMNECDSASignature represents the signature provided by RMN on the RMNReport structure. +// The V value of the signature is included in the top-level commit report as part of a +// bitmap. +type RMNECDSASignature struct { + R Bytes32 `json:"r"` + S Bytes32 `json:"s"` +} diff --git a/pkg/types/ccipocr3/v2/v2/common_types.go b/pkg/types/ccipocr3/v2/v2/common_types.go new file mode 100644 index 000000000..bc1048959 --- /dev/null +++ b/pkg/types/ccipocr3/v2/v2/common_types.go @@ -0,0 +1,148 @@ +package ccipocr3 + +import ( + "encoding/hex" + "fmt" + "math/big" + "strings" +) + +type Bytes []byte + +func NewBytesFromString(s string) (Bytes, error) { + if len(s) < 2 { + return nil, fmt.Errorf("Bytes must be of at least length 2 (i.e, '0x' prefix): %s", s) + } + + if !strings.HasPrefix(s, "0x") { + return nil, fmt.Errorf("Bytes must start with '0x' prefix: %s", s) + } + + b, err := hex.DecodeString(s[2:]) + if err != nil { + return nil, fmt.Errorf("failed to decode hex: %w", err) + } + + return Bytes(b), nil +} + +func (b Bytes) String() string { + return "0x" + hex.EncodeToString(b) +} + +func (b Bytes) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf(`"%s"`, b.String())), nil +} + +func (b *Bytes) UnmarshalJSON(data []byte) error { + v := string(data) + if len(v) < 2 { + return fmt.Errorf("Bytes must be of at least length 2 (i.e, '0x' prefix): %s", v) + } + + // trim the start and end double quotes + v = v[1 : len(v)-1] + + if !strings.HasPrefix(v, "0x") { + return fmt.Errorf("Bytes must start with '0x' prefix: %s", v) + } + + // Decode everything after the '0x' prefix. + bs, err := hex.DecodeString(v[2:]) + if err != nil { + return fmt.Errorf("failed to decode hex: %w", err) + } + + *b = bs + return nil +} + +type Bytes32 [32]byte + +func NewBytes32FromString(s string) (Bytes32, error) { + if len(s) < 2 { + return Bytes32{}, fmt.Errorf("Bytes32 must be of at least length 2 (i.e, '0x' prefix): %s", s) + } + + if !strings.HasPrefix(s, "0x") { + return Bytes32{}, fmt.Errorf("Bytes32 must start with '0x' prefix: %s", s) + } + + b, err := hex.DecodeString(s[2:]) + if err != nil { + return Bytes32{}, fmt.Errorf("failed to decode hex: %w", err) + } + + var res Bytes32 + copy(res[:], b) + return res, nil +} + +func (b Bytes32) String() string { + return "0x" + hex.EncodeToString(b[:]) +} + +func (b Bytes32) IsEmpty() bool { + return b == Bytes32{} +} + +func (b Bytes32) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf(`"%s"`, b.String())), nil +} + +func (b *Bytes32) UnmarshalJSON(data []byte) error { + v := string(data) + if len(v) < 4 { + return fmt.Errorf("invalid MerkleRoot: %s", v) + } + + bCp, err := hex.DecodeString(v[1 : len(v)-1][2:]) + if err != nil { + return err + } + + copy(b[:], bCp) + return nil +} + +type BigInt struct { + *big.Int +} + +func NewBigInt(i *big.Int) BigInt { + return BigInt{Int: i} +} + +func NewBigIntFromInt64(i int64) BigInt { + return BigInt{Int: big.NewInt(i)} +} + +func (b BigInt) MarshalJSON() ([]byte, error) { + if b.Int == nil { + return []byte("null"), nil + } + return []byte(fmt.Sprintf(`"%s"`, b.String())), nil +} + +func (b *BigInt) UnmarshalJSON(p []byte) error { + if string(p) == "null" { + return nil + } + + if len(p) < 2 { + return fmt.Errorf("invalid BigInt: %s", p) + } + p = p[1 : len(p)-1] + + z := big.NewInt(0) + _, ok := z.SetString(string(p), 10) + if !ok { + return fmt.Errorf("not a valid big integer: %s", p) + } + b.Int = z + return nil +} + +func (b BigInt) IsEmpty() bool { + return b.Int == nil +} diff --git a/pkg/types/ccipocr3/v2/v2/common_types_test.go b/pkg/types/ccipocr3/v2/v2/common_types_test.go new file mode 100644 index 000000000..9403a6093 --- /dev/null +++ b/pkg/types/ccipocr3/v2/v2/common_types_test.go @@ -0,0 +1,136 @@ +package ccipocr3 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewBytes32FromString(t *testing.T) { + testCases := []struct { + name string + input string + expected Bytes32 + expErr bool + }{ + { + name: "valid input", + input: "0x200000000000000000000000", + expected: Bytes32{0x20, 0}, + expErr: false, + }, + { + name: "invalid hex characters", + input: "lrfv", + expected: Bytes32{}, + expErr: true, + }, + { + name: "invalid input, no 0x prefix", + input: "200000000000000000000000", + expected: Bytes32{}, + expErr: true, + }, + { + name: "invalid input, not enough hex chars", + input: "0x2", + expected: Bytes32{}, + expErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual, err := NewBytes32FromString(tc.input) + if tc.expErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tc.expected, actual) + }) + } +} + +func TestBytes32_IsEmpty(t *testing.T) { + testCases := []struct { + name string + input Bytes32 + expected bool + }{ + { + name: "empty", + input: Bytes32{}, + expected: true, + }, + { + name: "not empty", + input: Bytes32{0x20, 0}, + expected: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, tc.input.IsEmpty()) + }) + } +} + +func TestNewBytesFromString(t *testing.T) { + tests := []struct { + name string + arg string + want Bytes + wantErr bool + }{ + { + "valid input", + "0x20", + Bytes{0x20}, + false, + }, + { + "valid long input", + "0x2010201020", + Bytes{0x20, 0x10, 0x20, 0x10, 0x20}, + false, + }, + { + "invalid input", + "0", + nil, + true, + }, + { + "invalid input, not enough hex chars", + "0x2", + nil, + true, + }, + { + "invalid input, no 0x prefix", + "20", + nil, + true, + }, + { + "invalid hex characters", + "0x2g", + nil, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewBytesFromString(tt.arg) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + }) + } +} diff --git a/pkg/types/ccipocr3/v2/v2/generic_types.go b/pkg/types/ccipocr3/v2/v2/generic_types.go new file mode 100644 index 000000000..dcf37f73d --- /dev/null +++ b/pkg/types/ccipocr3/v2/v2/generic_types.go @@ -0,0 +1,192 @@ +package ccipocr3 + +import ( + "encoding/json" + "fmt" + "math/big" + "strconv" + + "github.com/smartcontractkit/libocr/offchainreporting2plus/types" +) + +type TokenPrice struct { + TokenID types.Account `json:"tokenID"` + Price BigInt `json:"price"` +} + +func NewTokenPrice(tokenID types.Account, price *big.Int) TokenPrice { + return TokenPrice{ + TokenID: tokenID, + Price: BigInt{price}, + } +} + +type GasPriceChain struct { + GasPrice BigInt `json:"gasPrice"` + ChainSel ChainSelector `json:"chainSel"` +} + +func NewGasPriceChain(gasPrice *big.Int, chainSel ChainSelector) GasPriceChain { + return GasPriceChain{ + GasPrice: NewBigInt(gasPrice), + ChainSel: chainSel, + } +} + +type SeqNum uint64 + +func (s SeqNum) String() string { + return strconv.FormatUint(uint64(s), 10) +} + +func NewSeqNumRange(start, end SeqNum) SeqNumRange { + return SeqNumRange{start, end} +} + +// SeqNumRange defines an inclusive range of sequence numbers. +type SeqNumRange [2]SeqNum + +func (s SeqNumRange) Start() SeqNum { + return s[0] +} + +func (s SeqNumRange) End() SeqNum { + return s[1] +} + +func (s *SeqNumRange) SetStart(v SeqNum) { + s[0] = v +} + +func (s *SeqNumRange) SetEnd(v SeqNum) { + s[1] = v +} + +// Limit returns a range limited up to n elements by truncating the end if necessary. +// Example: [1 -> 10].Limit(5) => [1 -> 5] +func (s *SeqNumRange) Limit(n uint64) SeqNumRange { + limitedRange := NewSeqNumRange(s.Start(), s.End()) + + numElems := s.End() - s.Start() + 1 + if numElems <= 0 { + return limitedRange + } + + if uint64(numElems) > n { + newEnd := limitedRange.Start() + SeqNum(n) - 1 + if newEnd > limitedRange.End() { // overflow - do nothing + return limitedRange + } + limitedRange.SetEnd(newEnd) + } + + return limitedRange +} + +// Overlaps returns true if the two ranges overlap. +func (s SeqNumRange) Overlaps(other SeqNumRange) bool { + return s.Start() <= other.End() && other.Start() <= s.End() +} + +// Contains returns true if the range contains the given sequence number. +func (s SeqNumRange) Contains(seq SeqNum) bool { + return s.Start() <= seq && seq <= s.End() +} + +func (s SeqNumRange) String() string { + return fmt.Sprintf("[%d -> %d]", s[0], s[1]) +} + +type ChainSelector uint64 + +func (c ChainSelector) String() string { + return fmt.Sprintf("ChainSelector(%d)", c) +} + +// Message is the generic Any2Any message type for CCIP messages. +// It represents, in particular, a message emitted by a CCIP onramp. +// The message is expected to be consumed by a CCIP offramp after +// translating it into the appropriate format for the destination chain. +type Message struct { + // Header is the family-agnostic header for OnRamp and OffRamp messages. + // This is always set on all CCIP messages. + Header RampMessageHeader `json:"header"` + // Sender address on the source chain. + // i.e if the source chain is EVM, this is an abi-encoded EVM address. + Sender Bytes `json:"sender"` + // Data is the arbitrary data payload supplied by the message sender. + Data Bytes `json:"data"` + // Receiver is the receiver address on the destination chain. + // This is encoded in the destination chain family specific encoding. + // i.e if the destination is EVM, this is abi.encode(receiver). + Receiver Bytes `json:"receiver"` + // ExtraArgs is destination-chain specific extra args, such as the gasLimit for EVM chains. + ExtraArgs Bytes `json:"extraArgs"` + // FeeToken is the fee token address. + // i.e if the source chain is EVM, len(FeeToken) == 20 (i.e, is not abi-encoded). + FeeToken Bytes `json:"feeToken"` + // FeeTokenAmount is the amount of fee tokens paid. + FeeTokenAmount BigInt `json:"feeTokenAmount"` + // FeeValueJuels is the fee amount in Juels + FeeValueJuels BigInt `json:"feeValueJuels"` + // TokenAmounts is the array of tokens and amounts to transfer. + TokenAmounts []RampTokenAmount `json:"tokenAmounts"` +} + +func (c Message) String() string { + js, _ := json.Marshal(c) + return string(js) +} + +// RampMessageHeader is the family-agnostic header for OnRamp and OffRamp messages. +// The MessageID is not expected to match MsgHash, since it may originate from a different +// ramp family. +type RampMessageHeader struct { + // MessageID is a unique identifier for the message, it should be unique across all chains. + // It is generated on the chain that the CCIP send is requested (i.e. the source chain of a message). + MessageID Bytes32 `json:"messageId"` + // SourceChainSelector is the chain selector of the chain that the message originated from. + SourceChainSelector ChainSelector `json:"sourceChainSelector,string"` + // DestChainSelector is the chain selector of the chain that the message is destined for. + DestChainSelector ChainSelector `json:"destChainSelector,string"` + // SequenceNumber is an auto-incrementing sequence number for the message. + // Not unique across lanes. + SequenceNumber SeqNum `json:"seqNum,string"` + // Nonce is the nonce for this lane for this sender, not unique across senders/lanes + Nonce uint64 `json:"nonce"` + + // MsgHash is the hash of all the message fields. + // NOTE: The field is expected to be empty, and will be populated by the plugin using the MsgHasher interface. + MsgHash Bytes32 `json:"msgHash"` // populated + + // OnRamp is the address of the onramp that sent the message. + // NOTE: This is populated by the ccip reader. Not emitted explicitly onchain. + OnRamp Bytes `json:"onRamp"` +} + +// RampTokenAmount represents the family-agnostic token amounts used for both OnRamp & OffRamp messages. +type RampTokenAmount struct { + // SourcePoolAddress is the source pool address, encoded according to source family native encoding scheme. + // This value is trusted as it was obtained through the onRamp. It can be relied upon by the destination + // pool to validate the source pool. + SourcePoolAddress Bytes `json:"sourcePoolAddress"` + + // DestTokenAddress is the address of the destination token, abi encoded in the case of EVM chains. + // This value is UNTRUSTED as any pool owner can return whatever value they want. + DestTokenAddress Bytes `json:"destTokenAddress"` + + // ExtraData is optional pool data to be transferred to the destination chain. Be default this is capped at + // CCIP_LOCK_OR_BURN_V1_RET_BYTES bytes. If more data is required, the TokenTransferFeeConfig.destBytesOverhead + // has to be set for the specific token. + ExtraData Bytes `json:"extraData"` + + // Amount is the amount of tokens to be transferred. + Amount BigInt `json:"amount"` + + // DestExecData is destination chain specific execution data encoded in bytes. + // For an EVM destination, it consists of the amount of gas available for the releaseOrMint + // and transfer calls made by the offRamp. + // NOTE: this must be decoded before providing it as an execution input to the destination chain + // or hashing it. See Internal._hash(Any2EVMRampMessage) for more details as an example. + DestExecData Bytes `json:"destExecData"` +} diff --git a/pkg/types/ccipocr3/v2/v2/generic_types_test.go b/pkg/types/ccipocr3/v2/v2/generic_types_test.go new file mode 100644 index 000000000..af94cca92 --- /dev/null +++ b/pkg/types/ccipocr3/v2/v2/generic_types_test.go @@ -0,0 +1,253 @@ +package ccipocr3 + +import ( + "encoding/json" + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSeqNumRange(t *testing.T) { + t.Run("base", func(t *testing.T) { + rng := NewSeqNumRange(1, 2) + assert.Equal(t, SeqNum(1), rng.Start()) + assert.Equal(t, SeqNum(2), rng.End()) + }) + + t.Run("empty", func(t *testing.T) { + rng := SeqNumRange{} + assert.Equal(t, SeqNum(0), rng.Start()) + assert.Equal(t, SeqNum(0), rng.End()) + }) + + t.Run("override start and end", func(t *testing.T) { + rng := NewSeqNumRange(1, 2) + rng.SetStart(10) + rng.SetEnd(20) + assert.Equal(t, SeqNum(10), rng.Start()) + assert.Equal(t, SeqNum(20), rng.End()) + }) + + t.Run("string", func(t *testing.T) { + assert.Equal(t, "[1 -> 2]", NewSeqNumRange(1, 2).String()) + assert.Equal(t, "[0 -> 0]", SeqNumRange{}.String()) + }) +} + +func TestSeqNumRange_Overlap(t *testing.T) { + testCases := []struct { + name string + r1 SeqNumRange + r2 SeqNumRange + exp bool + }{ + {"OverlapMiddle", SeqNumRange{5, 10}, SeqNumRange{8, 12}, true}, + {"OverlapStart", SeqNumRange{5, 10}, SeqNumRange{10, 15}, true}, + {"OverlapEnd", SeqNumRange{5, 10}, SeqNumRange{0, 5}, true}, + {"NoOverlapBefore", SeqNumRange{5, 10}, SeqNumRange{0, 4}, false}, + {"NoOverlapAfter", SeqNumRange{5, 10}, SeqNumRange{11, 15}, false}, + {"SameRange", SeqNumRange{5, 10}, SeqNumRange{5, 10}, true}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.exp, tc.r1.Overlaps(tc.r2)) + }) + } +} + +func TestSeqNumRange_Contains(t *testing.T) { + tests := []struct { + name string + r SeqNumRange + seq SeqNum + expected bool + }{ + {"ContainsMiddle", SeqNumRange{5, 10}, SeqNum(7), true}, + {"ContainsStart", SeqNumRange{5, 10}, SeqNum(5), true}, + {"ContainsEnd", SeqNumRange{5, 10}, SeqNum(10), true}, + {"BeforeRange", SeqNumRange{5, 10}, SeqNum(4), false}, + {"AfterRange", SeqNumRange{5, 10}, SeqNum(11), false}, + {"EmptyRange", SeqNumRange{5, 5}, SeqNum(5), true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.r.Contains(tt.seq)) + }) + } +} + +func TestSeqNumRangeLimit(t *testing.T) { + testCases := []struct { + name string + rng SeqNumRange + n uint64 + want SeqNumRange + }{ + { + name: "no truncation", + rng: NewSeqNumRange(0, 10), + n: 11, + want: NewSeqNumRange(0, 10), + }, + { + name: "no truncation 2", + rng: NewSeqNumRange(100, 110), + n: 11, + want: NewSeqNumRange(100, 110), + }, + { + name: "truncation", + rng: NewSeqNumRange(0, 10), + n: 10, + want: NewSeqNumRange(0, 9), + }, + { + name: "truncation 2", + rng: NewSeqNumRange(100, 110), + n: 10, + want: NewSeqNumRange(100, 109), + }, + { + name: "empty", + rng: NewSeqNumRange(0, 0), + n: 0, + want: NewSeqNumRange(0, 0), + }, + { + name: "wrong range", + rng: NewSeqNumRange(20, 15), + n: 3, + want: NewSeqNumRange(20, 15), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := tc.rng.Limit(tc.n) + if got != tc.want { + t.Errorf("SeqNumRangeLimit(%v, %v) = %v; want %v", tc.rng, tc.n, got, tc.want) + } + }) + } +} + +func TestCCIPMsg_String(t *testing.T) { + tests := []struct { + name string + c Message + expected string + }{ + { + "base", + Message{ + Header: RampMessageHeader{ + MessageID: mustNewBytes32(t, "0x01"), + SourceChainSelector: ChainSelector(1), + DestChainSelector: ChainSelector(2), + SequenceNumber: 2, + Nonce: 1, + + MsgHash: mustNewBytes32(t, "0x23"), + OnRamp: mustNewBytes(t, "0x04D4cC5972ad487F71b85654d48b27D32b13a22F"), + }, + }, + `{"header":{"messageId":"0x0100000000000000000000000000000000000000000000000000000000000000","sourceChainSelector":"1","destChainSelector":"2","seqNum":"2","nonce":1,"msgHash":"0x2300000000000000000000000000000000000000000000000000000000000000","onRamp":"0x04d4cc5972ad487f71b85654d48b27d32b13a22f"},"sender":"0x","data":"0x","receiver":"0x","extraArgs":"0x","feeToken":"0x","feeTokenAmount":null,"feeValueJuels":null,"tokenAmounts":null}`, + }, + { + "with evm ramp message", + Message{ + Header: RampMessageHeader{ + MessageID: mustNewBytes32(t, "0x01"), + SourceChainSelector: ChainSelector(1), + DestChainSelector: ChainSelector(2), + SequenceNumber: 2, + Nonce: 1, + + MsgHash: mustNewBytes32(t, "0x23"), + OnRamp: mustNewBytes(t, "0x04D4cC5972ad487F71b85654d48b27D32b13a22F"), + }, + Sender: mustNewBytes(t, "0x04D4cC5972ad487F71b85654d48b27D32b13a22F"), + Receiver: mustNewBytes(t, "0x101112131415"), // simulate a non-evm receiver + Data: []byte("some data"), + ExtraArgs: []byte("extra args"), + FeeToken: mustNewBytes(t, "0xB5fCC870d2aC8745054b4ba99B1f176B93382162"), + FeeTokenAmount: BigInt{Int: big.NewInt(1000)}, + FeeValueJuels: BigInt{Int: big.NewInt(287)}, + TokenAmounts: []RampTokenAmount{ + { + SourcePoolAddress: mustNewBytes(t, "0x3E8456720B88A1DAdce8E2808C9Bf73dfFFd807c"), + DestTokenAddress: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, // simulate a non-evm token + ExtraData: []byte("extra token data"), + Amount: BigInt{Int: big.NewInt(2000)}, + DestExecData: []byte("extra token data"), + }, + }, + }, + `{"header":{"messageId":"0x0100000000000000000000000000000000000000000000000000000000000000","sourceChainSelector":"1","destChainSelector":"2","seqNum":"2","nonce":1,"msgHash":"0x2300000000000000000000000000000000000000000000000000000000000000","onRamp":"0x04d4cc5972ad487f71b85654d48b27d32b13a22f"},"sender":"0x04d4cc5972ad487f71b85654d48b27d32b13a22f","data":"0x736f6d652064617461","receiver":"0x101112131415","extraArgs":"0x65787472612061726773","feeToken":"0xb5fcc870d2ac8745054b4ba99b1f176b93382162","feeTokenAmount":"1000","feeValueJuels":"287","tokenAmounts":[{"sourcePoolAddress":"0x3e8456720b88a1dadce8e2808c9bf73dfffd807c","destTokenAddress":"0x0102030405060708090a","extraData":"0x657874726120746f6b656e2064617461","amount":"2000","destExecData":"0x657874726120746f6b656e2064617461"}]}`, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, test.c.String()) + }) + } +} + +func TestNewTokenPrice(t *testing.T) { + t.Run("base", func(t *testing.T) { + tp := NewTokenPrice("link", big.NewInt(1000)) + assert.Equal(t, "link", string(tp.TokenID)) + assert.Equal(t, uint64(1000), tp.Price.Int.Uint64()) + }) +} + +func TestNewGasPriceChain(t *testing.T) { + t.Run("base", func(t *testing.T) { + gpc := NewGasPriceChain(big.NewInt(1000), ChainSelector(1)) + assert.Equal(t, uint64(1000), (gpc.GasPrice).Uint64()) + assert.Equal(t, ChainSelector(1), gpc.ChainSel) + }) +} + +func TestMerkleRoot(t *testing.T) { + t.Run("str", func(t *testing.T) { + mr := Bytes32([32]byte{1}) + assert.Equal(t, "0x0100000000000000000000000000000000000000000000000000000000000000", mr.String()) + }) + + t.Run("json", func(t *testing.T) { + mr := Bytes32([32]byte{1}) + b, err := json.Marshal(mr) + assert.NoError(t, err) + assert.Equal(t, `"0x0100000000000000000000000000000000000000000000000000000000000000"`, string(b)) + + mr2 := Bytes32{} + err = json.Unmarshal(b, &mr2) + assert.NoError(t, err) + assert.Equal(t, mr, mr2) + + mr3 := Bytes32{} + err = json.Unmarshal([]byte(`"123"`), &mr3) + assert.Error(t, err) + + err = json.Unmarshal([]byte(`""`), &mr3) + assert.Error(t, err) + }) +} + +func mustNewBytes32(t *testing.T, s string) Bytes32 { + b32, err := NewBytes32FromString(s) + require.NoError(t, err) + return b32 +} + +func mustNewBytes(t *testing.T, s string) Bytes { + b, err := NewBytesFromString(s) + require.NoError(t, err) + return b +} diff --git a/pkg/types/ccipocr3/v2/v2/interfaces.go b/pkg/types/ccipocr3/v2/v2/interfaces.go new file mode 100644 index 000000000..3a263666e --- /dev/null +++ b/pkg/types/ccipocr3/v2/v2/interfaces.go @@ -0,0 +1,40 @@ +package ccipocr3 + +import ( + "context" +) + +type CommitPluginCodec interface { + Encode(context.Context, CommitPluginReport) ([]byte, error) + Decode(context.Context, []byte) (CommitPluginReport, error) +} + +type ExecutePluginCodec interface { + Encode(context.Context, ExecutePluginReport) ([]byte, error) + Decode(context.Context, []byte) (ExecutePluginReport, error) +} + +type MessageHasher interface { + Hash(context.Context, Message) (Bytes32, error) +} + +// RMNCrypto provides a chain-agnostic interface for verifying RMN signatures. +// For example, on EVM, RMN reports are abi-encoded prior to being signed. +// On Solana, they would be borsh encoded instead, etc. +type RMNCrypto interface { + // VerifyReportSignatures verifies each provided signature against the provided report and the signer addresses. + // If any signature is invalid (no matching signer address is found), an error is returned immediately. + VerifyReportSignatures( + ctx context.Context, + sigs []RMNECDSASignature, + report RMNReport, + signerAddresses []Bytes, + ) error +} + +// TokenDataEncoder is a generic interface for encoding offchain token data for different chain families. +// Right now it supports only USDC/CCTP, but every new token that requires offchain data processsing +// should be added to that interface and implemented in the downstream repositories (e.g. chainlink-ccip, chainlink). +type TokenDataEncoder interface { + EncodeUSDC(ctx context.Context, message Bytes, attestation Bytes) (Bytes, error) +} diff --git a/pkg/types/ccipocr3/v2/v2/plugin_commit_types.go b/pkg/types/ccipocr3/v2/v2/plugin_commit_types.go new file mode 100644 index 000000000..665821faf --- /dev/null +++ b/pkg/types/ccipocr3/v2/v2/plugin_commit_types.go @@ -0,0 +1,73 @@ +package ccipocr3 + +import "bytes" + +// CommitPluginReport contains the necessary information to commit CCIP +// messages from potentially many source chains, to a single destination chain. +// +// It must consist of either: +// +// 1. a non-empty MerkleRoots array, or +// 2. a non-empty PriceUpdates array +// +// If neither of the above is provided the report is considered empty and should +// not be transmitted on-chain. +// +// In the event the MerkleRoots array is non-empty, it may also contain +// RMNSignatures, if RMN is configured for some lanes involved in the commitment. +// A report with RMN signatures but without merkle roots is invalid. +type CommitPluginReport struct { + MerkleRoots []MerkleRootChain `json:"merkleRoots"` + PriceUpdates PriceUpdates `json:"priceUpdates"` + + // RMNSignatures are the ECDSA signatures from the RMN signing nodes on the RMNReport structure. + // For more details see the contract here: https://github.com/smartcontractkit/chainlink/blob/7ba0f37134a618375542079ff1805fe2224d7916/contracts/src/v0.8/ccip/interfaces/IRMNV2.sol#L8-L12 + RMNSignatures []RMNECDSASignature `json:"rmnSignatures"` + + // RMNRawVs is a 256-bit bitmap. A bit is set if the v value of the signature is 28, + // and unset if the v value is 27. + // Note that this implies that the maximum number of RMN signatures is 256. + RMNRawVs BigInt `json:"rmnRawVs"` +} + +// IsEmpty returns true if the CommitPluginReport is empty +func (r CommitPluginReport) IsEmpty() bool { + return len(r.MerkleRoots) == 0 && + len(r.PriceUpdates.TokenPriceUpdates) == 0 && + len(r.PriceUpdates.GasPriceUpdates) == 0 && + len(r.RMNSignatures) == 0 +} + +// MerkleRootChain Mirroring https://github.com/smartcontractkit/chainlink/blob/cd5c78959575f593b27fd83d8766086d0c678487/contracts/src/v0.8/ccip/libraries/Internal.sol#L356-L362 +type MerkleRootChain struct { + ChainSel ChainSelector `json:"chain"` + OnRampAddress Bytes `json:"onRampAddress"` + SeqNumsRange SeqNumRange `json:"seqNumsRange"` + MerkleRoot Bytes32 `json:"merkleRoot"` +} + +func (m MerkleRootChain) Equals(other MerkleRootChain) bool { + return m.ChainSel == other.ChainSel && + bytes.Equal(m.OnRampAddress, other.OnRampAddress) && + m.SeqNumsRange == other.SeqNumsRange && + m.MerkleRoot == other.MerkleRoot +} + +func NewMerkleRootChain( + chainSel ChainSelector, + onRampAddress Bytes, + seqNumsRange SeqNumRange, + merkleRoot Bytes32, +) MerkleRootChain { + return MerkleRootChain{ + ChainSel: chainSel, + OnRampAddress: onRampAddress, + SeqNumsRange: seqNumsRange, + MerkleRoot: merkleRoot, + } +} + +type PriceUpdates struct { + TokenPriceUpdates []TokenPrice `json:"tokenPriceUpdates"` + GasPriceUpdates []GasPriceChain `json:"gasPriceUpdates"` +} diff --git a/pkg/types/ccipocr3/v2/v2/plugin_commit_types_test.go b/pkg/types/ccipocr3/v2/v2/plugin_commit_types_test.go new file mode 100644 index 000000000..f49b9ef87 --- /dev/null +++ b/pkg/types/ccipocr3/v2/v2/plugin_commit_types_test.go @@ -0,0 +1,140 @@ +package ccipocr3 + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCommitPluginReport(t *testing.T) { + t.Run("is empty", func(t *testing.T) { + r := CommitPluginReport{} + assert.True(t, r.IsEmpty()) + }) + + t.Run("is not empty", func(t *testing.T) { + r := CommitPluginReport{ + MerkleRoots: make([]MerkleRootChain, 1), + } + assert.False(t, r.IsEmpty()) + + r = CommitPluginReport{ + PriceUpdates: PriceUpdates{ + TokenPriceUpdates: make([]TokenPrice, 1), + GasPriceUpdates: make([]GasPriceChain, 1), + }, + } + assert.False(t, r.IsEmpty()) + + r = CommitPluginReport{ + RMNSignatures: make([]RMNECDSASignature, 1), + } + assert.False(t, r.IsEmpty()) + + r = CommitPluginReport{ + MerkleRoots: make([]MerkleRootChain, 1), + PriceUpdates: PriceUpdates{ + TokenPriceUpdates: make([]TokenPrice, 1), + GasPriceUpdates: make([]GasPriceChain, 1), + }, + RMNSignatures: make([]RMNECDSASignature, 1), + } + assert.False(t, r.IsEmpty()) + }) +} + +func TestMerkleRootChain_Equals_Structs(t *testing.T) { + tests := []struct { + name string + m1 MerkleRootChain + m2 MerkleRootChain + expected bool + }{ + { + name: "equal MerkleRootChains", + m1: MerkleRootChain{ + ChainSel: ChainSelector(1), + OnRampAddress: []byte{0x01, 0x02}, + SeqNumsRange: SeqNumRange{1, 10}, + MerkleRoot: Bytes32{0x01, 0x02, 0x03}, + }, + m2: MerkleRootChain{ + ChainSel: ChainSelector(1), + OnRampAddress: []byte{0x01, 0x02}, + SeqNumsRange: SeqNumRange{1, 10}, + MerkleRoot: Bytes32{0x01, 0x02, 0x03}, + }, + expected: true, + }, + { + name: "different ChainSel", + m1: MerkleRootChain{ + ChainSel: ChainSelector(1), + OnRampAddress: []byte{0x01, 0x02}, + SeqNumsRange: SeqNumRange{1, 10}, + MerkleRoot: Bytes32{0x01, 0x02, 0x03}, + }, + m2: MerkleRootChain{ + ChainSel: ChainSelector(2), + OnRampAddress: []byte{0x01, 0x02}, + SeqNumsRange: SeqNumRange{1, 10}, + MerkleRoot: Bytes32{0x01, 0x02, 0x03}, + }, + expected: false, + }, + { + name: "different OnRampAddress", + m1: MerkleRootChain{ + ChainSel: ChainSelector(1), + OnRampAddress: []byte{0x01, 0x02}, + SeqNumsRange: SeqNumRange{1, 10}, + MerkleRoot: Bytes32{0x01, 0x02, 0x03}, + }, + m2: MerkleRootChain{ + ChainSel: ChainSelector(1), + OnRampAddress: []byte{0x03, 0x04}, + SeqNumsRange: SeqNumRange{1, 10}, + MerkleRoot: Bytes32{0x01, 0x02, 0x03}, + }, + expected: false, + }, + { + name: "different SeqNumsRange", + m1: MerkleRootChain{ + ChainSel: ChainSelector(1), + OnRampAddress: []byte{0x01, 0x02}, + SeqNumsRange: SeqNumRange{1, 10}, + MerkleRoot: Bytes32{0x01, 0x02, 0x03}, + }, + m2: MerkleRootChain{ + ChainSel: ChainSelector(1), + OnRampAddress: []byte{0x01, 0x02}, + SeqNumsRange: SeqNumRange{2, 20}, + MerkleRoot: Bytes32{0x01, 0x02, 0x03}, + }, + expected: false, + }, + { + name: "different MerkleRoot", + m1: MerkleRootChain{ + ChainSel: ChainSelector(1), + OnRampAddress: []byte{0x01, 0x02}, + SeqNumsRange: SeqNumRange{1, 10}, + MerkleRoot: Bytes32{0x01, 0x02, 0x03}, + }, + m2: MerkleRootChain{ + ChainSel: ChainSelector(1), + OnRampAddress: []byte{0x01, 0x02}, + SeqNumsRange: SeqNumRange{1, 10}, + MerkleRoot: Bytes32{0x04, 0x05, 0x06}, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.m1.Equals(tt.m2)) + }) + } +} diff --git a/pkg/types/ccipocr3/v2/v2/plugin_execute_types.go b/pkg/types/ccipocr3/v2/v2/plugin_execute_types.go new file mode 100644 index 000000000..61e813226 --- /dev/null +++ b/pkg/types/ccipocr3/v2/v2/plugin_execute_types.go @@ -0,0 +1,13 @@ +package ccipocr3 + +type ExecutePluginReport struct { + ChainReports []ExecutePluginReportSingleChain `json:"chainReports"` +} + +type ExecutePluginReportSingleChain struct { + SourceChainSelector ChainSelector `json:"sourceChainSelector"` + Messages []Message `json:"messages"` + OffchainTokenData [][][]byte `json:"offchainTokenData"` + Proofs []Bytes32 `json:"proofs"` + ProofFlagBits BigInt `json:"proofFlagBits"` +} diff --git a/pkg/types/ccipocr3/v2/v2/rmn_types.go b/pkg/types/ccipocr3/v2/v2/rmn_types.go new file mode 100644 index 000000000..8d8839495 --- /dev/null +++ b/pkg/types/ccipocr3/v2/v2/rmn_types.go @@ -0,0 +1,30 @@ +package ccipocr3 + +// RMNReport is the payload that is signed by the RMN nodes, transmitted and verified onchain. +type RMNReport struct { + ReportVersion string // e.g. "RMN_V1_6_ANY2EVM_REPORT". + DestChainID BigInt // If applies, a chain specific id, e.g. evm chain id otherwise empty. + DestChainSelector ChainSelector + RmnRemoteContractAddress Bytes + OfframpAddress Bytes + RmnHomeContractConfigDigest Bytes32 + LaneUpdates []RMNLaneUpdate +} + +// RMNLaneUpdate represents an interval that has been observed by an RMN node. +// It is part of the payload that is signed and transmitted onchain. +type RMNLaneUpdate struct { + SourceChainSelector ChainSelector + OnRampAddress Bytes // (for EVM should be abi-encoded) + MinSeqNr SeqNum + MaxSeqNr SeqNum + MerkleRoot Bytes32 +} + +// RMNECDSASignature represents the signature provided by RMN on the RMNReport structure. +// The V value of the signature is included in the top-level commit report as part of a +// bitmap. +type RMNECDSASignature struct { + R Bytes32 `json:"r"` + S Bytes32 `json:"s"` +} From f495481347948a8b1eb50c16d6c694b393699836 Mon Sep 17 00:00:00 2001 From: Will Winder Date: Tue, 8 Oct 2024 12:22:07 -0400 Subject: [PATCH 2/2] Add unknown address types. Remove mustNewBytes Remove mustNewBytes --- pkg/types/ccipocr3/v2/common_types.go | 28 ++ pkg/types/ccipocr3/v2/common_types_test.go | 10 + pkg/types/ccipocr3/v2/generic_types.go | 20 +- pkg/types/ccipocr3/v2/generic_types_test.go | 18 +- pkg/types/ccipocr3/v2/interfaces.go | 2 +- pkg/types/ccipocr3/v2/rmn_types.go | 6 +- pkg/types/ccipocr3/v2/v2/common_types.go | 148 ---------- pkg/types/ccipocr3/v2/v2/common_types_test.go | 136 ---------- pkg/types/ccipocr3/v2/v2/generic_types.go | 192 ------------- .../ccipocr3/v2/v2/generic_types_test.go | 253 ------------------ pkg/types/ccipocr3/v2/v2/interfaces.go | 40 --- .../ccipocr3/v2/v2/plugin_commit_types.go | 73 ----- .../v2/v2/plugin_commit_types_test.go | 140 ---------- .../ccipocr3/v2/v2/plugin_execute_types.go | 13 - pkg/types/ccipocr3/v2/v2/rmn_types.go | 30 --- 15 files changed, 60 insertions(+), 1049 deletions(-) delete mode 100644 pkg/types/ccipocr3/v2/v2/common_types.go delete mode 100644 pkg/types/ccipocr3/v2/v2/common_types_test.go delete mode 100644 pkg/types/ccipocr3/v2/v2/generic_types.go delete mode 100644 pkg/types/ccipocr3/v2/v2/generic_types_test.go delete mode 100644 pkg/types/ccipocr3/v2/v2/interfaces.go delete mode 100644 pkg/types/ccipocr3/v2/v2/plugin_commit_types.go delete mode 100644 pkg/types/ccipocr3/v2/v2/plugin_commit_types_test.go delete mode 100644 pkg/types/ccipocr3/v2/v2/plugin_execute_types.go delete mode 100644 pkg/types/ccipocr3/v2/v2/rmn_types.go diff --git a/pkg/types/ccipocr3/v2/common_types.go b/pkg/types/ccipocr3/v2/common_types.go index bc1048959..a1b868bfb 100644 --- a/pkg/types/ccipocr3/v2/common_types.go +++ b/pkg/types/ccipocr3/v2/common_types.go @@ -7,6 +7,34 @@ import ( "strings" ) +// UnknownAddress represents a raw address with an unknown encoding. +type UnknownAddress []byte + +// NewUnknownAddressFromHex creates a new UnknownAddress from a hex string. +func NewUnknownAddressFromHex(s string) (UnknownAddress, error) { + b, err := NewBytesFromString(s) + if err != nil { + return nil, err + } + return UnknownAddress(b), nil +} + +// String returns the hex representation of the unknown address. +func (a UnknownAddress) String() string { + return Bytes(a).String() +} + +func (a UnknownAddress) MarshalJSON() ([]byte, error) { + return Bytes(a).MarshalJSON() +} + +func (a *UnknownAddress) UnmarshalJSON(data []byte) error { + return (*Bytes)(a).UnmarshalJSON(data) +} + +// UnknownEncodedAddress represents an encoded address with an unknown encoding. +type UnknownEncodedAddress string + type Bytes []byte func NewBytesFromString(s string) (Bytes, error) { diff --git a/pkg/types/ccipocr3/v2/common_types_test.go b/pkg/types/ccipocr3/v2/common_types_test.go index 9403a6093..ddac03493 100644 --- a/pkg/types/ccipocr3/v2/common_types_test.go +++ b/pkg/types/ccipocr3/v2/common_types_test.go @@ -132,5 +132,15 @@ func TestNewBytesFromString(t *testing.T) { require.Equal(t, tt.want, got) } }) + + t.Run(tt.name, func(t *testing.T) { + got, err := NewUnknownAddressFromHex(tt.arg) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, UnknownAddress(tt.want), got) + } + }) } } diff --git a/pkg/types/ccipocr3/v2/generic_types.go b/pkg/types/ccipocr3/v2/generic_types.go index dcf37f73d..63ae6c229 100644 --- a/pkg/types/ccipocr3/v2/generic_types.go +++ b/pkg/types/ccipocr3/v2/generic_types.go @@ -5,16 +5,14 @@ import ( "fmt" "math/big" "strconv" - - "github.com/smartcontractkit/libocr/offchainreporting2plus/types" ) type TokenPrice struct { - TokenID types.Account `json:"tokenID"` - Price BigInt `json:"price"` + TokenID UnknownEncodedAddress `json:"tokenID"` + Price BigInt `json:"price"` } -func NewTokenPrice(tokenID types.Account, price *big.Int) TokenPrice { +func NewTokenPrice(tokenID UnknownEncodedAddress, price *big.Int) TokenPrice { return TokenPrice{ TokenID: tokenID, Price: BigInt{price}, @@ -113,18 +111,18 @@ type Message struct { Header RampMessageHeader `json:"header"` // Sender address on the source chain. // i.e if the source chain is EVM, this is an abi-encoded EVM address. - Sender Bytes `json:"sender"` + Sender UnknownAddress `json:"sender"` // Data is the arbitrary data payload supplied by the message sender. Data Bytes `json:"data"` // Receiver is the receiver address on the destination chain. // This is encoded in the destination chain family specific encoding. // i.e if the destination is EVM, this is abi.encode(receiver). - Receiver Bytes `json:"receiver"` + Receiver UnknownAddress `json:"receiver"` // ExtraArgs is destination-chain specific extra args, such as the gasLimit for EVM chains. ExtraArgs Bytes `json:"extraArgs"` // FeeToken is the fee token address. // i.e if the source chain is EVM, len(FeeToken) == 20 (i.e, is not abi-encoded). - FeeToken Bytes `json:"feeToken"` + FeeToken UnknownAddress `json:"feeToken"` // FeeTokenAmount is the amount of fee tokens paid. FeeTokenAmount BigInt `json:"feeTokenAmount"` // FeeValueJuels is the fee amount in Juels @@ -161,7 +159,7 @@ type RampMessageHeader struct { // OnRamp is the address of the onramp that sent the message. // NOTE: This is populated by the ccip reader. Not emitted explicitly onchain. - OnRamp Bytes `json:"onRamp"` + OnRamp UnknownAddress `json:"onRamp"` } // RampTokenAmount represents the family-agnostic token amounts used for both OnRamp & OffRamp messages. @@ -169,11 +167,11 @@ type RampTokenAmount struct { // SourcePoolAddress is the source pool address, encoded according to source family native encoding scheme. // This value is trusted as it was obtained through the onRamp. It can be relied upon by the destination // pool to validate the source pool. - SourcePoolAddress Bytes `json:"sourcePoolAddress"` + SourcePoolAddress UnknownAddress `json:"sourcePoolAddress"` // DestTokenAddress is the address of the destination token, abi encoded in the case of EVM chains. // This value is UNTRUSTED as any pool owner can return whatever value they want. - DestTokenAddress Bytes `json:"destTokenAddress"` + DestTokenAddress UnknownAddress `json:"destTokenAddress"` // ExtraData is optional pool data to be transferred to the destination chain. Be default this is capped at // CCIP_LOCK_OR_BURN_V1_RET_BYTES bytes. If more data is required, the TokenTransferFeeConfig.destBytesOverhead diff --git a/pkg/types/ccipocr3/v2/generic_types_test.go b/pkg/types/ccipocr3/v2/generic_types_test.go index af94cca92..d7964a34c 100644 --- a/pkg/types/ccipocr3/v2/generic_types_test.go +++ b/pkg/types/ccipocr3/v2/generic_types_test.go @@ -152,7 +152,7 @@ func TestCCIPMsg_String(t *testing.T) { Nonce: 1, MsgHash: mustNewBytes32(t, "0x23"), - OnRamp: mustNewBytes(t, "0x04D4cC5972ad487F71b85654d48b27D32b13a22F"), + OnRamp: mustNewUnknownAddress(t, "0x04D4cC5972ad487F71b85654d48b27D32b13a22F"), }, }, `{"header":{"messageId":"0x0100000000000000000000000000000000000000000000000000000000000000","sourceChainSelector":"1","destChainSelector":"2","seqNum":"2","nonce":1,"msgHash":"0x2300000000000000000000000000000000000000000000000000000000000000","onRamp":"0x04d4cc5972ad487f71b85654d48b27d32b13a22f"},"sender":"0x","data":"0x","receiver":"0x","extraArgs":"0x","feeToken":"0x","feeTokenAmount":null,"feeValueJuels":null,"tokenAmounts":null}`, @@ -168,18 +168,18 @@ func TestCCIPMsg_String(t *testing.T) { Nonce: 1, MsgHash: mustNewBytes32(t, "0x23"), - OnRamp: mustNewBytes(t, "0x04D4cC5972ad487F71b85654d48b27D32b13a22F"), + OnRamp: mustNewUnknownAddress(t, "0x04D4cC5972ad487F71b85654d48b27D32b13a22F"), }, - Sender: mustNewBytes(t, "0x04D4cC5972ad487F71b85654d48b27D32b13a22F"), - Receiver: mustNewBytes(t, "0x101112131415"), // simulate a non-evm receiver + Sender: mustNewUnknownAddress(t, "0x04D4cC5972ad487F71b85654d48b27D32b13a22F"), + Receiver: mustNewUnknownAddress(t, "0x101112131415"), // simulate a non-evm receiver Data: []byte("some data"), ExtraArgs: []byte("extra args"), - FeeToken: mustNewBytes(t, "0xB5fCC870d2aC8745054b4ba99B1f176B93382162"), + FeeToken: mustNewUnknownAddress(t, "0xB5fCC870d2aC8745054b4ba99B1f176B93382162"), FeeTokenAmount: BigInt{Int: big.NewInt(1000)}, FeeValueJuels: BigInt{Int: big.NewInt(287)}, TokenAmounts: []RampTokenAmount{ { - SourcePoolAddress: mustNewBytes(t, "0x3E8456720B88A1DAdce8E2808C9Bf73dfFFd807c"), + SourcePoolAddress: mustNewUnknownAddress(t, "0x3E8456720B88A1DAdce8E2808C9Bf73dfFFd807c"), DestTokenAddress: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, // simulate a non-evm token ExtraData: []byte("extra token data"), Amount: BigInt{Int: big.NewInt(2000)}, @@ -246,8 +246,8 @@ func mustNewBytes32(t *testing.T, s string) Bytes32 { return b32 } -func mustNewBytes(t *testing.T, s string) Bytes { - b, err := NewBytesFromString(s) +func mustNewUnknownAddress(t *testing.T, s string) UnknownAddress { + a, err := NewUnknownAddressFromHex(s) require.NoError(t, err) - return b + return a } diff --git a/pkg/types/ccipocr3/v2/interfaces.go b/pkg/types/ccipocr3/v2/interfaces.go index 3a263666e..7c6d69bfa 100644 --- a/pkg/types/ccipocr3/v2/interfaces.go +++ b/pkg/types/ccipocr3/v2/interfaces.go @@ -28,7 +28,7 @@ type RMNCrypto interface { ctx context.Context, sigs []RMNECDSASignature, report RMNReport, - signerAddresses []Bytes, + signerAddresses []UnknownAddress, ) error } diff --git a/pkg/types/ccipocr3/v2/rmn_types.go b/pkg/types/ccipocr3/v2/rmn_types.go index 8d8839495..c5f9fa64d 100644 --- a/pkg/types/ccipocr3/v2/rmn_types.go +++ b/pkg/types/ccipocr3/v2/rmn_types.go @@ -5,8 +5,8 @@ type RMNReport struct { ReportVersion string // e.g. "RMN_V1_6_ANY2EVM_REPORT". DestChainID BigInt // If applies, a chain specific id, e.g. evm chain id otherwise empty. DestChainSelector ChainSelector - RmnRemoteContractAddress Bytes - OfframpAddress Bytes + RmnRemoteContractAddress UnknownAddress + OfframpAddress UnknownAddress RmnHomeContractConfigDigest Bytes32 LaneUpdates []RMNLaneUpdate } @@ -15,7 +15,7 @@ type RMNReport struct { // It is part of the payload that is signed and transmitted onchain. type RMNLaneUpdate struct { SourceChainSelector ChainSelector - OnRampAddress Bytes // (for EVM should be abi-encoded) + OnRampAddress UnknownAddress // (for EVM should be abi-encoded) MinSeqNr SeqNum MaxSeqNr SeqNum MerkleRoot Bytes32 diff --git a/pkg/types/ccipocr3/v2/v2/common_types.go b/pkg/types/ccipocr3/v2/v2/common_types.go deleted file mode 100644 index bc1048959..000000000 --- a/pkg/types/ccipocr3/v2/v2/common_types.go +++ /dev/null @@ -1,148 +0,0 @@ -package ccipocr3 - -import ( - "encoding/hex" - "fmt" - "math/big" - "strings" -) - -type Bytes []byte - -func NewBytesFromString(s string) (Bytes, error) { - if len(s) < 2 { - return nil, fmt.Errorf("Bytes must be of at least length 2 (i.e, '0x' prefix): %s", s) - } - - if !strings.HasPrefix(s, "0x") { - return nil, fmt.Errorf("Bytes must start with '0x' prefix: %s", s) - } - - b, err := hex.DecodeString(s[2:]) - if err != nil { - return nil, fmt.Errorf("failed to decode hex: %w", err) - } - - return Bytes(b), nil -} - -func (b Bytes) String() string { - return "0x" + hex.EncodeToString(b) -} - -func (b Bytes) MarshalJSON() ([]byte, error) { - return []byte(fmt.Sprintf(`"%s"`, b.String())), nil -} - -func (b *Bytes) UnmarshalJSON(data []byte) error { - v := string(data) - if len(v) < 2 { - return fmt.Errorf("Bytes must be of at least length 2 (i.e, '0x' prefix): %s", v) - } - - // trim the start and end double quotes - v = v[1 : len(v)-1] - - if !strings.HasPrefix(v, "0x") { - return fmt.Errorf("Bytes must start with '0x' prefix: %s", v) - } - - // Decode everything after the '0x' prefix. - bs, err := hex.DecodeString(v[2:]) - if err != nil { - return fmt.Errorf("failed to decode hex: %w", err) - } - - *b = bs - return nil -} - -type Bytes32 [32]byte - -func NewBytes32FromString(s string) (Bytes32, error) { - if len(s) < 2 { - return Bytes32{}, fmt.Errorf("Bytes32 must be of at least length 2 (i.e, '0x' prefix): %s", s) - } - - if !strings.HasPrefix(s, "0x") { - return Bytes32{}, fmt.Errorf("Bytes32 must start with '0x' prefix: %s", s) - } - - b, err := hex.DecodeString(s[2:]) - if err != nil { - return Bytes32{}, fmt.Errorf("failed to decode hex: %w", err) - } - - var res Bytes32 - copy(res[:], b) - return res, nil -} - -func (b Bytes32) String() string { - return "0x" + hex.EncodeToString(b[:]) -} - -func (b Bytes32) IsEmpty() bool { - return b == Bytes32{} -} - -func (b Bytes32) MarshalJSON() ([]byte, error) { - return []byte(fmt.Sprintf(`"%s"`, b.String())), nil -} - -func (b *Bytes32) UnmarshalJSON(data []byte) error { - v := string(data) - if len(v) < 4 { - return fmt.Errorf("invalid MerkleRoot: %s", v) - } - - bCp, err := hex.DecodeString(v[1 : len(v)-1][2:]) - if err != nil { - return err - } - - copy(b[:], bCp) - return nil -} - -type BigInt struct { - *big.Int -} - -func NewBigInt(i *big.Int) BigInt { - return BigInt{Int: i} -} - -func NewBigIntFromInt64(i int64) BigInt { - return BigInt{Int: big.NewInt(i)} -} - -func (b BigInt) MarshalJSON() ([]byte, error) { - if b.Int == nil { - return []byte("null"), nil - } - return []byte(fmt.Sprintf(`"%s"`, b.String())), nil -} - -func (b *BigInt) UnmarshalJSON(p []byte) error { - if string(p) == "null" { - return nil - } - - if len(p) < 2 { - return fmt.Errorf("invalid BigInt: %s", p) - } - p = p[1 : len(p)-1] - - z := big.NewInt(0) - _, ok := z.SetString(string(p), 10) - if !ok { - return fmt.Errorf("not a valid big integer: %s", p) - } - b.Int = z - return nil -} - -func (b BigInt) IsEmpty() bool { - return b.Int == nil -} diff --git a/pkg/types/ccipocr3/v2/v2/common_types_test.go b/pkg/types/ccipocr3/v2/v2/common_types_test.go deleted file mode 100644 index 9403a6093..000000000 --- a/pkg/types/ccipocr3/v2/v2/common_types_test.go +++ /dev/null @@ -1,136 +0,0 @@ -package ccipocr3 - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestNewBytes32FromString(t *testing.T) { - testCases := []struct { - name string - input string - expected Bytes32 - expErr bool - }{ - { - name: "valid input", - input: "0x200000000000000000000000", - expected: Bytes32{0x20, 0}, - expErr: false, - }, - { - name: "invalid hex characters", - input: "lrfv", - expected: Bytes32{}, - expErr: true, - }, - { - name: "invalid input, no 0x prefix", - input: "200000000000000000000000", - expected: Bytes32{}, - expErr: true, - }, - { - name: "invalid input, not enough hex chars", - input: "0x2", - expected: Bytes32{}, - expErr: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - actual, err := NewBytes32FromString(tc.input) - if tc.expErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - assert.Equal(t, tc.expected, actual) - }) - } -} - -func TestBytes32_IsEmpty(t *testing.T) { - testCases := []struct { - name string - input Bytes32 - expected bool - }{ - { - name: "empty", - input: Bytes32{}, - expected: true, - }, - { - name: "not empty", - input: Bytes32{0x20, 0}, - expected: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.expected, tc.input.IsEmpty()) - }) - } -} - -func TestNewBytesFromString(t *testing.T) { - tests := []struct { - name string - arg string - want Bytes - wantErr bool - }{ - { - "valid input", - "0x20", - Bytes{0x20}, - false, - }, - { - "valid long input", - "0x2010201020", - Bytes{0x20, 0x10, 0x20, 0x10, 0x20}, - false, - }, - { - "invalid input", - "0", - nil, - true, - }, - { - "invalid input, not enough hex chars", - "0x2", - nil, - true, - }, - { - "invalid input, no 0x prefix", - "20", - nil, - true, - }, - { - "invalid hex characters", - "0x2g", - nil, - true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := NewBytesFromString(tt.arg) - if tt.wantErr { - require.Error(t, err) - } else { - require.NoError(t, err) - require.Equal(t, tt.want, got) - } - }) - } -} diff --git a/pkg/types/ccipocr3/v2/v2/generic_types.go b/pkg/types/ccipocr3/v2/v2/generic_types.go deleted file mode 100644 index dcf37f73d..000000000 --- a/pkg/types/ccipocr3/v2/v2/generic_types.go +++ /dev/null @@ -1,192 +0,0 @@ -package ccipocr3 - -import ( - "encoding/json" - "fmt" - "math/big" - "strconv" - - "github.com/smartcontractkit/libocr/offchainreporting2plus/types" -) - -type TokenPrice struct { - TokenID types.Account `json:"tokenID"` - Price BigInt `json:"price"` -} - -func NewTokenPrice(tokenID types.Account, price *big.Int) TokenPrice { - return TokenPrice{ - TokenID: tokenID, - Price: BigInt{price}, - } -} - -type GasPriceChain struct { - GasPrice BigInt `json:"gasPrice"` - ChainSel ChainSelector `json:"chainSel"` -} - -func NewGasPriceChain(gasPrice *big.Int, chainSel ChainSelector) GasPriceChain { - return GasPriceChain{ - GasPrice: NewBigInt(gasPrice), - ChainSel: chainSel, - } -} - -type SeqNum uint64 - -func (s SeqNum) String() string { - return strconv.FormatUint(uint64(s), 10) -} - -func NewSeqNumRange(start, end SeqNum) SeqNumRange { - return SeqNumRange{start, end} -} - -// SeqNumRange defines an inclusive range of sequence numbers. -type SeqNumRange [2]SeqNum - -func (s SeqNumRange) Start() SeqNum { - return s[0] -} - -func (s SeqNumRange) End() SeqNum { - return s[1] -} - -func (s *SeqNumRange) SetStart(v SeqNum) { - s[0] = v -} - -func (s *SeqNumRange) SetEnd(v SeqNum) { - s[1] = v -} - -// Limit returns a range limited up to n elements by truncating the end if necessary. -// Example: [1 -> 10].Limit(5) => [1 -> 5] -func (s *SeqNumRange) Limit(n uint64) SeqNumRange { - limitedRange := NewSeqNumRange(s.Start(), s.End()) - - numElems := s.End() - s.Start() + 1 - if numElems <= 0 { - return limitedRange - } - - if uint64(numElems) > n { - newEnd := limitedRange.Start() + SeqNum(n) - 1 - if newEnd > limitedRange.End() { // overflow - do nothing - return limitedRange - } - limitedRange.SetEnd(newEnd) - } - - return limitedRange -} - -// Overlaps returns true if the two ranges overlap. -func (s SeqNumRange) Overlaps(other SeqNumRange) bool { - return s.Start() <= other.End() && other.Start() <= s.End() -} - -// Contains returns true if the range contains the given sequence number. -func (s SeqNumRange) Contains(seq SeqNum) bool { - return s.Start() <= seq && seq <= s.End() -} - -func (s SeqNumRange) String() string { - return fmt.Sprintf("[%d -> %d]", s[0], s[1]) -} - -type ChainSelector uint64 - -func (c ChainSelector) String() string { - return fmt.Sprintf("ChainSelector(%d)", c) -} - -// Message is the generic Any2Any message type for CCIP messages. -// It represents, in particular, a message emitted by a CCIP onramp. -// The message is expected to be consumed by a CCIP offramp after -// translating it into the appropriate format for the destination chain. -type Message struct { - // Header is the family-agnostic header for OnRamp and OffRamp messages. - // This is always set on all CCIP messages. - Header RampMessageHeader `json:"header"` - // Sender address on the source chain. - // i.e if the source chain is EVM, this is an abi-encoded EVM address. - Sender Bytes `json:"sender"` - // Data is the arbitrary data payload supplied by the message sender. - Data Bytes `json:"data"` - // Receiver is the receiver address on the destination chain. - // This is encoded in the destination chain family specific encoding. - // i.e if the destination is EVM, this is abi.encode(receiver). - Receiver Bytes `json:"receiver"` - // ExtraArgs is destination-chain specific extra args, such as the gasLimit for EVM chains. - ExtraArgs Bytes `json:"extraArgs"` - // FeeToken is the fee token address. - // i.e if the source chain is EVM, len(FeeToken) == 20 (i.e, is not abi-encoded). - FeeToken Bytes `json:"feeToken"` - // FeeTokenAmount is the amount of fee tokens paid. - FeeTokenAmount BigInt `json:"feeTokenAmount"` - // FeeValueJuels is the fee amount in Juels - FeeValueJuels BigInt `json:"feeValueJuels"` - // TokenAmounts is the array of tokens and amounts to transfer. - TokenAmounts []RampTokenAmount `json:"tokenAmounts"` -} - -func (c Message) String() string { - js, _ := json.Marshal(c) - return string(js) -} - -// RampMessageHeader is the family-agnostic header for OnRamp and OffRamp messages. -// The MessageID is not expected to match MsgHash, since it may originate from a different -// ramp family. -type RampMessageHeader struct { - // MessageID is a unique identifier for the message, it should be unique across all chains. - // It is generated on the chain that the CCIP send is requested (i.e. the source chain of a message). - MessageID Bytes32 `json:"messageId"` - // SourceChainSelector is the chain selector of the chain that the message originated from. - SourceChainSelector ChainSelector `json:"sourceChainSelector,string"` - // DestChainSelector is the chain selector of the chain that the message is destined for. - DestChainSelector ChainSelector `json:"destChainSelector,string"` - // SequenceNumber is an auto-incrementing sequence number for the message. - // Not unique across lanes. - SequenceNumber SeqNum `json:"seqNum,string"` - // Nonce is the nonce for this lane for this sender, not unique across senders/lanes - Nonce uint64 `json:"nonce"` - - // MsgHash is the hash of all the message fields. - // NOTE: The field is expected to be empty, and will be populated by the plugin using the MsgHasher interface. - MsgHash Bytes32 `json:"msgHash"` // populated - - // OnRamp is the address of the onramp that sent the message. - // NOTE: This is populated by the ccip reader. Not emitted explicitly onchain. - OnRamp Bytes `json:"onRamp"` -} - -// RampTokenAmount represents the family-agnostic token amounts used for both OnRamp & OffRamp messages. -type RampTokenAmount struct { - // SourcePoolAddress is the source pool address, encoded according to source family native encoding scheme. - // This value is trusted as it was obtained through the onRamp. It can be relied upon by the destination - // pool to validate the source pool. - SourcePoolAddress Bytes `json:"sourcePoolAddress"` - - // DestTokenAddress is the address of the destination token, abi encoded in the case of EVM chains. - // This value is UNTRUSTED as any pool owner can return whatever value they want. - DestTokenAddress Bytes `json:"destTokenAddress"` - - // ExtraData is optional pool data to be transferred to the destination chain. Be default this is capped at - // CCIP_LOCK_OR_BURN_V1_RET_BYTES bytes. If more data is required, the TokenTransferFeeConfig.destBytesOverhead - // has to be set for the specific token. - ExtraData Bytes `json:"extraData"` - - // Amount is the amount of tokens to be transferred. - Amount BigInt `json:"amount"` - - // DestExecData is destination chain specific execution data encoded in bytes. - // For an EVM destination, it consists of the amount of gas available for the releaseOrMint - // and transfer calls made by the offRamp. - // NOTE: this must be decoded before providing it as an execution input to the destination chain - // or hashing it. See Internal._hash(Any2EVMRampMessage) for more details as an example. - DestExecData Bytes `json:"destExecData"` -} diff --git a/pkg/types/ccipocr3/v2/v2/generic_types_test.go b/pkg/types/ccipocr3/v2/v2/generic_types_test.go deleted file mode 100644 index af94cca92..000000000 --- a/pkg/types/ccipocr3/v2/v2/generic_types_test.go +++ /dev/null @@ -1,253 +0,0 @@ -package ccipocr3 - -import ( - "encoding/json" - "math/big" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestSeqNumRange(t *testing.T) { - t.Run("base", func(t *testing.T) { - rng := NewSeqNumRange(1, 2) - assert.Equal(t, SeqNum(1), rng.Start()) - assert.Equal(t, SeqNum(2), rng.End()) - }) - - t.Run("empty", func(t *testing.T) { - rng := SeqNumRange{} - assert.Equal(t, SeqNum(0), rng.Start()) - assert.Equal(t, SeqNum(0), rng.End()) - }) - - t.Run("override start and end", func(t *testing.T) { - rng := NewSeqNumRange(1, 2) - rng.SetStart(10) - rng.SetEnd(20) - assert.Equal(t, SeqNum(10), rng.Start()) - assert.Equal(t, SeqNum(20), rng.End()) - }) - - t.Run("string", func(t *testing.T) { - assert.Equal(t, "[1 -> 2]", NewSeqNumRange(1, 2).String()) - assert.Equal(t, "[0 -> 0]", SeqNumRange{}.String()) - }) -} - -func TestSeqNumRange_Overlap(t *testing.T) { - testCases := []struct { - name string - r1 SeqNumRange - r2 SeqNumRange - exp bool - }{ - {"OverlapMiddle", SeqNumRange{5, 10}, SeqNumRange{8, 12}, true}, - {"OverlapStart", SeqNumRange{5, 10}, SeqNumRange{10, 15}, true}, - {"OverlapEnd", SeqNumRange{5, 10}, SeqNumRange{0, 5}, true}, - {"NoOverlapBefore", SeqNumRange{5, 10}, SeqNumRange{0, 4}, false}, - {"NoOverlapAfter", SeqNumRange{5, 10}, SeqNumRange{11, 15}, false}, - {"SameRange", SeqNumRange{5, 10}, SeqNumRange{5, 10}, true}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.exp, tc.r1.Overlaps(tc.r2)) - }) - } -} - -func TestSeqNumRange_Contains(t *testing.T) { - tests := []struct { - name string - r SeqNumRange - seq SeqNum - expected bool - }{ - {"ContainsMiddle", SeqNumRange{5, 10}, SeqNum(7), true}, - {"ContainsStart", SeqNumRange{5, 10}, SeqNum(5), true}, - {"ContainsEnd", SeqNumRange{5, 10}, SeqNum(10), true}, - {"BeforeRange", SeqNumRange{5, 10}, SeqNum(4), false}, - {"AfterRange", SeqNumRange{5, 10}, SeqNum(11), false}, - {"EmptyRange", SeqNumRange{5, 5}, SeqNum(5), true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.expected, tt.r.Contains(tt.seq)) - }) - } -} - -func TestSeqNumRangeLimit(t *testing.T) { - testCases := []struct { - name string - rng SeqNumRange - n uint64 - want SeqNumRange - }{ - { - name: "no truncation", - rng: NewSeqNumRange(0, 10), - n: 11, - want: NewSeqNumRange(0, 10), - }, - { - name: "no truncation 2", - rng: NewSeqNumRange(100, 110), - n: 11, - want: NewSeqNumRange(100, 110), - }, - { - name: "truncation", - rng: NewSeqNumRange(0, 10), - n: 10, - want: NewSeqNumRange(0, 9), - }, - { - name: "truncation 2", - rng: NewSeqNumRange(100, 110), - n: 10, - want: NewSeqNumRange(100, 109), - }, - { - name: "empty", - rng: NewSeqNumRange(0, 0), - n: 0, - want: NewSeqNumRange(0, 0), - }, - { - name: "wrong range", - rng: NewSeqNumRange(20, 15), - n: 3, - want: NewSeqNumRange(20, 15), - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - got := tc.rng.Limit(tc.n) - if got != tc.want { - t.Errorf("SeqNumRangeLimit(%v, %v) = %v; want %v", tc.rng, tc.n, got, tc.want) - } - }) - } -} - -func TestCCIPMsg_String(t *testing.T) { - tests := []struct { - name string - c Message - expected string - }{ - { - "base", - Message{ - Header: RampMessageHeader{ - MessageID: mustNewBytes32(t, "0x01"), - SourceChainSelector: ChainSelector(1), - DestChainSelector: ChainSelector(2), - SequenceNumber: 2, - Nonce: 1, - - MsgHash: mustNewBytes32(t, "0x23"), - OnRamp: mustNewBytes(t, "0x04D4cC5972ad487F71b85654d48b27D32b13a22F"), - }, - }, - `{"header":{"messageId":"0x0100000000000000000000000000000000000000000000000000000000000000","sourceChainSelector":"1","destChainSelector":"2","seqNum":"2","nonce":1,"msgHash":"0x2300000000000000000000000000000000000000000000000000000000000000","onRamp":"0x04d4cc5972ad487f71b85654d48b27d32b13a22f"},"sender":"0x","data":"0x","receiver":"0x","extraArgs":"0x","feeToken":"0x","feeTokenAmount":null,"feeValueJuels":null,"tokenAmounts":null}`, - }, - { - "with evm ramp message", - Message{ - Header: RampMessageHeader{ - MessageID: mustNewBytes32(t, "0x01"), - SourceChainSelector: ChainSelector(1), - DestChainSelector: ChainSelector(2), - SequenceNumber: 2, - Nonce: 1, - - MsgHash: mustNewBytes32(t, "0x23"), - OnRamp: mustNewBytes(t, "0x04D4cC5972ad487F71b85654d48b27D32b13a22F"), - }, - Sender: mustNewBytes(t, "0x04D4cC5972ad487F71b85654d48b27D32b13a22F"), - Receiver: mustNewBytes(t, "0x101112131415"), // simulate a non-evm receiver - Data: []byte("some data"), - ExtraArgs: []byte("extra args"), - FeeToken: mustNewBytes(t, "0xB5fCC870d2aC8745054b4ba99B1f176B93382162"), - FeeTokenAmount: BigInt{Int: big.NewInt(1000)}, - FeeValueJuels: BigInt{Int: big.NewInt(287)}, - TokenAmounts: []RampTokenAmount{ - { - SourcePoolAddress: mustNewBytes(t, "0x3E8456720B88A1DAdce8E2808C9Bf73dfFFd807c"), - DestTokenAddress: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, // simulate a non-evm token - ExtraData: []byte("extra token data"), - Amount: BigInt{Int: big.NewInt(2000)}, - DestExecData: []byte("extra token data"), - }, - }, - }, - `{"header":{"messageId":"0x0100000000000000000000000000000000000000000000000000000000000000","sourceChainSelector":"1","destChainSelector":"2","seqNum":"2","nonce":1,"msgHash":"0x2300000000000000000000000000000000000000000000000000000000000000","onRamp":"0x04d4cc5972ad487f71b85654d48b27d32b13a22f"},"sender":"0x04d4cc5972ad487f71b85654d48b27d32b13a22f","data":"0x736f6d652064617461","receiver":"0x101112131415","extraArgs":"0x65787472612061726773","feeToken":"0xb5fcc870d2ac8745054b4ba99b1f176b93382162","feeTokenAmount":"1000","feeValueJuels":"287","tokenAmounts":[{"sourcePoolAddress":"0x3e8456720b88a1dadce8e2808c9bf73dfffd807c","destTokenAddress":"0x0102030405060708090a","extraData":"0x657874726120746f6b656e2064617461","amount":"2000","destExecData":"0x657874726120746f6b656e2064617461"}]}`, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.expected, test.c.String()) - }) - } -} - -func TestNewTokenPrice(t *testing.T) { - t.Run("base", func(t *testing.T) { - tp := NewTokenPrice("link", big.NewInt(1000)) - assert.Equal(t, "link", string(tp.TokenID)) - assert.Equal(t, uint64(1000), tp.Price.Int.Uint64()) - }) -} - -func TestNewGasPriceChain(t *testing.T) { - t.Run("base", func(t *testing.T) { - gpc := NewGasPriceChain(big.NewInt(1000), ChainSelector(1)) - assert.Equal(t, uint64(1000), (gpc.GasPrice).Uint64()) - assert.Equal(t, ChainSelector(1), gpc.ChainSel) - }) -} - -func TestMerkleRoot(t *testing.T) { - t.Run("str", func(t *testing.T) { - mr := Bytes32([32]byte{1}) - assert.Equal(t, "0x0100000000000000000000000000000000000000000000000000000000000000", mr.String()) - }) - - t.Run("json", func(t *testing.T) { - mr := Bytes32([32]byte{1}) - b, err := json.Marshal(mr) - assert.NoError(t, err) - assert.Equal(t, `"0x0100000000000000000000000000000000000000000000000000000000000000"`, string(b)) - - mr2 := Bytes32{} - err = json.Unmarshal(b, &mr2) - assert.NoError(t, err) - assert.Equal(t, mr, mr2) - - mr3 := Bytes32{} - err = json.Unmarshal([]byte(`"123"`), &mr3) - assert.Error(t, err) - - err = json.Unmarshal([]byte(`""`), &mr3) - assert.Error(t, err) - }) -} - -func mustNewBytes32(t *testing.T, s string) Bytes32 { - b32, err := NewBytes32FromString(s) - require.NoError(t, err) - return b32 -} - -func mustNewBytes(t *testing.T, s string) Bytes { - b, err := NewBytesFromString(s) - require.NoError(t, err) - return b -} diff --git a/pkg/types/ccipocr3/v2/v2/interfaces.go b/pkg/types/ccipocr3/v2/v2/interfaces.go deleted file mode 100644 index 3a263666e..000000000 --- a/pkg/types/ccipocr3/v2/v2/interfaces.go +++ /dev/null @@ -1,40 +0,0 @@ -package ccipocr3 - -import ( - "context" -) - -type CommitPluginCodec interface { - Encode(context.Context, CommitPluginReport) ([]byte, error) - Decode(context.Context, []byte) (CommitPluginReport, error) -} - -type ExecutePluginCodec interface { - Encode(context.Context, ExecutePluginReport) ([]byte, error) - Decode(context.Context, []byte) (ExecutePluginReport, error) -} - -type MessageHasher interface { - Hash(context.Context, Message) (Bytes32, error) -} - -// RMNCrypto provides a chain-agnostic interface for verifying RMN signatures. -// For example, on EVM, RMN reports are abi-encoded prior to being signed. -// On Solana, they would be borsh encoded instead, etc. -type RMNCrypto interface { - // VerifyReportSignatures verifies each provided signature against the provided report and the signer addresses. - // If any signature is invalid (no matching signer address is found), an error is returned immediately. - VerifyReportSignatures( - ctx context.Context, - sigs []RMNECDSASignature, - report RMNReport, - signerAddresses []Bytes, - ) error -} - -// TokenDataEncoder is a generic interface for encoding offchain token data for different chain families. -// Right now it supports only USDC/CCTP, but every new token that requires offchain data processsing -// should be added to that interface and implemented in the downstream repositories (e.g. chainlink-ccip, chainlink). -type TokenDataEncoder interface { - EncodeUSDC(ctx context.Context, message Bytes, attestation Bytes) (Bytes, error) -} diff --git a/pkg/types/ccipocr3/v2/v2/plugin_commit_types.go b/pkg/types/ccipocr3/v2/v2/plugin_commit_types.go deleted file mode 100644 index 665821faf..000000000 --- a/pkg/types/ccipocr3/v2/v2/plugin_commit_types.go +++ /dev/null @@ -1,73 +0,0 @@ -package ccipocr3 - -import "bytes" - -// CommitPluginReport contains the necessary information to commit CCIP -// messages from potentially many source chains, to a single destination chain. -// -// It must consist of either: -// -// 1. a non-empty MerkleRoots array, or -// 2. a non-empty PriceUpdates array -// -// If neither of the above is provided the report is considered empty and should -// not be transmitted on-chain. -// -// In the event the MerkleRoots array is non-empty, it may also contain -// RMNSignatures, if RMN is configured for some lanes involved in the commitment. -// A report with RMN signatures but without merkle roots is invalid. -type CommitPluginReport struct { - MerkleRoots []MerkleRootChain `json:"merkleRoots"` - PriceUpdates PriceUpdates `json:"priceUpdates"` - - // RMNSignatures are the ECDSA signatures from the RMN signing nodes on the RMNReport structure. - // For more details see the contract here: https://github.com/smartcontractkit/chainlink/blob/7ba0f37134a618375542079ff1805fe2224d7916/contracts/src/v0.8/ccip/interfaces/IRMNV2.sol#L8-L12 - RMNSignatures []RMNECDSASignature `json:"rmnSignatures"` - - // RMNRawVs is a 256-bit bitmap. A bit is set if the v value of the signature is 28, - // and unset if the v value is 27. - // Note that this implies that the maximum number of RMN signatures is 256. - RMNRawVs BigInt `json:"rmnRawVs"` -} - -// IsEmpty returns true if the CommitPluginReport is empty -func (r CommitPluginReport) IsEmpty() bool { - return len(r.MerkleRoots) == 0 && - len(r.PriceUpdates.TokenPriceUpdates) == 0 && - len(r.PriceUpdates.GasPriceUpdates) == 0 && - len(r.RMNSignatures) == 0 -} - -// MerkleRootChain Mirroring https://github.com/smartcontractkit/chainlink/blob/cd5c78959575f593b27fd83d8766086d0c678487/contracts/src/v0.8/ccip/libraries/Internal.sol#L356-L362 -type MerkleRootChain struct { - ChainSel ChainSelector `json:"chain"` - OnRampAddress Bytes `json:"onRampAddress"` - SeqNumsRange SeqNumRange `json:"seqNumsRange"` - MerkleRoot Bytes32 `json:"merkleRoot"` -} - -func (m MerkleRootChain) Equals(other MerkleRootChain) bool { - return m.ChainSel == other.ChainSel && - bytes.Equal(m.OnRampAddress, other.OnRampAddress) && - m.SeqNumsRange == other.SeqNumsRange && - m.MerkleRoot == other.MerkleRoot -} - -func NewMerkleRootChain( - chainSel ChainSelector, - onRampAddress Bytes, - seqNumsRange SeqNumRange, - merkleRoot Bytes32, -) MerkleRootChain { - return MerkleRootChain{ - ChainSel: chainSel, - OnRampAddress: onRampAddress, - SeqNumsRange: seqNumsRange, - MerkleRoot: merkleRoot, - } -} - -type PriceUpdates struct { - TokenPriceUpdates []TokenPrice `json:"tokenPriceUpdates"` - GasPriceUpdates []GasPriceChain `json:"gasPriceUpdates"` -} diff --git a/pkg/types/ccipocr3/v2/v2/plugin_commit_types_test.go b/pkg/types/ccipocr3/v2/v2/plugin_commit_types_test.go deleted file mode 100644 index f49b9ef87..000000000 --- a/pkg/types/ccipocr3/v2/v2/plugin_commit_types_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package ccipocr3 - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestCommitPluginReport(t *testing.T) { - t.Run("is empty", func(t *testing.T) { - r := CommitPluginReport{} - assert.True(t, r.IsEmpty()) - }) - - t.Run("is not empty", func(t *testing.T) { - r := CommitPluginReport{ - MerkleRoots: make([]MerkleRootChain, 1), - } - assert.False(t, r.IsEmpty()) - - r = CommitPluginReport{ - PriceUpdates: PriceUpdates{ - TokenPriceUpdates: make([]TokenPrice, 1), - GasPriceUpdates: make([]GasPriceChain, 1), - }, - } - assert.False(t, r.IsEmpty()) - - r = CommitPluginReport{ - RMNSignatures: make([]RMNECDSASignature, 1), - } - assert.False(t, r.IsEmpty()) - - r = CommitPluginReport{ - MerkleRoots: make([]MerkleRootChain, 1), - PriceUpdates: PriceUpdates{ - TokenPriceUpdates: make([]TokenPrice, 1), - GasPriceUpdates: make([]GasPriceChain, 1), - }, - RMNSignatures: make([]RMNECDSASignature, 1), - } - assert.False(t, r.IsEmpty()) - }) -} - -func TestMerkleRootChain_Equals_Structs(t *testing.T) { - tests := []struct { - name string - m1 MerkleRootChain - m2 MerkleRootChain - expected bool - }{ - { - name: "equal MerkleRootChains", - m1: MerkleRootChain{ - ChainSel: ChainSelector(1), - OnRampAddress: []byte{0x01, 0x02}, - SeqNumsRange: SeqNumRange{1, 10}, - MerkleRoot: Bytes32{0x01, 0x02, 0x03}, - }, - m2: MerkleRootChain{ - ChainSel: ChainSelector(1), - OnRampAddress: []byte{0x01, 0x02}, - SeqNumsRange: SeqNumRange{1, 10}, - MerkleRoot: Bytes32{0x01, 0x02, 0x03}, - }, - expected: true, - }, - { - name: "different ChainSel", - m1: MerkleRootChain{ - ChainSel: ChainSelector(1), - OnRampAddress: []byte{0x01, 0x02}, - SeqNumsRange: SeqNumRange{1, 10}, - MerkleRoot: Bytes32{0x01, 0x02, 0x03}, - }, - m2: MerkleRootChain{ - ChainSel: ChainSelector(2), - OnRampAddress: []byte{0x01, 0x02}, - SeqNumsRange: SeqNumRange{1, 10}, - MerkleRoot: Bytes32{0x01, 0x02, 0x03}, - }, - expected: false, - }, - { - name: "different OnRampAddress", - m1: MerkleRootChain{ - ChainSel: ChainSelector(1), - OnRampAddress: []byte{0x01, 0x02}, - SeqNumsRange: SeqNumRange{1, 10}, - MerkleRoot: Bytes32{0x01, 0x02, 0x03}, - }, - m2: MerkleRootChain{ - ChainSel: ChainSelector(1), - OnRampAddress: []byte{0x03, 0x04}, - SeqNumsRange: SeqNumRange{1, 10}, - MerkleRoot: Bytes32{0x01, 0x02, 0x03}, - }, - expected: false, - }, - { - name: "different SeqNumsRange", - m1: MerkleRootChain{ - ChainSel: ChainSelector(1), - OnRampAddress: []byte{0x01, 0x02}, - SeqNumsRange: SeqNumRange{1, 10}, - MerkleRoot: Bytes32{0x01, 0x02, 0x03}, - }, - m2: MerkleRootChain{ - ChainSel: ChainSelector(1), - OnRampAddress: []byte{0x01, 0x02}, - SeqNumsRange: SeqNumRange{2, 20}, - MerkleRoot: Bytes32{0x01, 0x02, 0x03}, - }, - expected: false, - }, - { - name: "different MerkleRoot", - m1: MerkleRootChain{ - ChainSel: ChainSelector(1), - OnRampAddress: []byte{0x01, 0x02}, - SeqNumsRange: SeqNumRange{1, 10}, - MerkleRoot: Bytes32{0x01, 0x02, 0x03}, - }, - m2: MerkleRootChain{ - ChainSel: ChainSelector(1), - OnRampAddress: []byte{0x01, 0x02}, - SeqNumsRange: SeqNumRange{1, 10}, - MerkleRoot: Bytes32{0x04, 0x05, 0x06}, - }, - expected: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.expected, tt.m1.Equals(tt.m2)) - }) - } -} diff --git a/pkg/types/ccipocr3/v2/v2/plugin_execute_types.go b/pkg/types/ccipocr3/v2/v2/plugin_execute_types.go deleted file mode 100644 index 61e813226..000000000 --- a/pkg/types/ccipocr3/v2/v2/plugin_execute_types.go +++ /dev/null @@ -1,13 +0,0 @@ -package ccipocr3 - -type ExecutePluginReport struct { - ChainReports []ExecutePluginReportSingleChain `json:"chainReports"` -} - -type ExecutePluginReportSingleChain struct { - SourceChainSelector ChainSelector `json:"sourceChainSelector"` - Messages []Message `json:"messages"` - OffchainTokenData [][][]byte `json:"offchainTokenData"` - Proofs []Bytes32 `json:"proofs"` - ProofFlagBits BigInt `json:"proofFlagBits"` -} diff --git a/pkg/types/ccipocr3/v2/v2/rmn_types.go b/pkg/types/ccipocr3/v2/v2/rmn_types.go deleted file mode 100644 index 8d8839495..000000000 --- a/pkg/types/ccipocr3/v2/v2/rmn_types.go +++ /dev/null @@ -1,30 +0,0 @@ -package ccipocr3 - -// RMNReport is the payload that is signed by the RMN nodes, transmitted and verified onchain. -type RMNReport struct { - ReportVersion string // e.g. "RMN_V1_6_ANY2EVM_REPORT". - DestChainID BigInt // If applies, a chain specific id, e.g. evm chain id otherwise empty. - DestChainSelector ChainSelector - RmnRemoteContractAddress Bytes - OfframpAddress Bytes - RmnHomeContractConfigDigest Bytes32 - LaneUpdates []RMNLaneUpdate -} - -// RMNLaneUpdate represents an interval that has been observed by an RMN node. -// It is part of the payload that is signed and transmitted onchain. -type RMNLaneUpdate struct { - SourceChainSelector ChainSelector - OnRampAddress Bytes // (for EVM should be abi-encoded) - MinSeqNr SeqNum - MaxSeqNr SeqNum - MerkleRoot Bytes32 -} - -// RMNECDSASignature represents the signature provided by RMN on the RMNReport structure. -// The V value of the signature is included in the top-level commit report as part of a -// bitmap. -type RMNECDSASignature struct { - R Bytes32 `json:"r"` - S Bytes32 `json:"s"` -}