-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Changes from 8 commits
f0785ec
9791c77
b7b21eb
15f9df9
2eaaf3e
ab6c88a
9f3af71
bba042d
a67d04c
f70efc5
bd09438
b3ed3aa
3659bb5
59effa1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"chainlink": patch | ||
--- | ||
|
||
RMNCrypto evm implementation for CCIP - RMN Integration #added |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can be private? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added |
||
DestChainId *big.Int | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can pull this out to a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sure There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if this version hash should be pulled out into a There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
||
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 | ||
} |
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)), | ||
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) | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 calledmethod
in the ABIThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.