Skip to content

Commit

Permalink
ccip - EVM Implementation of RMNCrypto interface (#14416)
Browse files Browse the repository at this point in the history
* upgrade cl-common to RMNCrypto iface branch

* implement RMN crypto evm ecdsa sig verifier

* personal code review

* no panics

* changeset

* add comment

* fix linter errs

* makramkd code review fixes

* goimports
  • Loading branch information
dimkouv authored Sep 17, 2024
1 parent d14a9b5 commit 3c5bdf8
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 5 deletions.
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...)
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
184 changes: 184 additions & 0 deletions core/capabilities/ccip/ccipevm/rmncrypto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
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
)

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))
}
}

const (
// v is the recovery ID for ECDSA signatures. This implementation assumes that v is always 27.
v = 27
)

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

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

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

// Should be replaced by gethwrapper types when they're available
type evmRMNRemoteReport struct {
DestChainID *big.Int `abi:"destChainId"`
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 {
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))

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
}
68 changes: 68 additions & 0 deletions core/capabilities/ccip/ccipevm/rmncrypto_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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 := int64(4083663998511321420)

reportData := cciptypes.RMNReport{
ReportVersion: "RMN_V1_6_ANY2EVM_REPORT",
DestChainID: cciptypes.NewBigIntFromInt64(destChainEvmID),
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)
}

0 comments on commit 3c5bdf8

Please sign in to comment.