-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ccip - EVM Implementation of RMNCrypto interface (#14416)
* 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
Showing
5 changed files
with
274 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"chainlink": patch | ||
--- | ||
|
||
RMNCrypto evm implementation for CCIP - RMN Integration #added |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |