Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

ccip - EVM Implementation of RMNCrypto interface #14416

Merged
merged 14 commits into from
Sep 17, 2024
5 changes: 5 additions & 0 deletions .changeset/curvy-boxes-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink": patch
---

RMNCrypto evm implementation for CCIP - RMN Integration #added
12 changes: 12 additions & 0 deletions core/capabilities/ccip/ccipevm/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"bytes"
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/accounts/abi"
)

func decodeExtraArgsV1V2(extraArgs []byte) (gasLimit *big.Int, err error) {
Expand Down Expand Up @@ -31,3 +33,13 @@ func decodeExtraArgsV1V2(extraArgs []byte) (gasLimit *big.Int, err error) {
}
return ifaces[0].(*big.Int), nil
}

// abiEncodeMethodInputs encodes the inputs for a method call.
// example abi: `[{ "name" : "method", "type": "function", "inputs": [{"name": "a", "type": "uint256"}]}]`
func abiEncodeMethodInputs(abiDef abi.ABI, inputs ...interface{}) ([]byte, error) {
packed, err := abiDef.Pack("method", inputs...)
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm I thought the name matters here, at least for the contract ABI's, or is that not the case in this particular scenario?

Copy link
Contributor

Choose a reason for hiding this comment

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

I see this only used in one place and the method is indeed called method, so thats why this worse. Might be worth checking this with a test, but I feel like this will fail if there is no method called method in the ABI

Copy link
Contributor Author

Choose a reason for hiding this comment

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

or is that not the case in this particular scenario?

It's not the case,this is similar to https://github.com/smartcontractkit/chainlink/blob/e37f7e2d1a1859a0853ad6ecfbe3c339c02d2248/core/chains/evm/utils/ethabi.go#L34-L33 but more performant, since it doesn't parse the abi on each call. That's why i didn't re-use what we have.

if err != nil {
return nil, err
}
return packed[4:], nil // remove the method selector
}
10 changes: 5 additions & 5 deletions core/capabilities/ccip/ccipevm/msghasher.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,12 @@ func (h *MessageHasherV1) Hash(_ context.Context, msg cciptypes.Message) (ccipty
Amount: rta.Amount.Int,
})
}
encodedRampTokenAmounts, err := abiEncode("encodeTokenAmountsHashPreimage", rampTokenAmounts)
encodedRampTokenAmounts, err := h.abiEncode("encodeTokenAmountsHashPreimage", rampTokenAmounts)
if err != nil {
return [32]byte{}, fmt.Errorf("abi encode token amounts: %w", err)
}

metaDataHashInput, err := abiEncode(
metaDataHashInput, err := h.abiEncode(
"encodeMetadataHashPreimage",
ANY_2_EVM_MESSAGE_HASH,
uint64(msg.Header.SourceChainSelector),
Expand All @@ -86,7 +86,7 @@ func (h *MessageHasherV1) Hash(_ context.Context, msg cciptypes.Message) (ccipty
return [32]byte{}, fmt.Errorf("decode extra args: %w", err)
}

fixedSizeFieldsEncoded, err := abiEncode(
fixedSizeFieldsEncoded, err := h.abiEncode(
"encodeFixedSizeFieldsHashPreimage",
msg.Header.MessageID,
[]byte(msg.Sender),
Expand All @@ -99,7 +99,7 @@ func (h *MessageHasherV1) Hash(_ context.Context, msg cciptypes.Message) (ccipty
return [32]byte{}, fmt.Errorf("abi encode fixed size values: %w", err)
}

packedValues, err := abiEncode(
packedValues, err := h.abiEncode(
"encodeFinalHashPreimage",
leafDomainSeparator,
utils.Keccak256Fixed(metaDataHashInput),
Expand All @@ -114,7 +114,7 @@ func (h *MessageHasherV1) Hash(_ context.Context, msg cciptypes.Message) (ccipty
return utils.Keccak256Fixed(packedValues), nil
}

func abiEncode(method string, values ...interface{}) ([]byte, error) {
func (h *MessageHasherV1) abiEncode(method string, values ...interface{}) ([]byte, error) {
res, err := messageHasherABI.Pack(method, values...)
if err != nil {
return nil, err
Expand Down
179 changes: 179 additions & 0 deletions core/capabilities/ccip/ccipevm/rmncrypto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package ccipevm

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

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3"
)

// encodingUtilsAbi is the ABI for the EncodingUtils contract.
// Should be imported when gethwrappers are moved from ccip repo to core.
// nolint:lll
const encodingUtilsAbiRaw = `[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"DoNotDeploy","type":"error"},{"inputs":[{"internalType":"bytes32","name":"rmnReportVersion","type":"bytes32"},{"components":[{"internalType":"uint256","name":"destChainId","type":"uint256"},{"internalType":"uint64","name":"destChainSelector","type":"uint64"},{"internalType":"address","name":"rmnRemoteContractAddress","type":"address"},{"internalType":"address","name":"offrampAddress","type":"address"},{"internalType":"bytes32","name":"rmnHomeContractConfigDigest","type":"bytes32"},{"components":[{"internalType":"uint64","name":"sourceChainSelector","type":"uint64"},{"internalType":"bytes","name":"onRampAddress","type":"bytes"},{"internalType":"uint64","name":"minSeqNr","type":"uint64"},{"internalType":"uint64","name":"maxSeqNr","type":"uint64"},{"internalType":"bytes32","name":"merkleRoot","type":"bytes32"}],"internalType":"struct Internal.MerkleRoot[]","name":"destLaneUpdates","type":"tuple[]"}],"internalType":"struct RMNRemote.Report","name":"rmnReport","type":"tuple"}],"name":"_rmnReport","outputs":[],"stateMutability":"nonpayable","type":"function"}]`
const addressEncodeAbiRaw = `[{"name":"method","type":"function","inputs":[{"name": "", "type": "address"}]}]`

var (
EncodingUtilsABI abi.ABI
AddressEncodeABI abi.ABI
Copy link
Contributor

Choose a reason for hiding this comment

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

Can be private?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sure

)

func init() {
var err error

EncodingUtilsABI, err = abi.JSON(strings.NewReader(encodingUtilsAbiRaw))
if err != nil {
panic(fmt.Errorf("failed to parse encoding utils ABI: %v", err))
}

AddressEncodeABI, err = abi.JSON(strings.NewReader(addressEncodeAbiRaw))
if err != nil {
panic(fmt.Errorf("failed to parse address encode ABI: %v", err))
}
}

// EVMRMNCrypto is the RMNCrypto implementation for EVM chains.
type EVMRMNCrypto struct{}

// Interface compliance check
var _ cciptypes.RMNCrypto = (*EVMRMNCrypto)(nil)

func NewEVMRMNCrypto() *EVMRMNCrypto {
return &EVMRMNCrypto{}
}

type evmRMNRemoteReport struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe add a TODO also to use the types from the gethwrappers when they're available?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added

DestChainId *big.Int

Check failure on line 53 in core/capabilities/ccip/ccipevm/rmncrypto.go

View workflow job for this annotation

GitHub Actions / lint

var-naming: struct field DestChainId should be DestChainID (revive)
DestChainSelector uint64
RmnRemoteContractAddress common.Address
OfframpAddress common.Address
RmnHomeContractConfigDigest [32]byte
DestLaneUpdates []evmInternalMerkleRoot
}

type evmInternalMerkleRoot struct {
SourceChainSelector uint64
OnRampAddress []byte
MinSeqNr uint64
MaxSeqNr uint64
MerkleRoot [32]byte
}

func (r *EVMRMNCrypto) VerifyReportSignatures(
_ context.Context,
sigs []cciptypes.RMNECDSASignature,
report cciptypes.RMNReport,
signerAddresses []cciptypes.Bytes,
) error {
const v = 27 // used in ecrecover
Copy link
Contributor

Choose a reason for hiding this comment

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

Can pull this out to a const block outside this object

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sure

Copy link
Contributor Author

Choose a reason for hiding this comment

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

also added a more proper comment


if sigs == nil {
return fmt.Errorf("no signatures provided")
}
if report.LaneUpdates == nil {
return fmt.Errorf("no lane updates provided")
}

rmnVersionHash := crypto.Keccak256Hash([]byte(report.ReportVersion))
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if this version hash should be pulled out into a var (similar to how we have the LEAF_DOMAIN_SEPARATOR for the message hasher) and have this struct versioned (i.e EVMRMNCryptoV1_6 for V1.6)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm could be but then the interface/common-types might also not be compatible with next versions.
I recommend waiting for next version before defining a generic report type to use across different versions.


evmLaneUpdates := make([]evmInternalMerkleRoot, len(report.LaneUpdates))
for i, lu := range report.LaneUpdates {
onRampAddress := common.BytesToAddress(lu.OnRampAddress)
onRampAddrAbi, err := abiEncodeMethodInputs(AddressEncodeABI, onRampAddress)
if err != nil {
return fmt.Errorf("ΑΒΙ encode onRampAddress: %w", err)
}
evmLaneUpdates[i] = evmInternalMerkleRoot{
SourceChainSelector: uint64(lu.SourceChainSelector),
OnRampAddress: onRampAddrAbi,
MinSeqNr: uint64(lu.MinSeqNr),
MaxSeqNr: uint64(lu.MaxSeqNr),
MerkleRoot: lu.MerkleRoot,
}
}

evmReport := evmRMNRemoteReport{
DestChainId: report.DestChainID.Int,
DestChainSelector: uint64(report.DestChainSelector),
RmnRemoteContractAddress: common.HexToAddress(report.RmnRemoteContractAddress.String()),
OfframpAddress: common.HexToAddress(report.OfframpAddress.String()),
RmnHomeContractConfigDigest: report.RmnHomeContractConfigDigest,
DestLaneUpdates: evmLaneUpdates,
}

abiEnc, err := EncodingUtilsABI.Methods["_rmnReport"].Inputs.Pack(rmnVersionHash, evmReport)
if err != nil {
return fmt.Errorf("failed to ABI encode args: %w", err)
}

signedHash := crypto.Keccak256Hash(abiEnc)

// keep track of the previous signer for validating signers ordering
prevSignerAddr := common.Address{}

for _, sig := range sigs {
recoveredAddress, err := recoverAddressFromSig(
v,
sig.R,
sig.S,
signedHash[:],
)
if err != nil {
return fmt.Errorf("failed to recover public key from signature: %w", err)
}

// make sure that signers are ordered correctly (ASC addresses).
if bytes.Compare(prevSignerAddr.Bytes(), recoveredAddress.Bytes()) == 1 {
return fmt.Errorf("signers are not ordered correctly")
}
prevSignerAddr = recoveredAddress

// Check if the public key is in the list of the provided RMN nodes
found := false
for _, signerAddr := range signerAddresses {
signerAddrEvm := common.BytesToAddress(signerAddr)
if signerAddrEvm == recoveredAddress {
found = true
break
}
}
if !found {
return fmt.Errorf("the recovered public key does not match any signer address, verification failed")
}
}

return nil
}

// recoverAddressFromSig Recovers a public address from an ECDSA signature using r, s, v, and the hash of the message.
func recoverAddressFromSig(v int, r, s [32]byte, hash []byte) (common.Address, error) {
// Ensure v is either 27 or 28 (as used in Ethereum)
if v != 27 && v != 28 {
return common.Address{}, errors.New("v must be 27 or 28")
}

// Construct the signature by concatenating r, s, and the recovery ID (v - 27 to convert to 0/1)
sig := append(r[:], s[:]...)
sig = append(sig, byte(v-27))

// Recover the public key bytes from the signature and message hash
pubKeyBytes, err := crypto.Ecrecover(hash, sig)
if err != nil {
return common.Address{}, fmt.Errorf("failed to recover public key: %v", err)
}

// Convert the recovered public key to an ECDSA public key
pubKey, err := crypto.UnmarshalPubkey(pubKeyBytes)
if err != nil {
return common.Address{}, fmt.Errorf("failed to unmarshal public key: %v", err)
} // or SigToPub

return crypto.PubkeyToAddress(*pubKey), nil
}
66 changes: 66 additions & 0 deletions core/capabilities/ccip/ccipevm/rmncrypto_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package ccipevm

import (
"testing"

"github.com/ethereum/go-ethereum/common"
cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3"
"github.com/smartcontractkit/chainlink-common/pkg/utils/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func Test_VerifyRmnReportSignatures(t *testing.T) {
// NOTE: The following test data (public keys, signatures, ...) are shared from the RMN team.

onchainRmnRemoteAddr := common.HexToAddress("0x7821bcd6944457d17c631157efeb0c621baa76eb")

rmnHomeContractConfigDigestHex := "0x785936570d1c7422ef30b7da5555ad2f175fa2dd97a2429a2e71d1e07c94e060"
rmnHomeContractConfigDigest := common.FromHex(rmnHomeContractConfigDigestHex)
require.Len(t, rmnHomeContractConfigDigest, 32)
var rmnHomeContractConfigDigest32 [32]byte
copy(rmnHomeContractConfigDigest32[:], rmnHomeContractConfigDigest)

rootHex := "0x48e688aefc20a04fdec6b8ff19df358fd532455659dcf529797cda358e9e5205"
root := common.FromHex(rootHex)
require.Len(t, root, 32)
var root32 [32]byte
copy(root32[:], root)

onRampAddr := common.HexToAddress("0x6662cb20464f4be557262693bea0409f068397ed")

destChainEvmID := uint64(4083663998511321420)

reportData := cciptypes.RMNReport{
ReportVersion: "RMN_V1_6_ANY2EVM_REPORT",
DestChainID: cciptypes.NewBigIntFromInt64(int64(destChainEvmID)),

Check failure on line 36 in core/capabilities/ccip/ccipevm/rmncrypto_test.go

View workflow job for this annotation

GitHub Actions / lint

G115: integer overflow conversion uint64 -> int64 (gosec)
DestChainSelector: 5266174733271469989,
RmnRemoteContractAddress: common.HexToAddress("0x3d015cec4411357eff4ea5f009a581cc519f75d3").Bytes(),
OfframpAddress: common.HexToAddress("0xc5cdb7711a478058023373b8ae9e7421925140f8").Bytes(),
RmnHomeContractConfigDigest: rmnHomeContractConfigDigest32,
LaneUpdates: []cciptypes.RMNLaneUpdate{
{
SourceChainSelector: 8258882951688608272,
OnRampAddress: onRampAddr.Bytes(),
MinSeqNr: 9018980618932210108,
MaxSeqNr: 8239368306600774074,
MerkleRoot: root32,
},
},
}

ctx := tests.Context(t)

rmnCrypto := NewEVMRMNCrypto()

r, _ := cciptypes.NewBytes32FromString("0x89546b4652d0377062a398e413344e4da6034ae877c437d0efe0e5246b70a9a1")
s, _ := cciptypes.NewBytes32FromString("0x95eef2d24d856ccac3886db8f4aebea60684ed73942392692908fed79a679b4e")

err := rmnCrypto.VerifyReportSignatures(
ctx,
[]cciptypes.RMNECDSASignature{{R: r, S: s}},
reportData,
[]cciptypes.Bytes{onchainRmnRemoteAddr.Bytes()},
)
assert.NoError(t, err)
}
Loading