diff --git a/.changeset/silver-pens-float.md b/.changeset/silver-pens-float.md new file mode 100644 index 00000000000..e8a135b9de4 --- /dev/null +++ b/.changeset/silver-pens-float.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +#internal: Adding test for rmn home reader diff --git a/core/capabilities/ccip/ccip_integration_tests/integrationhelpers/integration_helpers.go b/core/capabilities/ccip/ccip_integration_tests/integrationhelpers/integration_helpers.go index 0674d77bfe9..fc31471c149 100644 --- a/core/capabilities/ccip/ccip_integration_tests/integrationhelpers/integration_helpers.go +++ b/core/capabilities/ccip/ccip_integration_tests/integrationhelpers/integration_helpers.go @@ -2,6 +2,8 @@ package integrationhelpers import ( "context" + "crypto/ed25519" + "encoding/hex" "encoding/json" "fmt" "math/big" @@ -9,14 +11,18 @@ import ( "testing" "time" + mapset "github.com/deckarep/golang-set/v2" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" + "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-ccip/pkg/consts" ccipreader "github.com/smartcontractkit/chainlink-ccip/pkg/reader" + "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" "github.com/smartcontractkit/chainlink-common/pkg/types" - "github.com/stretchr/testify/require" configsevm "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/configs/evm" cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" @@ -25,6 +31,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/chains/evm/headtracker" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ccip_home" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/rmn_home" kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" @@ -80,15 +87,16 @@ const ( ) type TestUniverse struct { - Transactor *bind.TransactOpts - Backend *backends.SimulatedBackend - CapReg *kcr.CapabilitiesRegistry - CCIPHome *ccip_home.CCIPHome - TestingT *testing.T - LogPoller logpoller.LogPoller - HeadTracker logpoller.HeadTracker - SimClient client.Client - HomeChainReader ccipreader.HomeChain + Transactor *bind.TransactOpts + Backend *backends.SimulatedBackend + CapReg *kcr.CapabilitiesRegistry + CCIPHome *ccip_home.CCIPHome + TestingT *testing.T + LogPoller logpoller.LogPoller + HeadTracker logpoller.HeadTracker + SimClient client.Client + HomeChainReader ccipreader.HomeChain + HomeContractReader types.ContractReader } func NewTestUniverse(ctx context.Context, t *testing.T, lggr logger.Logger) TestUniverse { @@ -128,17 +136,19 @@ func NewTestUniverse(ctx context.Context, t *testing.T, lggr logger.Logger) Test require.NoError(t, lp.Start(ctx)) t.Cleanup(func() { require.NoError(t, lp.Close()) }) - hcr := NewHomeChainReader(t, lp, headTracker, cl, ccAddress) + cr := NewReader(t, lp, headTracker, cl, ccAddress, configsevm.HomeChainReaderConfigRaw) + hcr := NewHomeChainReader(t, cr, ccAddress) return TestUniverse{ - Transactor: transactor, - Backend: backend, - CapReg: capReg, - CCIPHome: cc, - TestingT: t, - SimClient: cl, - LogPoller: lp, - HeadTracker: headTracker, - HomeChainReader: hcr, + Transactor: transactor, + Backend: backend, + CapReg: capReg, + CCIPHome: cc, + TestingT: t, + SimClient: cl, + LogPoller: lp, + HeadTracker: headTracker, + HomeChainReader: hcr, + HomeContractReader: cr, } } @@ -227,9 +237,7 @@ func (t *TestUniverse) AddCapability(p2pIDs [][32]byte) { } } -func NewHomeChainReader(t *testing.T, logPoller logpoller.LogPoller, headTracker logpoller.HeadTracker, client client.Client, ccAddress common.Address) ccipreader.HomeChain { - cr := NewReader(t, logPoller, headTracker, client, ccAddress, configsevm.HomeChainReaderConfigRaw) - +func NewHomeChainReader(t *testing.T, cr types.ContractReader, ccAddress common.Address) ccipreader.HomeChain { hcr := ccipreader.NewHomeChainReader(cr, logger.TestLogger(t), 50*time.Millisecond, types.BoundContract{ Address: ccAddress.String(), Name: consts.ContractNameCCIPConfig, @@ -365,3 +373,65 @@ func SetupConfigInfo(chainSelector uint64, readers [][32]byte, fChain uint8, cfg }, } } + +func GenerateRMNHomeConfigs( + peerID string, + offchainPK string, + offchainCfg string, + chainSelector uint64, + minObservers uint64, + observerBitmap *big.Int) (rmn_home.RMNHomeStaticConfig, rmn_home.RMNHomeDynamicConfig, error) { + peerIDByte, _ := hex.DecodeString(peerID) + var peerIDBytes [32]byte + copy(peerIDBytes[:], peerIDByte) + + offchainPublicKey, err := hex.DecodeString(offchainPK) + + if err != nil { + return rmn_home.RMNHomeStaticConfig{}, rmn_home.RMNHomeDynamicConfig{}, fmt.Errorf("error decoding offchain public key: %w", err) + } + + var offchainPublicKeyBytes [32]byte + copy(offchainPublicKeyBytes[:], offchainPublicKey) + + staticConfig := rmn_home.RMNHomeStaticConfig{ + Nodes: []rmn_home.RMNHomeNode{ + { + PeerId: peerIDBytes, + OffchainPublicKey: offchainPublicKeyBytes, + }, + }, + OffchainConfig: []byte(offchainCfg), + } + + dynamicConfig := rmn_home.RMNHomeDynamicConfig{ + SourceChains: []rmn_home.RMNHomeSourceChain{ + { + ChainSelector: chainSelector, + MinObservers: minObservers, + ObserverNodesBitmap: observerBitmap, + }, + }, + OffchainConfig: []byte(offchainCfg), + } + return staticConfig, dynamicConfig, nil +} + +func GenerateExpectedRMNHomeNodesInfo(staticConfig rmn_home.RMNHomeStaticConfig, chainID int) []ccipreader.HomeNodeInfo { + expectedCandidateNodesInfo := make([]ccipreader.HomeNodeInfo, 0) + + supportedCandidateSourceChains := mapset.NewSet(ccipocr3.ChainSelector(chainID)) + + var counter uint32 + for _, n := range staticConfig.Nodes { + pk := ed25519.PublicKey(n.OffchainPublicKey[:]) + expectedCandidateNodesInfo = append(expectedCandidateNodesInfo, ccipreader.HomeNodeInfo{ + ID: ccipreader.NodeID(counter), + PeerID: n.PeerId, + SupportedSourceChains: supportedCandidateSourceChains, + OffchainPublicKey: &pk, + }) + counter++ + } + return expectedCandidateNodesInfo +} diff --git a/core/capabilities/ccip/ccip_integration_tests/rmn/rmn_home_test.go b/core/capabilities/ccip/ccip_integration_tests/rmn/rmn_home_test.go new file mode 100644 index 00000000000..12a207ea5b9 --- /dev/null +++ b/core/capabilities/ccip/ccip_integration_tests/rmn/rmn_home_test.go @@ -0,0 +1,174 @@ +package rmn + +import ( + "bytes" + "math/big" + "slices" + "testing" + "time" + + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccip_integration_tests/integrationhelpers" + + "github.com/smartcontractkit/chainlink-ccip/pkg/consts" + "github.com/smartcontractkit/chainlink-common/pkg/types" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + + readerpkg "github.com/smartcontractkit/chainlink-ccip/pkg/reader" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/logger" + + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/rmn_home" +) + +func TestRMNHomeReader_GetRMNNodesInfo(t *testing.T) { + ctx := testutils.Context(t) + lggr := logger.TestLogger(t) + uni := integrationhelpers.NewTestUniverse(ctx, t, lggr) + zeroBytes := [32]byte{0} + + const ( + chainID1 = 1 + minObservers1 = 1 + observerBitmap1 = 1 + + chainID2 = 2 + minObservers2 = 0 + observerBitmap2 = 1 + ) + + //================================Deploy and configure RMNHome=============================== + rmnHomeAddress, _, rmnHome, err := rmn_home.DeployRMNHome(uni.Transactor, uni.Backend) + require.NoError(t, err) + uni.Backend.Commit() + + staticConfig, dynamicConfig, err := integrationhelpers.GenerateRMNHomeConfigs( + "PeerID1", + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "This is a sample offchain configuration in the static config", + chainID1, + minObservers1, + big.NewInt(observerBitmap1), + ) + require.NoError(t, err) + + _, err = rmnHome.SetCandidate(uni.Transactor, staticConfig, dynamicConfig, zeroBytes) + require.NoError(t, err) + uni.Backend.Commit() + + configDigest, err := rmnHome.GetCandidateDigest(&bind.CallOpts{}) + require.NoError(t, err) + + _, err = rmnHome.PromoteCandidateAndRevokeActive(uni.Transactor, configDigest, zeroBytes) + require.NoError(t, err) + uni.Backend.Commit() + + rmnHomeBoundContract := types.BoundContract{ + Address: rmnHomeAddress.String(), + Name: consts.ContractNameRMNHome, + } + + err = uni.HomeContractReader.Bind(testutils.Context(t), []types.BoundContract{rmnHomeBoundContract}) + require.NoError(t, err) + + rmnHomeReader := readerpkg.NewRMNHomePoller(uni.HomeContractReader, rmnHomeBoundContract, lggr, 100*time.Millisecond) + + err = rmnHomeReader.Start(testutils.Context(t)) + require.NoError(t, err) + + t.Cleanup(func() { + err1 := rmnHomeReader.Close() + require.NoError(t, err1) + }) + + //================================Test RMNHome Reader=============================== + expectedNodesInfo := integrationhelpers.GenerateExpectedRMNHomeNodesInfo(staticConfig, chainID1) + + require.Eventually( + t, + assertRMNHomeNodesInfo(t, rmnHomeReader, configDigest, expectedNodesInfo, nil), + 5*time.Second, + 100*time.Millisecond, + ) + + // Add a new candidate config + staticConfig2, dynamicConfig2, err := integrationhelpers.GenerateRMNHomeConfigs( + "PeerID2", + "1123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "This is a sample offchain configuration in the static config 2", + chainID2, + minObservers2, + big.NewInt(observerBitmap2), + ) + require.NoError(t, err) + + _, err = rmnHome.SetCandidate(uni.Transactor, staticConfig2, dynamicConfig2, zeroBytes) + require.NoError(t, err) + uni.Backend.Commit() + + candidateConfigDigest, err := rmnHome.GetCandidateDigest(&bind.CallOpts{}) + require.NoError(t, err) + + expectedCandidateNodesInfo := integrationhelpers.GenerateExpectedRMNHomeNodesInfo(staticConfig2, chainID2) + + require.Eventually( + t, + assertRMNHomeNodesInfo(t, rmnHomeReader, candidateConfigDigest, expectedCandidateNodesInfo, nil), + 5*time.Second, + 100*time.Millisecond, + ) + + // Promote the candidate config + _, err = rmnHome.PromoteCandidateAndRevokeActive(uni.Transactor, candidateConfigDigest, configDigest) + require.NoError(t, err) + uni.Backend.Commit() + + require.Eventually( + t, + assertRMNHomeNodesInfo(t, rmnHomeReader, candidateConfigDigest, expectedCandidateNodesInfo, &configDigest), + 5*time.Second, + 100*time.Millisecond, + ) +} + +func assertRMNHomeNodesInfo( + t *testing.T, + rmnHomeReader readerpkg.RMNHome, + configDigest [32]byte, + expectedNodesInfo []readerpkg.HomeNodeInfo, + prevConfigDigest *[32]byte, +) func() bool { + return func() bool { + nodesInfo, err := rmnHomeReader.GetRMNNodesInfo(configDigest) + if err != nil { + t.Logf("Error getting RMN nodes info: %v", err) + return false + } + + equal := slices.EqualFunc(expectedNodesInfo, nodesInfo, func(a, b readerpkg.HomeNodeInfo) bool { + return a.ID == b.ID && + a.PeerID == b.PeerID && + bytes.Equal(*a.OffchainPublicKey, *b.OffchainPublicKey) && + a.SupportedSourceChains.Equal(b.SupportedSourceChains) + }) + + if !equal { + t.Logf("Expected nodes info doesn't match actual nodes info") + t.Logf("Expected: %+v", expectedNodesInfo) + t.Logf("Actual: %+v", nodesInfo) + return false + } + + if prevConfigDigest != nil { + isPrevConfigStillSet := rmnHomeReader.IsRMNHomeConfigDigestSet(*prevConfigDigest) + if isPrevConfigStillSet { + t.Logf("Previous config is still set") + return false + } + } + + return true + } +}