diff --git a/.changeset/curvy-boxes-burn.md b/.changeset/curvy-boxes-burn.md new file mode 100644 index 00000000000..8a189c5f5d1 --- /dev/null +++ b/.changeset/curvy-boxes-burn.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +RMNCrypto evm implementation for CCIP - RMN Integration #added diff --git a/core/capabilities/ccip/ccipevm/helpers.go b/core/capabilities/ccip/ccipevm/helpers.go index ee83230a4ce..4cfd64b7c65 100644 --- a/core/capabilities/ccip/ccipevm/helpers.go +++ b/core/capabilities/ccip/ccipevm/helpers.go @@ -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) { @@ -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 +} diff --git a/core/capabilities/ccip/ccipevm/msghasher.go b/core/capabilities/ccip/ccipevm/msghasher.go index e620d96a43a..cf37a28b003 100644 --- a/core/capabilities/ccip/ccipevm/msghasher.go +++ b/core/capabilities/ccip/ccipevm/msghasher.go @@ -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), @@ -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), @@ -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), @@ -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 diff --git a/core/capabilities/ccip/ccipevm/rmncrypto.go b/core/capabilities/ccip/ccipevm/rmncrypto.go new file mode 100644 index 00000000000..794a466524f --- /dev/null +++ b/core/capabilities/ccip/ccipevm/rmncrypto.go @@ -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 +} diff --git a/core/capabilities/ccip/ccipevm/rmncrypto_test.go b/core/capabilities/ccip/ccipevm/rmncrypto_test.go new file mode 100644 index 00000000000..e12b20ab304 --- /dev/null +++ b/core/capabilities/ccip/ccipevm/rmncrypto_test.go @@ -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) +}