diff --git a/.changeset/popular-rules-live.md b/.changeset/popular-rules-live.md new file mode 100644 index 00000000000..2d996a28dc2 --- /dev/null +++ b/.changeset/popular-rules-live.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +Fixes a race condition with the Finalizer when clearing txs #bugfix diff --git a/.github/e2e-tests.yml b/.github/e2e-tests.yml index 12cfcfb9256..66d19dfaf0d 100644 --- a/.github/e2e-tests.yml +++ b/.github/e2e-tests.yml @@ -987,6 +987,45 @@ runner-test-matrix: E2E_TEST_SELECTED_NETWORK: SIMULATED_1,SIMULATED_2,SIMULATED_3 E2E_JD_VERSION: 0.6.0 + - id: smoke/ccip/ccip_message_limitations_test.go:* + path: integration-tests/smoke/ccip/ccip_message_limitations_test.go + test_env_type: docker + runs_on: ubuntu-latest + triggers: + - PR E2E Core Tests + - Nightly E2E Tests + test_cmd: cd integration-tests/smoke/ccip && go test -run '^Test_CCIPMessageLimitations' -timeout 18m -test.parallel=1 -count=1 -json ./... + pyroscope_env: ci-smoke-ccipv1_6-evm-simulated + test_env_vars: + E2E_TEST_SELECTED_NETWORK: SIMULATED_1,SIMULATED_2 + E2E_JD_VERSION: 0.6.0 + + - id: smoke/ccip/ccip_token_price_updates_test.go:* + path: integration-tests/smoke/ccip/ccip_token_price_updates_test.go + test_env_type: docker + runs_on: ubuntu-latest + triggers: + - PR E2E Core Tests + - Nightly E2E Tests + test_cmd: cd integration-tests/smoke/ccip && go test ccip_token_price_updates_test.go -timeout 18m -test.parallel=1 -count=1 -json + pyroscope_env: ci-smoke-ccipv1_6-evm-simulated + test_env_vars: + E2E_TEST_SELECTED_NETWORK: SIMULATED_1,SIMULATED_2 + E2E_JD_VERSION: 0.6.0 + + - id: smoke/ccip/ccip_gas_price_updates_test.go:* + path: integration-tests/smoke/ccip/ccip_gas_price_updates_test.go + test_env_type: docker + runs_on: ubuntu-latest + triggers: + - PR E2E Core Tests + - Nightly E2E Tests + test_cmd: cd integration-tests/smoke/ccip && go test ccip_gas_price_updates_test.go -timeout 18m -test.parallel=1 -count=1 -json + pyroscope_env: ci-smoke-ccipv1_6-evm-simulated + test_env_vars: + E2E_TEST_SELECTED_NETWORK: SIMULATED_1,SIMULATED_2 + E2E_JD_VERSION: 0.6.0 + - id: smoke/ccip/ccip_rmn_test.go:^TestRMN_TwoMessagesOnTwoLanesIncludingBatching$ path: integration-tests/smoke/ccip/ccip_rmn_test.go test_env_type: docker diff --git a/core/capabilities/launcher.go b/core/capabilities/launcher.go index 97aea5d3c8c..27c43fe0a53 100644 --- a/core/capabilities/launcher.go +++ b/core/capabilities/launcher.go @@ -135,6 +135,7 @@ func (w *launcher) Name() string { } func (w *launcher) Launch(ctx context.Context, state *registrysyncer.LocalRegistry) error { + w.lggr.Debug("CapabilitiesLauncher triggered...") w.registry.SetLocalRegistry(state) allDONIDs := []registrysyncer.DonID{} @@ -222,6 +223,7 @@ func (w *launcher) Launch(ctx context.Context, state *registrysyncer.LocalRegist return errors.New("invariant violation: node is part of more than one workflowDON") } + w.lggr.Debug("Notifying DON set...") w.workflowDonNotifier.NotifyDonSet(myDON.DON) for _, rcd := range remoteCapabilityDONs { diff --git a/core/chains/evm/txmgr/evm_tx_store.go b/core/chains/evm/txmgr/evm_tx_store.go index d76580907b3..95756790cf3 100644 --- a/core/chains/evm/txmgr/evm_tx_store.go +++ b/core/chains/evm/txmgr/evm_tx_store.go @@ -1411,9 +1411,6 @@ func (o *evmTxStore) UpdateTxFatalErrorAndDeleteAttempts(ctx context.Context, et var cancel context.CancelFunc ctx, cancel = o.stopCh.Ctx(ctx) defer cancel() - if etx.State != txmgr.TxInProgress && etx.State != txmgr.TxUnstarted && etx.State != txmgr.TxConfirmed { - return pkgerrors.Errorf("can only transition to fatal_error from in_progress, unstarted, or confirmed, transaction is currently %s", etx.State) - } if !etx.Error.Valid { return errors.New("expected error field to be set") } diff --git a/core/chains/evm/txmgr/finalizer.go b/core/chains/evm/txmgr/finalizer.go index b5fe5ae37e2..bc496202cd6 100644 --- a/core/chains/evm/txmgr/finalizer.go +++ b/core/chains/evm/txmgr/finalizer.go @@ -580,7 +580,7 @@ func (f *evmFinalizer) ProcessOldTxsWithoutReceipts(ctx context.Context, oldTxID "an external wallet has been used to send a transaction from account %s with nonce %s."+ " Please note that Chainlink requires exclusive ownership of it's private keys and sharing keys across multiple"+ " chainlink instances, or using the chainlink keys with an external wallet is NOT SUPPORTED and WILL lead to missed transactions", - oldTx.ID, head.BlockNumber(), latestFinalizedHead.BlockNumber(), oldTx.FromAddress, oldTx.Sequence.String()), "txID", oldTx.ID, "sequence", oldTx.Sequence.String(), "fromAddress", oldTx.FromAddress) + oldTx.ID, head.BlockNumber(), latestFinalizedHead.BlockNumber(), oldTx.FromAddress, oldTx.Sequence), "txID", oldTx.ID, "sequence", oldTx.Sequence, "fromAddress", oldTx.FromAddress) // Signal pending tasks for these transactions as failed // Store errors and continue to allow all transactions a chance to be signaled diff --git a/core/cmd/shell_local.go b/core/cmd/shell_local.go index bead4ba5afd..2a87caba8cf 100644 --- a/core/cmd/shell_local.go +++ b/core/cmd/shell_local.go @@ -469,14 +469,12 @@ func (s *Shell) runNode(c *cli.Context) error { } } - if s.Config.Capabilities().WorkflowRegistry().Address() != "" { - err2 := app.GetKeyStore().Workflow().EnsureKey(rootCtx) - if err2 != nil { - return errors.Wrap(err2, "failed to ensure workflow key") - } + err2 := app.GetKeyStore().Workflow().EnsureKey(rootCtx) + if err2 != nil { + return errors.Wrap(err2, "failed to ensure workflow key") } - err2 := app.GetKeyStore().CSA().EnsureKey(rootCtx) + err2 = app.GetKeyStore().CSA().EnsureKey(rootCtx) if err2 != nil { return errors.Wrap(err2, "failed to ensure CSA key") } diff --git a/core/scripts/keystone/src/05_deploy_initialize_capabilities_registry.go b/core/scripts/keystone/src/05_deploy_initialize_capabilities_registry.go index f4e394b7da5..166022ac753 100644 --- a/core/scripts/keystone/src/05_deploy_initialize_capabilities_registry.go +++ b/core/scripts/keystone/src/05_deploy_initialize_capabilities_registry.go @@ -8,11 +8,9 @@ import ( "log" "os" "strings" - "time" "github.com/ethereum/go-ethereum/common" "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/durationpb" ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" @@ -34,126 +32,24 @@ type peer struct { var ( workflowDonPeers = []peer{ { - PeerID: "12D3KooWBCF1XT5Wi8FzfgNCqRL76Swv8TRU3TiD4QiJm8NMNX7N", - Signer: "0x9639dCc7D0ca4468B5f684ef89F12F0B365c9F6d", - EncryptionPublicKey: "0xe7f44e3eedf3527199efec7334183b5384ba0e7c7c57b390b63a3de5a10cd53c", + PeerID: "12D3KooWQXfwA26jysiKKPXKuHcJtWTbGSwzoJxj4rYtEJyQTnFj", + Signer: "0xC44686106b85687F741e1d6182a5e2eD2211a115", + EncryptionPublicKey: "0x0f8b6629bc26321b39dfb7e2bc096584fe43dccfda54b67c24f53fd827efbc72", }, { - PeerID: "12D3KooWG1AyvwmCpZ93J8pBQUE1SuzrjDXnT4BeouncHR3jWLCG", - Signer: "0x8f0fAE64f5f75067833ed5deDC2804B62b21383d", - EncryptionPublicKey: "0x315c6097f89baef3c3ae1503b801aaabf411134ffec66bbe8d1d184540588728", + PeerID: "12D3KooWGCRg5wNKoRFUAypA68ZkwXz8dT5gyF3VdQpEH3FtLqHZ", + Signer: "0x0ee7C8Aa7F8cb5E08415C57B79d7d387F2665E8b", + EncryptionPublicKey: "0x4cb8a297d524469e63e8d8a15c7682891126987acaa39bc4f1db78c066f7af63", }, { - PeerID: "12D3KooWGeUKZBRMbx27FUTgBwZa9Ap9Ym92mywwpuqkEtz8XWyv", - Signer: "0xf09A863D920840c13277e76F43CFBdfB22b8FB7C", - EncryptionPublicKey: "0xa7a5e118213552a939f310e19167f49e9ad952cfe9d51eaae1ad37d92d9f0583", + PeerID: "12D3KooWHggbPfMcSSAwpBZHvwpo2UHzkf1ij3qjTnRiWQ7S5p4g", + Signer: "0xEd850731Be048afE986DaA90Bb482BC3b0f78aec", + EncryptionPublicKey: "0x7a9be509ace5f004fa397b7013893fed13a135dd273f7293dc3c7b6e57f1764e", }, { - PeerID: "12D3KooW9zYWQv3STmDeNDidyzxsJSTxoCTLicafgfeEz9nhwhC4", - Signer: "0x7eD90b519bC3054a575C464dBf39946b53Ff90EF", - EncryptionPublicKey: "0x75f75a86910eed0259e3107b3c368f72c0ad0301bac696fd340916e2437194c3", - }, - { - PeerID: "12D3KooWG1AeBnSJH2mdcDusXQVye2jqodZ6pftTH98HH6xvrE97", - Signer: "0x8F572978673d711b2F061EB7d514BD46EAD6668A", - EncryptionPublicKey: "0xd032f1e884a22fd05151f59565f05a4ccbf984afccbbee13469fc25947e69360", - }, - { - PeerID: "12D3KooWBf3PrkhNoPEmp7iV291YnPuuTsgEDHTscLajxoDvwHGA", - Signer: "0x21eF07Dfaf8f7C10CB0d53D18b641ee690541f9D", - EncryptionPublicKey: "0xed64ed4a2c2954f7390bfdf41a714934c0e55693ad1c0b91505d51f4eb9e4c06", - }, - { - PeerID: "12D3KooWP3FrMTFXXRU2tBC8aYvEBgUX6qhcH9q2JZCUi9Wvc2GX", - Signer: "0x7Fa21F6f716CFaF8f249564D72Ce727253186C89", - EncryptionPublicKey: "0xed64ed4a2c2954f7390bfdf41a714934c0e55693ad1c0b91505d51f4eb9e4c06", - }, - } - triggerDonPeers = []peer{ - { - PeerID: "12D3KooWBaiTbbRwwt2fbNifiL7Ew9tn3vds9AJE3Nf3eaVBX36m", - Signer: "0x9CcE7293a4Cc2621b61193135A95928735e4795F", - EncryptionPublicKey: "0xed64ed4a2c2954f7390bfdf41a714934c0e55693ad1c0b91505d51f4eb9e4c06", - }, - { - PeerID: "12D3KooWS7JSY9fzSfWgbCE1S3W2LNY6ZVpRuun74moVBkKj6utE", - Signer: "0x3c775F20bCB2108C1A818741Ce332Bb5fe0dB925", - EncryptionPublicKey: "0xed64ed4a2c2954f7390bfdf41a714934c0e55693ad1c0b91505d51f4eb9e4c06", - }, - { - PeerID: "12D3KooWMMTDXcWhpVnwrdAer1jnVARTmnr3RyT3v7Djg8ZuoBh9", - Signer: "0x50314239e2CF05555ceeD53E7F47eB2A8Eab0dbB", - EncryptionPublicKey: "0xce0e88d12d568653757f1db154f9c503db3d3d7b37cb03d84b61f39f09824cc0", - }, - { - PeerID: "12D3KooWGzVXsKxXsF4zLgxSDM8Gzx1ywq2pZef4PrHMKuVg4K3P", - Signer: "0xd76A4f98898c3b9A72b244476d7337b50D54BCd8", - EncryptionPublicKey: "0xce0e88d12d568653757f1db154f9c503db3d3d7b37cb03d84b61f39f09824cc0", - }, - { - PeerID: "12D3KooWSyjmmzjVtCzwN7bXzZQFmWiJRuVcKBerNjVgL7HdLJBW", - Signer: "0x656A873f6895b8a03Fb112dE927d43FA54B2c92A", - EncryptionPublicKey: "0x91f11910104ff55209d6d344a15eef6a222a54d4973aaebd301807444b555e3f", - }, - { - PeerID: "12D3KooWLGz9gzhrNsvyM6XnXS3JRkZoQdEzuAvysovnSChNK5ZK", - Signer: "0x5d1e87d87bF2e0cD4Ea64F381a2dbF45e5f0a553", - EncryptionPublicKey: "0x20ff771215e567cf7e9a1fea8f2d4df90adc8303794175f79893037ff8808b51", - }, - { - PeerID: "12D3KooWAvZnvknFAfSiUYjATyhzEJLTeKvAzpcLELHi4ogM3GET", - Signer: "0x91d9b0062265514f012Eb8fABA59372fD9520f56", - EncryptionPublicKey: "0x54176f154052068943569b676fa7eec7dc836e17bbe743ce56b1c7e205191d9c", - }, - } - targetDonPeers = []peer{ - { - PeerID: "12D3KooWJrthXtnPHw7xyHFAxo6NxifYTvc8igKYaA6wRRRqtsMb", - Signer: "0x3F82750353Ea7a051ec9bA011BC628284f9a5327", - EncryptionPublicKey: "0x1a746e0fcaf3e50db87bcc765fbbaee7d24a28166ea1461338a03fcbffb088cf", - }, - { - PeerID: "12D3KooWFQekP9sGex4XhqEJav5EScjTpDVtDqJFg1JvrePBCEGJ", - Signer: "0xc23545876A208AA0443B1b8d552c7be4FF4b53F0", - EncryptionPublicKey: "0x1a746e0fcaf3e50db87bcc765fbbaee7d24a28166ea1461338a03fcbffb088cf", - }, - { - PeerID: "12D3KooWFLEq4hYtdyKWwe47dXGEbSiHMZhmr5xLSJNhpfiEz8NF", - Signer: "0x82601Fa43d8B1dC1d4eB640451aC86a7CDA37011", - EncryptionPublicKey: "0x1a746e0fcaf3e50db87bcc765fbbaee7d24a28166ea1461338a03fcbffb088cf", - }, - { - PeerID: "12D3KooWN2hztiXNNS1jMQTTvvPRYcarK1C7T3Mdqk4x4gwyo5WS", - Signer: "0x1a684B3d8f917fe496b7B1A8b29EDDAED64F649f", - EncryptionPublicKey: "0x1a746e0fcaf3e50db87bcc765fbbaee7d24a28166ea1461338a03fcbffb088cf", - }, - } - - aptosTargetDonPeers = []peer{ - { - PeerID: "12D3KooWNBr1AD3vD3dzSLgg1tK56qyJoenDx7EYNnZpbr1g4jD6", - Signer: "a41f9a561ff2266d94240996a76f9c2b3b7d8184", - EncryptionPublicKey: "0xf28fcfaf2933289b3a98d387f6edf85853df32528c094dee9e737f4ca63e5a30", - }, - { - PeerID: "12D3KooWRRgWiZGw5GYsPa62CkwFNKJb5u4hWo4DinnvjG6GE6Nj", - Signer: "e4f3c7204776530fb7833db6f9dbfdb8bd0ec96892965324a71c20d6776f67f0", - EncryptionPublicKey: "0x49c837675372d8f430e69ccd91c43029600c2c6469a2f933c4a1c4bbbc974c6d", - }, - { - PeerID: "12D3KooWKwzgUHw5YbqUsYUVt3yiLSJcqc8ANofUtqHX6qTm7ox2", - Signer: "4071ea00e2e2c76b3406018ba9f66bf6b9aee3a6762e62ac823b1ee91ba7d7b0", - EncryptionPublicKey: "0x8fe005ef16d57091160c0b4373232e7389c321dff971fc0251a39e360d9ac34a", - }, - { - PeerID: "12D3KooWBRux5o2bw1j3SQwEHzCspjkt7Xe3Y3agRUuab2SUnExj", - Signer: "6f5180c7d276876dbe413bf9b0efff7301d1367f39f4bac64180090cab70989b", - EncryptionPublicKey: "0x90dd41db21351c06396761dd683a82c791cd71e536fce246e582a4ef058091ae", - }, - { - PeerID: "12D3KooWFqvDaMSDGa6eMSTF9en6G2c3ZbGLmaA5Xs3AgxVBPb8B", - Signer: "dbce9a6df8a04d54e52a109d01ee9b5d32873b1d2436cf7b7fae61fd6eca46f8", - EncryptionPublicKey: "0x87cf298dd236a307ea887cd5d81eb0b708e3dd48c984c0700bb26c072e427942", + PeerID: "12D3KooWC44picK3WvVkP1oufHYu1mB18xUg6mF2sNKM8Hc3EmdW", + Signer: "0xD3400B75f2016878dC2013ff2bC2Cf03bD5F19f5", + EncryptionPublicKey: "0x66dcec518809c1dd4ec5c094f651061b974d3cdbf5388cf4f47740c76fb58616", }, } @@ -304,7 +200,7 @@ func (c *deployAndInitializeCapabilitiesRegistryCommand) Run(args []string) { Version: "1.0.0", CapabilityType: uint8(0), // trigger } - sid, err := reg.GetHashedCapabilityId(&bind.CallOpts{}, streamsTrigger.LabelledName, streamsTrigger.Version) + _, err = reg.GetHashedCapabilityId(&bind.CallOpts{}, streamsTrigger.LabelledName, streamsTrigger.Version) if err != nil { panic(err) } @@ -334,7 +230,7 @@ func (c *deployAndInitializeCapabilitiesRegistryCommand) Run(args []string) { Version: "1.0.0", CapabilityType: uint8(3), // target } - wid, err := reg.GetHashedCapabilityId(&bind.CallOpts{}, writeChain.LabelledName, writeChain.Version) + _, err = reg.GetHashedCapabilityId(&bind.CallOpts{}, writeChain.LabelledName, writeChain.Version) if err != nil { log.Printf("failed to call GetHashedCapabilityId: %s", err) } @@ -344,7 +240,7 @@ func (c *deployAndInitializeCapabilitiesRegistryCommand) Run(args []string) { Version: "1.0.0", CapabilityType: uint8(3), // target } - awid, err := reg.GetHashedCapabilityId(&bind.CallOpts{}, aptosWriteChain.LabelledName, aptosWriteChain.Version) + _, err = reg.GetHashedCapabilityId(&bind.CallOpts{}, aptosWriteChain.LabelledName, aptosWriteChain.Version) if err != nil { log.Printf("failed to call GetHashedCapabilityId: %s", err) } @@ -402,36 +298,6 @@ func (c *deployAndInitializeCapabilitiesRegistryCommand) Run(args []string) { nodes = append(nodes, n) } - for _, triggerPeer := range triggerDonPeers { - n, innerErr := peerToNode(nopID, triggerPeer) - if innerErr != nil { - panic(innerErr) - } - - n.HashedCapabilityIds = [][32]byte{sid} - nodes = append(nodes, n) - } - - for _, targetPeer := range targetDonPeers { - n, innerErr := peerToNode(nopID, targetPeer) - if innerErr != nil { - panic(innerErr) - } - - n.HashedCapabilityIds = [][32]byte{wid} - nodes = append(nodes, n) - } - - for _, targetPeer := range aptosTargetDonPeers { - n, innerErr := peerToNode(nopID, targetPeer) - if innerErr != nil { - panic(innerErr) - } - - n.HashedCapabilityIds = [][32]byte{awid} - nodes = append(nodes, n) - } - tx, err = reg.AddNodes(env.Owner, nodes) if err != nil { log.Printf("failed to AddNodes: %s", err) @@ -476,95 +342,6 @@ func (c *deployAndInitializeCapabilitiesRegistryCommand) Run(args []string) { log.Printf("workflowDON: failed to AddDON: %s", err) } - // trigger DON - ps, err = peers(triggerDonPeers) - if err != nil { - panic(err) - } - - config := &capabilitiespb.CapabilityConfig{ - DefaultConfig: values.Proto(values.EmptyMap()).GetMapValue(), - RemoteConfig: &capabilitiespb.CapabilityConfig_RemoteTriggerConfig{ - RemoteTriggerConfig: &capabilitiespb.RemoteTriggerConfig{ - RegistrationRefresh: durationpb.New(20 * time.Second), - RegistrationExpiry: durationpb.New(60 * time.Second), - // F + 1 - MinResponsesToAggregate: uint32(1) + 1, - }, - }, - } - configb, err := proto.Marshal(config) - if err != nil { - panic(err) - } - cfgs = []kcr.CapabilitiesRegistryCapabilityConfiguration{ - { - CapabilityId: sid, - Config: configb, - }, - } - _, err = reg.AddDON(env.Owner, ps, cfgs, true, false, 1) - if err != nil { - log.Printf("triggerDON: failed to AddDON: %s", err) - } - - // target DON - ps, err = peers(targetDonPeers) - if err != nil { - panic(err) - } - - targetCapabilityConfig := newCapabilityConfig() - targetCapabilityConfig.RemoteConfig = &capabilitiespb.CapabilityConfig_RemoteTargetConfig{ - RemoteTargetConfig: &capabilitiespb.RemoteTargetConfig{ - RequestHashExcludedAttributes: []string{"signed_report.Signatures"}, - }, - } - - remoteTargetConfigBytes, err := proto.Marshal(targetCapabilityConfig) - if err != nil { - panic(err) - } - - cfgs = []kcr.CapabilitiesRegistryCapabilityConfiguration{ - { - CapabilityId: wid, - Config: remoteTargetConfigBytes, - }, - } - _, err = reg.AddDON(env.Owner, ps, cfgs, true, false, 1) - if err != nil { - log.Printf("targetDON: failed to AddDON: %s", err) - } - - // Aptos target DON - ps, err = peers(aptosTargetDonPeers) - if err != nil { - panic(err) - } - - targetCapabilityConfig = newCapabilityConfig() - targetCapabilityConfig.RemoteConfig = &capabilitiespb.CapabilityConfig_RemoteTargetConfig{ - RemoteTargetConfig: &capabilitiespb.RemoteTargetConfig{ - RequestHashExcludedAttributes: []string{"signed_report.Signatures"}, - }, - } - - remoteTargetConfigBytes, err = proto.Marshal(targetCapabilityConfig) - if err != nil { - panic(err) - } - - cfgs = []kcr.CapabilitiesRegistryCapabilityConfiguration{ - { - CapabilityId: awid, - Config: remoteTargetConfigBytes, - }, - } - _, err = reg.AddDON(env.Owner, ps, cfgs, true, false, 1) - if err != nil { - log.Printf("targetDON: failed to AddDON: %s", err) - } } func deployCapabilitiesRegistry(env helpers.Environment) *kcr.CapabilitiesRegistry { diff --git a/core/services/chainlink/application.go b/core/services/chainlink/application.go index 68b9b99a823..ac0d28760eb 100644 --- a/core/services/chainlink/application.go +++ b/core/services/chainlink/application.go @@ -330,6 +330,7 @@ func NewApplication(opts ApplicationOpts) (Application, error) { return relayer.NewContractReader(ctx, bytes) }, eventHandler) + globalLogger.Debugw("Creating WorkflowRegistrySyncer") wfSyncer := syncer.NewWorkflowRegistry(globalLogger, func(ctx context.Context, bytes []byte) (syncer.ContractReader, error) { return relayer.NewContractReader(ctx, bytes) }, cfg.Capabilities().WorkflowRegistry().Address(), diff --git a/core/services/keystore/keystoretest.go b/core/services/keystore/keystoretest.go index e179b51bb54..626cc4bab99 100644 --- a/core/services/keystore/keystoretest.go +++ b/core/services/keystore/keystoretest.go @@ -75,5 +75,6 @@ func NewInMemory(ds sqlutil.DataSource, scryptParams utils.ScryptParams, lggr lo starknet: newStarkNetKeyStore(km), aptos: newAptosKeyStore(km), vrf: newVRFKeyStore(km), + workflow: newWorkflowKeyStore(km), } } diff --git a/core/services/keystore/models.go b/core/services/keystore/models.go index e0b53ef95e4..151934827c3 100644 --- a/core/services/keystore/models.go +++ b/core/services/keystore/models.go @@ -239,6 +239,9 @@ func (kr *keyRing) raw() (rawKeys rawKeyRing) { for _, vrfKey := range kr.VRF { rawKeys.VRF = append(rawKeys.VRF, vrfKey.Raw()) } + for _, workflowKey := range kr.Workflow { + rawKeys.Workflow = append(rawKeys.Workflow, workflowKey.Raw()) + } return rawKeys } @@ -284,6 +287,12 @@ func (kr *keyRing) logPubKeys(lggr logger.Logger) { for _, VRFKey := range kr.VRF { vrfIDs = append(vrfIDs, VRFKey.ID()) } + workflowIDs := make([]string, len(kr.Workflow)) + i := 0 + for _, workflowKey := range kr.Workflow { + workflowIDs[i] = workflowKey.ID() + i++ + } if len(csaIDs) > 0 { lggr.Infow(fmt.Sprintf("Unlocked %d CSA keys", len(csaIDs)), "keys", csaIDs) } @@ -314,6 +323,9 @@ func (kr *keyRing) logPubKeys(lggr logger.Logger) { if len(vrfIDs) > 0 { lggr.Infow(fmt.Sprintf("Unlocked %d VRF keys", len(vrfIDs)), "keys", vrfIDs) } + if len(workflowIDs) > 0 { + lggr.Infow(fmt.Sprintf("Unlocked %d Workflow keys", len(workflowIDs)), "keys", workflowIDs) + } if len(kr.LegacyKeys.legacyRawKeys) > 0 { lggr.Infow(fmt.Sprintf("%d keys stored in legacy system", kr.LegacyKeys.legacyRawKeys.len())) } @@ -333,6 +345,7 @@ type rawKeyRing struct { StarkNet []starkkey.Raw Aptos []aptoskey.Raw VRF []vrfkey.Raw + Workflow []workflowkey.Raw LegacyKeys LegacyKeyStorage `json:"-"` } @@ -379,6 +392,10 @@ func (rawKeys rawKeyRing) keys() (*keyRing, error) { vrfKey := rawVRFKey.Key() keyRing.VRF[vrfKey.ID()] = vrfKey } + for _, rawWorkflowKey := range rawKeys.Workflow { + workflowKey := rawWorkflowKey.Key() + keyRing.Workflow[workflowKey.ID()] = workflowKey + } keyRing.LegacyKeys = rawKeys.LegacyKeys return keyRing, nil diff --git a/core/services/keystore/workflow_test.go b/core/services/keystore/workflow_test.go index 051ebdb76a7..d7e540b9c75 100644 --- a/core/services/keystore/workflow_test.go +++ b/core/services/keystore/workflow_test.go @@ -175,4 +175,39 @@ func Test_EncryptionKeyStore_E2E(t *testing.T) { require.NoError(t, err) require.Len(t, keys, 1) }) + + t.Run("persists keys across restarts", func(t *testing.T) { + defer reset() + ctx := testutils.Context(t) + + keys, err := ks.GetAll() + require.NoError(t, err) + assert.Empty(t, keys) + + err = keyStore.Workflow().EnsureKey(ctx) + require.NoError(t, err) + + keys, err = ks.GetAll() + require.NoError(t, err) + + require.NoError(t, err) + require.Len(t, keys, 1) + + // Now instantiate the keystore again, but with the same DB + // This should fetch the key directly from the DB. + keyStore := keystore.ExposedNewMaster(t, db) + require.NoError(t, keyStore.Unlock(testutils.Context(t), cltest.Password)) + + gotKeys, err := keyStore.Workflow().GetAll() + require.NoError(t, err) + require.Len(t, gotKeys, 1) + + assert.Equal(t, keys[0].PublicKeyString(), gotKeys[0].PublicKeyString()) + + err = keyStore.Workflow().EnsureKey(testutils.Context(t)) + require.NoError(t, err) + gotKeys, err = keyStore.Workflow().GetAll() + require.NoError(t, err) + require.Len(t, gotKeys, 1) + }) } diff --git a/core/services/workflows/syncer/workflow_registry.go b/core/services/workflows/syncer/workflow_registry.go index 6642679b228..b33645cdb9e 100644 --- a/core/services/workflows/syncer/workflow_registry.go +++ b/core/services/workflows/syncer/workflow_registry.go @@ -204,12 +204,14 @@ func (w *workflowRegistry) Start(_ context.Context) error { defer w.wg.Done() defer cancel() + w.lggr.Debugw("Waiting for DON...") don, err := w.workflowDonNotifier.WaitForDon(ctx) if err != nil { w.lggr.Errorf("failed to wait for don: %v", err) return } + w.lggr.Debugw("Loading initial workflows for DON", "DON", don.ID) loadWorkflowsHead, err := w.initialWorkflowsStateLoader.LoadWorkflows(ctx, don) if err != nil { w.lggr.Errorf("failed to load workflows: %v", err) @@ -333,6 +335,7 @@ func (w *workflowRegistry) syncEventsLoop(ctx context.Context, lastReadBlockNumb case <-ctx.Done(): return case <-ticker: + w.lggr.Debugw("Syncing with WorkflowRegistry") // for each event type, send a signal for it to execute a query and produce a new // batch of event logs for i := 0; i < len(w.eventTypes); i++ { diff --git a/deployment/ccip/changeset/accept_ownership_test.go b/deployment/ccip/changeset/accept_ownership_test.go index 97a5e1f9aa9..1c83d368a45 100644 --- a/deployment/ccip/changeset/accept_ownership_test.go +++ b/deployment/ccip/changeset/accept_ownership_test.go @@ -8,12 +8,13 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" - "github.com/stretchr/testify/require" - "golang.org/x/exp/maps" - "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/stretchr/testify/require" + "golang.org/x/exp/maps" + "github.com/smartcontractkit/chainlink/v2/core/logger" ) @@ -85,8 +86,8 @@ func genTestTransferOwnershipConfig( ) return commonchangeset.TransferOwnershipConfig{ - TimelocksPerChain: timelocksPerChain, - Contracts: contracts, + OwnersPerChain: timelocksPerChain, + Contracts: contracts, } } @@ -123,10 +124,10 @@ func genTestAcceptOwnershipConfig( ) return commonchangeset.AcceptOwnershipConfig{ - TimelocksPerChain: timelocksPerChain, - ProposerMCMSes: proposerMCMses, - Contracts: contracts, - MinDelay: time.Duration(0), + OwnersPerChain: timelocksPerChain, + ProposerMCMSes: proposerMCMses, + Contracts: contracts, + MinDelay: time.Duration(0), } } diff --git a/deployment/ccip/changeset/test_helpers.go b/deployment/ccip/changeset/test_helpers.go index 062af4e0117..22ec324609c 100644 --- a/deployment/ccip/changeset/test_helpers.go +++ b/deployment/ccip/changeset/test_helpers.go @@ -490,6 +490,20 @@ func TestSendRequest( testRouter bool, evm2AnyMessage router.ClientEVM2AnyMessage, ) (msgSentEvent *onramp.OnRampCCIPMessageSent) { + msgSentEvent, err := DoSendRequest(t, e, state, src, dest, testRouter, evm2AnyMessage) + require.NoError(t, err) + return msgSentEvent +} + +// DoSendRequest similar to TestSendRequest but returns an error. +func DoSendRequest( + t *testing.T, + e deployment.Environment, + state CCIPOnChainState, + src, dest uint64, + testRouter bool, + evm2AnyMessage router.ClientEVM2AnyMessage, +) (*onramp.OnRampCCIPMessageSent, error) { t.Logf("Sending CCIP request from chain selector %d to chain selector %d", src, dest) tx, blockNum, err := CCIPSendRequest( @@ -499,13 +513,19 @@ func TestSendRequest( testRouter, evm2AnyMessage, ) - require.NoError(t, err) + if err != nil { + return nil, err + } + it, err := state.Chains[src].OnRamp.FilterCCIPMessageSent(&bind.FilterOpts{ Start: blockNum, End: &blockNum, Context: context.Background(), }, []uint64{dest}, []uint64{}) - require.NoError(t, err) + if err != nil { + return nil, err + } + require.True(t, it.Next()) t.Logf("CCIP message (id %x) sent from chain selector %d to chain selector %d tx %s seqNum %d nonce %d sender %s", it.Event.Message.Header.MessageId[:], @@ -516,7 +536,7 @@ func TestSendRequest( it.Event.Message.Header.Nonce, it.Event.Message.Sender.String(), ) - return it.Event + return it.Event, nil } // MakeEVMExtraArgsV2 creates the extra args for the EVM2Any message that is destined @@ -1108,6 +1128,7 @@ func TransferAndWaitForSuccess( receiver common.Address, data []byte, expectedStatus int, + extraArgs []byte, ) { identifier := SourceDestPair{ SourceChainSelector: sourceChain, @@ -1128,7 +1149,7 @@ func TransferAndWaitForSuccess( Data: data, TokenAmounts: tokens, FeeToken: common.HexToAddress("0x0"), - ExtraArgs: nil, + ExtraArgs: extraArgs, }) expectedSeqNum[identifier] = msgSentEvent.SequenceNumber expectedSeqNumExec[identifier] = []uint64{msgSentEvent.SequenceNumber} diff --git a/deployment/common/changeset/accept_ownership.go b/deployment/common/changeset/accept_ownership.go index 7f89e5cb75f..79aa876eabb 100644 --- a/deployment/common/changeset/accept_ownership.go +++ b/deployment/common/changeset/accept_ownership.go @@ -12,6 +12,7 @@ import ( "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/mcms" "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/timelock" + "github.com/smartcontractkit/chainlink/deployment" "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" ) @@ -22,8 +23,8 @@ type OwnershipAcceptor interface { } type AcceptOwnershipConfig struct { - // TimelocksPerChain is a mapping from chain selector to the timelock contract address on that chain. - TimelocksPerChain map[uint64]common.Address + // OwnersPerChain is a mapping from chain selector to the owner contract address on that chain. + OwnersPerChain map[uint64]common.Address // ProposerMCMSes is a mapping from chain selector to the proposer MCMS contract on that chain. ProposerMCMSes map[uint64]*gethwrappers.ManyChainMultiSig @@ -39,11 +40,11 @@ type AcceptOwnershipConfig struct { } func (a AcceptOwnershipConfig) Validate() error { - // check that we have timelocks and proposer mcmses for the chains + // check that we have owners and proposer mcmses for the chains // in the Contracts field. for chainSelector := range a.Contracts { - if _, ok := a.TimelocksPerChain[chainSelector]; !ok { - return fmt.Errorf("missing timelock for chain %d", chainSelector) + if _, ok := a.OwnersPerChain[chainSelector]; !ok { + return fmt.Errorf("missing owner for chain %d", chainSelector) } if _, ok := a.ProposerMCMSes[chainSelector]; !ok { return fmt.Errorf("missing proposer MCMS for chain %d", chainSelector) @@ -88,7 +89,7 @@ func NewAcceptOwnershipChangeset( } proposal, err := proposalutils.BuildProposalFromBatches( - cfg.TimelocksPerChain, + cfg.OwnersPerChain, cfg.ProposerMCMSes, batches, "Accept ownership of contracts", diff --git a/deployment/common/changeset/accept_ownership_test.go b/deployment/common/changeset/accept_ownership_test.go index 27feb6389bc..fc2c296f2d8 100644 --- a/deployment/common/changeset/accept_ownership_test.go +++ b/deployment/common/changeset/accept_ownership_test.go @@ -6,8 +6,9 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" - "github.com/smartcontractkit/chainlink/deployment/common/changeset" "github.com/stretchr/testify/assert" + + "github.com/smartcontractkit/chainlink/deployment/common/changeset" ) func TestAcceptOwnershipConfig_Validate(t *testing.T) { @@ -19,7 +20,7 @@ func TestAcceptOwnershipConfig_Validate(t *testing.T) { { name: "valid config", config: changeset.AcceptOwnershipConfig{ - TimelocksPerChain: map[uint64]common.Address{ + OwnersPerChain: map[uint64]common.Address{ 1: common.HexToAddress("0x1"), }, ProposerMCMSes: map[uint64]*gethwrappers.ManyChainMultiSig{ @@ -35,7 +36,7 @@ func TestAcceptOwnershipConfig_Validate(t *testing.T) { { name: "missing timelock", config: changeset.AcceptOwnershipConfig{ - TimelocksPerChain: map[uint64]common.Address{}, + OwnersPerChain: map[uint64]common.Address{}, ProposerMCMSes: map[uint64]*gethwrappers.ManyChainMultiSig{ 1: {}, }, @@ -49,7 +50,7 @@ func TestAcceptOwnershipConfig_Validate(t *testing.T) { { name: "missing proposer MCMS", config: changeset.AcceptOwnershipConfig{ - TimelocksPerChain: map[uint64]common.Address{ + OwnersPerChain: map[uint64]common.Address{ 1: common.HexToAddress("0x1"), }, ProposerMCMSes: map[uint64]*gethwrappers.ManyChainMultiSig{}, diff --git a/deployment/common/changeset/transfer_ownership.go b/deployment/common/changeset/transfer_ownership.go index a0085fb61ca..36fe2cbed78 100644 --- a/deployment/common/changeset/transfer_ownership.go +++ b/deployment/common/changeset/transfer_ownership.go @@ -6,6 +6,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" gethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/smartcontractkit/chainlink/deployment" ) @@ -15,18 +16,18 @@ type OwnershipTransferrer interface { } type TransferOwnershipConfig struct { - // TimelocksPerChain is a mapping from chain selector to the timelock contract address on that chain. - TimelocksPerChain map[uint64]common.Address + // OwnersPerChain is a mapping from chain selector to the owner's contract address on that chain. + OwnersPerChain map[uint64]common.Address // Contracts is a mapping from chain selector to the ownership transferrers on that chain. Contracts map[uint64][]OwnershipTransferrer } func (t TransferOwnershipConfig) Validate() error { - // check that we have timelocks for the chains in the Contracts field. + // check that we have owners for the chains in the Contracts field. for chainSelector := range t.Contracts { - if _, ok := t.TimelocksPerChain[chainSelector]; !ok { - return fmt.Errorf("missing timelock for chain %d", chainSelector) + if _, ok := t.OwnersPerChain[chainSelector]; !ok { + return fmt.Errorf("missing owners for chain %d", chainSelector) } } @@ -36,8 +37,8 @@ func (t TransferOwnershipConfig) Validate() error { var _ deployment.ChangeSet[TransferOwnershipConfig] = NewTransferOwnershipChangeset // NewTransferOwnershipChangeset creates a changeset that transfers ownership of all the -// contracts in the provided configuration to the the appropriate timelock on that chain. -// If the owner is already the timelock contract, no transaction is sent. +// contracts in the provided configuration to correct owner on that chain. +// If the owner is already the provided address, no transaction is sent. func NewTransferOwnershipChangeset( e deployment.Environment, cfg TransferOwnershipConfig, @@ -47,14 +48,14 @@ func NewTransferOwnershipChangeset( } for chainSelector, contracts := range cfg.Contracts { - timelock := cfg.TimelocksPerChain[chainSelector] + ownerAddress := cfg.OwnersPerChain[chainSelector] for _, contract := range contracts { owner, err := contract.Owner(nil) if err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to get owner of contract %T: %v", contract, err) } - if owner != timelock { - tx, err := contract.TransferOwnership(e.Chains[chainSelector].DeployerKey, timelock) + if owner != ownerAddress { + tx, err := contract.TransferOwnership(e.Chains[chainSelector].DeployerKey, ownerAddress) _, err = deployment.ConfirmIfNoError(e.Chains[chainSelector], tx, err) if err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to transfer ownership of contract %T: %v", contract, err) diff --git a/deployment/keystone/capability_registry_deployer.go b/deployment/keystone/capability_registry_deployer.go index a4648c27905..11c85453927 100644 --- a/deployment/keystone/capability_registry_deployer.go +++ b/deployment/keystone/capability_registry_deployer.go @@ -19,8 +19,12 @@ type CapabilitiesRegistryDeployer struct { contract *capabilities_registry.CapabilitiesRegistry } -func NewCapabilitiesRegistryDeployer(lggr logger.Logger) *CapabilitiesRegistryDeployer { - return &CapabilitiesRegistryDeployer{lggr: lggr} +func NewCapabilitiesRegistryDeployer() (*CapabilitiesRegistryDeployer, error) { + lggr, err := logger.New() + if err != nil { + return nil, err + } + return &CapabilitiesRegistryDeployer{lggr: lggr}, nil } func (c *CapabilitiesRegistryDeployer) Contract() *capabilities_registry.CapabilitiesRegistry { diff --git a/deployment/keystone/changeset/accept_ownership.go b/deployment/keystone/changeset/accept_ownership.go new file mode 100644 index 00000000000..8a4f3c60c53 --- /dev/null +++ b/deployment/keystone/changeset/accept_ownership.go @@ -0,0 +1,89 @@ +package changeset + +import ( + "time" + + "github.com/ethereum/go-ethereum/common" + + ccipowner "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/common/changeset" +) + +func toOwnershipAcceptors[T changeset.OwnershipAcceptor](items []T) []changeset.OwnershipAcceptor { + ownershipAcceptors := make([]changeset.OwnershipAcceptor, len(items)) + for i, item := range items { + ownershipAcceptors[i] = item + } + return ownershipAcceptors +} + +type AcceptAllOwnershipRequest struct { + ChainSelector uint64 + MinDelay time.Duration +} + +var _ deployment.ChangeSet[*AcceptAllOwnershipRequest] = AcceptAllOwnershipsProposal + +// AcceptAllOwnershipsProposal creates a MCMS proposal to call accept ownership on all the Keystone contracts in the address book. +func AcceptAllOwnershipsProposal(e deployment.Environment, req *AcceptAllOwnershipRequest) (deployment.ChangesetOutput, error) { + chainSelector := req.ChainSelector + minDelay := req.MinDelay + chain := e.Chains[chainSelector] + addrBook := e.ExistingAddresses + + // Fetch contracts from the address book. + timelocks, err := timelocksFromAddrBook(addrBook, chain) + if err != nil { + return deployment.ChangesetOutput{}, err + } + capRegs, err := capRegistriesFromAddrBook(addrBook, chain) + if err != nil { + return deployment.ChangesetOutput{}, err + } + ocr3, err := ocr3FromAddrBook(addrBook, chain) + if err != nil { + return deployment.ChangesetOutput{}, err + } + forwarders, err := forwardersFromAddrBook(addrBook, chain) + if err != nil { + return deployment.ChangesetOutput{}, err + } + consumers, err := feedsConsumersFromAddrBook(addrBook, chain) + if err != nil { + return deployment.ChangesetOutput{}, err + } + mcmsProposers, err := proposersFromAddrBook(addrBook, chain) + if err != nil { + return deployment.ChangesetOutput{}, err + } + + // Initialize the OwnershipAcceptors slice + var ownershipAcceptors []changeset.OwnershipAcceptor + + // Append all contracts + ownershipAcceptors = append(ownershipAcceptors, toOwnershipAcceptors(capRegs)...) + ownershipAcceptors = append(ownershipAcceptors, toOwnershipAcceptors(ocr3)...) + ownershipAcceptors = append(ownershipAcceptors, toOwnershipAcceptors(forwarders)...) + ownershipAcceptors = append(ownershipAcceptors, toOwnershipAcceptors(consumers)...) + + // Construct the configuration + cfg := changeset.AcceptOwnershipConfig{ + OwnersPerChain: map[uint64]common.Address{ + // Assuming there is only one timelock per chain. + chainSelector: timelocks[0].Address(), + }, + ProposerMCMSes: map[uint64]*ccipowner.ManyChainMultiSig{ + // Assuming there is only one MCMS proposer per chain. + chainSelector: mcmsProposers[0], + }, + Contracts: map[uint64][]changeset.OwnershipAcceptor{ + chainSelector: ownershipAcceptors, + }, + MinDelay: minDelay, + } + + // Create and return the changeset + return changeset.NewAcceptOwnershipChangeset(e, cfg) +} diff --git a/deployment/keystone/changeset/accept_ownership_test.go b/deployment/keystone/changeset/accept_ownership_test.go new file mode 100644 index 00000000000..996ff08c149 --- /dev/null +++ b/deployment/keystone/changeset/accept_ownership_test.go @@ -0,0 +1,86 @@ +package changeset_test + +import ( + "math/big" + "testing" + "time" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + + commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/deployment/common/types" + "github.com/smartcontractkit/chainlink/deployment/environment/memory" + "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" +) + +func TestAcceptAllOwnership(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + cfg := memory.MemoryEnvironmentConfig{ + Nodes: 1, + Chains: 2, + } + env := memory.NewMemoryEnvironment(t, lggr, zapcore.DebugLevel, cfg) + registrySel := env.AllChainSelectors()[0] + chCapReg, err := changeset.DeployCapabilityRegistry(env, registrySel) + require.NoError(t, err) + require.NotNil(t, chCapReg) + err = env.ExistingAddresses.Merge(chCapReg.AddressBook) + require.NoError(t, err) + + chOcr3, err := changeset.DeployOCR3(env, registrySel) + require.NoError(t, err) + require.NotNil(t, chOcr3) + err = env.ExistingAddresses.Merge(chOcr3.AddressBook) + require.NoError(t, err) + + chForwarder, err := changeset.DeployForwarder(env, registrySel) + require.NoError(t, err) + require.NotNil(t, chForwarder) + err = env.ExistingAddresses.Merge(chForwarder.AddressBook) + require.NoError(t, err) + + chConsumer, err := changeset.DeployFeedsConsumer(env, &changeset.DeployFeedsConsumerRequest{ + ChainSelector: registrySel, + }) + require.NoError(t, err) + require.NotNil(t, chConsumer) + err = env.ExistingAddresses.Merge(chConsumer.AddressBook) + require.NoError(t, err) + + chMcms, err := commonchangeset.DeployMCMSWithTimelock(env, map[uint64]types.MCMSWithTimelockConfig{ + registrySel: { + Canceller: commonchangeset.SingleGroupMCMS(t), + Bypasser: commonchangeset.SingleGroupMCMS(t), + Proposer: commonchangeset.SingleGroupMCMS(t), + TimelockExecutors: env.AllDeployerKeys(), + TimelockMinDelay: big.NewInt(0), + }, + }) + err = env.ExistingAddresses.Merge(chMcms.AddressBook) + require.NoError(t, err) + + require.NoError(t, err) + require.NotNil(t, chMcms) + + resp, err := changeset.TransferAllOwnership(env, &changeset.TransferAllOwnershipRequest{ + ChainSelector: registrySel, + }) + require.NoError(t, err) + require.NotNil(t, resp) + + // Test the changeset + output, err := changeset.AcceptAllOwnershipsProposal(env, &changeset.AcceptAllOwnershipRequest{ + ChainSelector: registrySel, + MinDelay: time.Duration(0), + }) + require.NoError(t, err) + require.NotNil(t, output) + require.Len(t, output.Proposals, 1) + proposal := output.Proposals[0] + require.Len(t, proposal.Transactions, 1) + txs := proposal.Transactions[0] + require.Len(t, txs.Batch, 4) +} diff --git a/deployment/keystone/changeset/addrbook_utils.go b/deployment/keystone/changeset/addrbook_utils.go new file mode 100644 index 00000000000..4b6d6d151b1 --- /dev/null +++ b/deployment/keystone/changeset/addrbook_utils.go @@ -0,0 +1,112 @@ +package changeset + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + ccipowner "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/keystone" + capReg "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/feeds_consumer" + keystoneForwarder "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/forwarder" + ocr3Capability "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/ocr3_capability" +) + +// contractConstructor is a function type that takes an address and a client, +// returning the contract instance and an error. +type contractConstructor[T any] func(address common.Address, client bind.ContractBackend) (*T, error) + +// getContractsFromAddrBook retrieves a list of contract instances of a specified type from the address book. +// It uses the provided constructor to initialize matching contracts for the given chain. +func getContractsFromAddrBook[T any]( + addrBook deployment.AddressBook, + chain deployment.Chain, + desiredType deployment.ContractType, + constructor contractConstructor[T], +) ([]*T, error) { + chainAddresses, err := addrBook.AddressesForChain(chain.Selector) + if err != nil { + return nil, fmt.Errorf("failed to get addresses for chain %d: %w", chain.Selector, err) + } + + var contracts []*T + for addr, typeAndVersion := range chainAddresses { + if typeAndVersion.Type == desiredType { + address := common.HexToAddress(addr) + contractInstance, err := constructor(address, chain.Client) + if err != nil { + return nil, fmt.Errorf("failed to construct %s at %s: %w", desiredType, addr, err) + } + contracts = append(contracts, contractInstance) + } + } + + if len(contracts) == 0 { + return nil, fmt.Errorf("no %s found for chain %d", desiredType, chain.Selector) + } + + return contracts, nil +} + +// capRegistriesFromAddrBook retrieves CapabilitiesRegistry contracts for the given chain. +func capRegistriesFromAddrBook(addrBook deployment.AddressBook, chain deployment.Chain) ([]*capReg.CapabilitiesRegistry, error) { + return getContractsFromAddrBook[capReg.CapabilitiesRegistry]( + addrBook, + chain, + keystone.CapabilitiesRegistry, + capReg.NewCapabilitiesRegistry, + ) +} + +// ocr3FromAddrBook retrieves OCR3Capability contracts for the given chain. +func ocr3FromAddrBook(addrBook deployment.AddressBook, chain deployment.Chain) ([]*ocr3Capability.OCR3Capability, error) { + return getContractsFromAddrBook[ocr3Capability.OCR3Capability]( + addrBook, + chain, + keystone.OCR3Capability, + ocr3Capability.NewOCR3Capability, + ) +} + +// forwardersFromAddrBook retrieves KeystoneForwarder contracts for the given chain. +func forwardersFromAddrBook(addrBook deployment.AddressBook, chain deployment.Chain) ([]*keystoneForwarder.KeystoneForwarder, error) { + return getContractsFromAddrBook[keystoneForwarder.KeystoneForwarder]( + addrBook, + chain, + keystone.KeystoneForwarder, + keystoneForwarder.NewKeystoneForwarder, + ) +} + +// feedsConsumersFromAddrBook retrieves FeedsConsumer contracts for the given chain. +func feedsConsumersFromAddrBook(addrBook deployment.AddressBook, chain deployment.Chain) ([]*feeds_consumer.KeystoneFeedsConsumer, error) { + return getContractsFromAddrBook[feeds_consumer.KeystoneFeedsConsumer]( + addrBook, + chain, + keystone.FeedConsumer, + feeds_consumer.NewKeystoneFeedsConsumer, + ) +} + +// proposersFromAddrBook retrieves ManyChainMultiSig proposer contracts for the given chain. +func proposersFromAddrBook(addrBook deployment.AddressBook, chain deployment.Chain) ([]*ccipowner.ManyChainMultiSig, error) { + return getContractsFromAddrBook[ccipowner.ManyChainMultiSig]( + addrBook, + chain, + keystone.ProposerManyChainMultiSig, + ccipowner.NewManyChainMultiSig, + ) +} + +// timelocksFromAddrBook retrieves RBACTimelock contracts for the given chain. +func timelocksFromAddrBook(addrBook deployment.AddressBook, chain deployment.Chain) ([]*ccipowner.RBACTimelock, error) { + return getContractsFromAddrBook[ccipowner.RBACTimelock]( + addrBook, + chain, + keystone.RBACTimelock, + ccipowner.NewRBACTimelock, + ) +} diff --git a/deployment/keystone/changeset/deploy_consumer.go b/deployment/keystone/changeset/deploy_consumer.go new file mode 100644 index 00000000000..fc7992e2a7d --- /dev/null +++ b/deployment/keystone/changeset/deploy_consumer.go @@ -0,0 +1,31 @@ +package changeset + +import ( + "fmt" + + "github.com/smartcontractkit/chainlink/deployment" + kslib "github.com/smartcontractkit/chainlink/deployment/keystone" +) + +type DeployFeedsConsumerRequest struct { + ChainSelector uint64 +} + +var _ deployment.ChangeSet[*DeployFeedsConsumerRequest] = DeployFeedsConsumer + +// DeployFeedsConsumer deploys the FeedsConsumer contract to the chain with the given chainSelector. +func DeployFeedsConsumer(env deployment.Environment, req *DeployFeedsConsumerRequest) (deployment.ChangesetOutput, error) { + chainSelector := req.ChainSelector + lggr := env.Logger + chain, ok := env.Chains[chainSelector] + if !ok { + return deployment.ChangesetOutput{}, fmt.Errorf("chain not found in environment") + } + ab := deployment.NewMemoryAddressBook() + deployResp, err := kslib.DeployFeedsConsumer(chain, ab) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to deploy FeedsConsumer: %w", err) + } + lggr.Infof("Deployed %s chain selector %d addr %s", deployResp.Tv.String(), chain.Selector, deployResp.Address.String()) + return deployment.ChangesetOutput{AddressBook: ab}, nil +} diff --git a/deployment/keystone/changeset/deploy_consumer_test.go b/deployment/keystone/changeset/deploy_consumer_test.go new file mode 100644 index 00000000000..9a1e8f57da7 --- /dev/null +++ b/deployment/keystone/changeset/deploy_consumer_test.go @@ -0,0 +1,40 @@ +package changeset_test + +import ( + "testing" + + "go.uber.org/zap/zapcore" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink/deployment/environment/memory" + "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" +) + +func TestDeployFeedsConsumer(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + cfg := memory.MemoryEnvironmentConfig{ + Nodes: 1, + Chains: 2, + } + env := memory.NewMemoryEnvironment(t, lggr, zapcore.DebugLevel, cfg) + + registrySel := env.AllChainSelectors()[0] + resp, err := changeset.DeployFeedsConsumer(env, &changeset.DeployFeedsConsumerRequest{ + ChainSelector: registrySel, + }) + require.NoError(t, err) + require.NotNil(t, resp) + // feeds consumer should be deployed on chain 0 + addrs, err := resp.AddressBook.AddressesForChain(registrySel) + require.NoError(t, err) + require.Len(t, addrs, 1) + + // no feeds consumer registry on chain 1 + require.NotEqual(t, registrySel, env.AllChainSelectors()[1]) + oaddrs, _ := resp.AddressBook.AddressesForChain(env.AllChainSelectors()[1]) + require.Len(t, oaddrs, 0) +} diff --git a/deployment/keystone/changeset/deploy_forwarder.go b/deployment/keystone/changeset/deploy_forwarder.go index 55ab0dcd86d..2dc160dcf4c 100644 --- a/deployment/keystone/changeset/deploy_forwarder.go +++ b/deployment/keystone/changeset/deploy_forwarder.go @@ -22,10 +22,11 @@ func DeployForwarder(env deployment.Environment, registryChainSel uint64) (deplo ab := deployment.NewMemoryAddressBook() for _, chain := range env.Chains { lggr.Infow("deploying forwarder", "chainSelector", chain.Selector) - err := kslib.DeployForwarder(lggr, chain, ab) + forwarderResp, err := kslib.DeployForwarder(chain, ab) if err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to deploy KeystoneForwarder to chain selector %d: %w", chain.Selector, err) } + lggr.Infof("Deployed %s chain selector %d addr %s", forwarderResp.Tv.String(), chain.Selector, forwarderResp.Address.String()) } return deployment.ChangesetOutput{AddressBook: ab}, nil diff --git a/deployment/keystone/changeset/deploy_ocr3.go b/deployment/keystone/changeset/deploy_ocr3.go index 6684d8e046b..a88fe4afa62 100644 --- a/deployment/keystone/changeset/deploy_ocr3.go +++ b/deployment/keystone/changeset/deploy_ocr3.go @@ -4,11 +4,13 @@ import ( "fmt" "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink/deployment" kslib "github.com/smartcontractkit/chainlink/deployment/keystone" ) func DeployOCR3(env deployment.Environment, config interface{}) (deployment.ChangesetOutput, error) { + lggr := env.Logger registryChainSel, ok := config.(uint64) if !ok { return deployment.ChangesetOutput{}, deployment.ErrInvalidConfig @@ -19,10 +21,11 @@ func DeployOCR3(env deployment.Environment, config interface{}) (deployment.Chan if !ok { return deployment.ChangesetOutput{}, fmt.Errorf("chain not found in environment") } - err := kslib.DeployOCR3(env.Logger, c, ab) + ocr3Resp, err := kslib.DeployOCR3(c, ab) if err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to deploy OCR3Capability: %w", err) } + lggr.Infof("Deployed %s chain selector %d addr %s", ocr3Resp.Tv.String(), c.Selector, ocr3Resp.Address.String()) return deployment.ChangesetOutput{AddressBook: ab}, nil } diff --git a/deployment/keystone/changeset/deploy_registry.go b/deployment/keystone/changeset/deploy_registry.go index e9b142812d7..d07e2728282 100644 --- a/deployment/keystone/changeset/deploy_registry.go +++ b/deployment/keystone/changeset/deploy_registry.go @@ -10,14 +10,17 @@ import ( var _ deployment.ChangeSet[uint64] = DeployCapabilityRegistry func DeployCapabilityRegistry(env deployment.Environment, registrySelector uint64) (deployment.ChangesetOutput, error) { + lggr := env.Logger chain, ok := env.Chains[registrySelector] if !ok { return deployment.ChangesetOutput{}, fmt.Errorf("chain not found in environment") } ab := deployment.NewMemoryAddressBook() - err := kslib.DeployCapabilitiesRegistry(env.Logger, chain, ab) + capabilitiesRegistryResp, err := kslib.DeployCapabilitiesRegistry(chain, ab) if err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to deploy CapabilitiesRegistry: %w", err) } + lggr.Infof("Deployed %s chain selector %d addr %s", capabilitiesRegistryResp.Tv.String(), chain.Selector, capabilitiesRegistryResp.Address.String()) + return deployment.ChangesetOutput{AddressBook: ab}, nil } diff --git a/deployment/keystone/changeset/internal/test/utils.go b/deployment/keystone/changeset/internal/test/utils.go index cea20fd327d..7c0ab557a83 100644 --- a/deployment/keystone/changeset/internal/test/utils.go +++ b/deployment/keystone/changeset/internal/test/utils.go @@ -13,6 +13,7 @@ import ( capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/values" + "github.com/smartcontractkit/chainlink/deployment" "github.com/smartcontractkit/chainlink/deployment/environment/memory" @@ -43,7 +44,7 @@ type SetupTestRegistryResponse struct { func SetupTestRegistry(t *testing.T, lggr logger.Logger, req *SetupTestRegistryRequest) *SetupTestRegistryResponse { chain := testChain(t) // deploy the registry - registry := deployCapReg(t, lggr, chain) + registry := deployCapReg(t, chain) // convert req to nodeoperators nops := make([]kcr.CapabilitiesRegistryNodeOperator, 0) for nop := range req.NopToNodes { @@ -101,9 +102,10 @@ func SetupTestRegistry(t *testing.T, lggr logger.Logger, req *SetupTestRegistryR } } -func deployCapReg(t *testing.T, lggr logger.Logger, chain deployment.Chain) *kcr.CapabilitiesRegistry { - capabilitiesRegistryDeployer := kslib.NewCapabilitiesRegistryDeployer(lggr) - _, err := capabilitiesRegistryDeployer.Deploy(kslib.DeployRequest{Chain: chain}) +func deployCapReg(t *testing.T, chain deployment.Chain) *kcr.CapabilitiesRegistry { + capabilitiesRegistryDeployer, err := kslib.NewCapabilitiesRegistryDeployer() + require.NoError(t, err) + _, err = capabilitiesRegistryDeployer.Deploy(kslib.DeployRequest{Chain: chain}) require.NoError(t, err) return capabilitiesRegistryDeployer.Contract() } diff --git a/deployment/keystone/changeset/transfer_ownership.go b/deployment/keystone/changeset/transfer_ownership.go new file mode 100644 index 00000000000..73af5d5bdb2 --- /dev/null +++ b/deployment/keystone/changeset/transfer_ownership.go @@ -0,0 +1,84 @@ +package changeset + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/common/changeset" +) + +func toOwnershipTransferrer[T changeset.OwnershipTransferrer](items []T) []changeset.OwnershipTransferrer { + ownershipAcceptors := make([]changeset.OwnershipTransferrer, len(items)) + for i, item := range items { + ownershipAcceptors[i] = item + } + return ownershipAcceptors +} + +type TransferAllOwnershipRequest struct { + ChainSelector uint64 +} + +var _ deployment.ChangeSet[*TransferAllOwnershipRequest] = TransferAllOwnership + +// TransferAllOwnership transfers ownership of all Keystone contracts in the address book to the existing timelock. +func TransferAllOwnership(e deployment.Environment, req *TransferAllOwnershipRequest) (deployment.ChangesetOutput, error) { + chainSelector := req.ChainSelector + chain := e.Chains[chainSelector] + addrBook := e.ExistingAddresses + + // Fetch timelocks for the specified chain. + timelocks, err := timelocksFromAddrBook(addrBook, chain) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to fetch timelocks: %w", err) + } + if len(timelocks) == 0 { + return deployment.ChangesetOutput{}, fmt.Errorf("no timelocks found for chain %d", chainSelector) + } + + // Fetch contracts from the address book. + capRegs, err := capRegistriesFromAddrBook(addrBook, chain) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to fetch capabilities registries: %w", err) + } + + ocr3s, err := ocr3FromAddrBook(addrBook, chain) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to fetch OCR3 capabilities: %w", err) + } + + forwarders, err := forwardersFromAddrBook(addrBook, chain) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to fetch forwarders: %w", err) + } + + consumers, err := feedsConsumersFromAddrBook(addrBook, chain) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to fetch feeds consumers: %w", err) + } + + // Initialize the Contracts slice + var ownershipTransferrers []changeset.OwnershipTransferrer + + // Append all contracts + ownershipTransferrers = append(ownershipTransferrers, toOwnershipTransferrer(capRegs)...) + ownershipTransferrers = append(ownershipTransferrers, toOwnershipTransferrer(ocr3s)...) + ownershipTransferrers = append(ownershipTransferrers, toOwnershipTransferrer(forwarders)...) + ownershipTransferrers = append(ownershipTransferrers, toOwnershipTransferrer(consumers)...) + + // Construct the configuration + cfg := changeset.TransferOwnershipConfig{ + OwnersPerChain: map[uint64]common.Address{ + // Assuming there is only one timelock per chain. + chainSelector: timelocks[0].Address(), + }, + Contracts: map[uint64][]changeset.OwnershipTransferrer{ + chainSelector: ownershipTransferrers, + }, + } + + // Create and return the changeset + return changeset.NewTransferOwnershipChangeset(e, cfg) +} diff --git a/deployment/keystone/changeset/transfer_ownership_test.go b/deployment/keystone/changeset/transfer_ownership_test.go new file mode 100644 index 00000000000..dc5630076bd --- /dev/null +++ b/deployment/keystone/changeset/transfer_ownership_test.go @@ -0,0 +1,72 @@ +package changeset_test + +import ( + "math/big" + "testing" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + + commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/deployment/common/types" + "github.com/smartcontractkit/chainlink/deployment/environment/memory" + "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" +) + +func TestTransferAllOwnership(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + cfg := memory.MemoryEnvironmentConfig{ + Nodes: 1, + Chains: 2, + } + env := memory.NewMemoryEnvironment(t, lggr, zapcore.DebugLevel, cfg) + registrySel := env.AllChainSelectors()[0] + chCapReg, err := changeset.DeployCapabilityRegistry(env, registrySel) + require.NoError(t, err) + require.NotNil(t, chCapReg) + err = env.ExistingAddresses.Merge(chCapReg.AddressBook) + require.NoError(t, err) + + chOcr3, err := changeset.DeployOCR3(env, registrySel) + require.NoError(t, err) + require.NotNil(t, chOcr3) + err = env.ExistingAddresses.Merge(chOcr3.AddressBook) + require.NoError(t, err) + + chForwarder, err := changeset.DeployForwarder(env, registrySel) + require.NoError(t, err) + require.NotNil(t, chForwarder) + err = env.ExistingAddresses.Merge(chForwarder.AddressBook) + require.NoError(t, err) + + chConsumer, err := changeset.DeployFeedsConsumer(env, &changeset.DeployFeedsConsumerRequest{ + ChainSelector: registrySel, + }) + require.NoError(t, err) + require.NotNil(t, chConsumer) + err = env.ExistingAddresses.Merge(chConsumer.AddressBook) + require.NoError(t, err) + + chMcms, err := commonchangeset.DeployMCMSWithTimelock(env, map[uint64]types.MCMSWithTimelockConfig{ + registrySel: { + Canceller: commonchangeset.SingleGroupMCMS(t), + Bypasser: commonchangeset.SingleGroupMCMS(t), + Proposer: commonchangeset.SingleGroupMCMS(t), + TimelockExecutors: env.AllDeployerKeys(), + TimelockMinDelay: big.NewInt(0), + }, + }) + err = env.ExistingAddresses.Merge(chMcms.AddressBook) + require.NoError(t, err) + + require.NoError(t, err) + require.NotNil(t, chMcms) + + resp, err := changeset.TransferAllOwnership(env, &changeset.TransferAllOwnershipRequest{ + ChainSelector: registrySel, + }) + require.NoError(t, err) + require.NotNil(t, resp) +} diff --git a/deployment/keystone/consumer_deployer.go b/deployment/keystone/consumer_deployer.go new file mode 100644 index 00000000000..12d148144b5 --- /dev/null +++ b/deployment/keystone/consumer_deployer.go @@ -0,0 +1,55 @@ +package keystone + +import ( + "fmt" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/feeds_consumer" +) + +type KeystoneFeedsConsumerDeployer struct { + lggr logger.Logger + contract *feeds_consumer.KeystoneFeedsConsumer +} + +func NewKeystoneFeedsConsumerDeployer() (*KeystoneFeedsConsumerDeployer, error) { + lggr, err := logger.New() + if err != nil { + return nil, err + } + return &KeystoneFeedsConsumerDeployer{lggr: lggr}, nil +} + +func (c *KeystoneFeedsConsumerDeployer) deploy(req DeployRequest) (*DeployResponse, error) { + est, err := estimateDeploymentGas(req.Chain.Client, feeds_consumer.KeystoneFeedsConsumerABI) + if err != nil { + return nil, fmt.Errorf("failed to estimate gas: %w", err) + } + c.lggr.Debugf("Feeds Consumer estimated gas: %d", est) + + consumerAddr, tx, consumer, err := feeds_consumer.DeployKeystoneFeedsConsumer( + req.Chain.DeployerKey, + req.Chain.Client) + if err != nil { + return nil, fmt.Errorf("failed to deploy feeds consumer: %w", err) + } + + _, err = req.Chain.Confirm(tx) + if err != nil { + return nil, fmt.Errorf("failed to confirm and save feeds consumer: %w", err) + } + + tv := deployment.TypeAndVersion{ + Type: FeedConsumer, + } + + resp := &DeployResponse{ + Address: consumerAddr, + Tx: tx.Hash(), + Tv: tv, + } + c.contract = consumer + return resp, nil +} diff --git a/deployment/keystone/contract_set.go b/deployment/keystone/contract_set.go index ccd89f6905f..ee503a54b4d 100644 --- a/deployment/keystone/contract_set.go +++ b/deployment/keystone/contract_set.go @@ -3,8 +3,6 @@ package keystone import ( "fmt" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink/deployment" ) @@ -18,7 +16,7 @@ type deployContractSetResponse struct { deployment.AddressBook } -func deployContractsToChain(lggr logger.Logger, req deployContractsRequest) (*deployContractSetResponse, error) { +func deployContractsToChain(req deployContractsRequest) (*deployContractSetResponse, error) { if req.ad == nil { req.ad = deployment.NewMemoryAddressBook() } @@ -29,16 +27,16 @@ func deployContractsToChain(lggr logger.Logger, req deployContractsRequest) (*de // cap reg and ocr3 only deployed on registry chain if req.isRegistryChain { - err := DeployCapabilitiesRegistry(lggr, req.chain, resp.AddressBook) + _, err := DeployCapabilitiesRegistry(req.chain, resp.AddressBook) if err != nil { return nil, fmt.Errorf("failed to deploy CapabilitiesRegistry: %w", err) } - err = DeployOCR3(lggr, req.chain, resp.AddressBook) + _, err = DeployOCR3(req.chain, resp.AddressBook) if err != nil { return nil, fmt.Errorf("failed to deploy OCR3Capability: %w", err) } } - err := DeployForwarder(lggr, req.chain, resp.AddressBook) + _, err := DeployForwarder(req.chain, resp.AddressBook) if err != nil { return nil, fmt.Errorf("failed to deploy KeystoneForwarder: %w", err) } @@ -47,48 +45,71 @@ func deployContractsToChain(lggr logger.Logger, req deployContractsRequest) (*de // DeployCapabilitiesRegistry deploys the CapabilitiesRegistry contract to the chain // and saves the address in the address book. This mutates the address book. -func DeployCapabilitiesRegistry(lggr logger.Logger, chain deployment.Chain, ab deployment.AddressBook) error { - capabilitiesRegistryDeployer := CapabilitiesRegistryDeployer{lggr: lggr} +func DeployCapabilitiesRegistry(chain deployment.Chain, ab deployment.AddressBook) (*DeployResponse, error) { + capabilitiesRegistryDeployer, err := NewCapabilitiesRegistryDeployer() capabilitiesRegistryResp, err := capabilitiesRegistryDeployer.Deploy(DeployRequest{Chain: chain}) if err != nil { - return fmt.Errorf("failed to deploy CapabilitiesRegistry: %w", err) + return nil, fmt.Errorf("failed to deploy CapabilitiesRegistry: %w", err) } err = ab.Save(chain.Selector, capabilitiesRegistryResp.Address.String(), capabilitiesRegistryResp.Tv) if err != nil { - return fmt.Errorf("failed to save CapabilitiesRegistry: %w", err) + return nil, fmt.Errorf("failed to save CapabilitiesRegistry: %w", err) } - lggr.Infof("Deployed %s chain selector %d addr %s", capabilitiesRegistryResp.Tv.String(), chain.Selector, capabilitiesRegistryResp.Address.String()) - return nil + return capabilitiesRegistryResp, nil } // DeployOCR3 deploys the OCR3Capability contract to the chain // and saves the address in the address book. This mutates the address book. -func DeployOCR3(lggr logger.Logger, chain deployment.Chain, ab deployment.AddressBook) error { - ocr3Deployer := OCR3Deployer{lggr: lggr} +func DeployOCR3(chain deployment.Chain, ab deployment.AddressBook) (*DeployResponse, error) { + ocr3Deployer, err := NewOCR3Deployer() + if err != nil { + return nil, fmt.Errorf("failed to create OCR3Deployer: %w", err) + } ocr3Resp, err := ocr3Deployer.deploy(DeployRequest{Chain: chain}) if err != nil { - return fmt.Errorf("failed to deploy OCR3Capability: %w", err) + return nil, fmt.Errorf("failed to deploy OCR3Capability: %w", err) } err = ab.Save(chain.Selector, ocr3Resp.Address.String(), ocr3Resp.Tv) if err != nil { - return fmt.Errorf("failed to save OCR3Capability: %w", err) + return nil, fmt.Errorf("failed to save OCR3Capability: %w", err) } - lggr.Infof("Deployed %s chain selector %d addr %s", ocr3Resp.Tv.String(), chain.Selector, ocr3Resp.Address.String()) - return nil + + return ocr3Resp, nil } // DeployForwarder deploys the KeystoneForwarder contract to the chain // and saves the address in the address book. This mutates the address book. -func DeployForwarder(lggr logger.Logger, chain deployment.Chain, ab deployment.AddressBook) error { - forwarderDeployer := KeystoneForwarderDeployer{lggr: lggr} +func DeployForwarder(chain deployment.Chain, ab deployment.AddressBook) (*DeployResponse, error) { + forwarderDeployer, err := NewKeystoneForwarderDeployer() + if err != nil { + return nil, fmt.Errorf("failed to create KeystoneForwarderDeployer: %w", err) + } forwarderResp, err := forwarderDeployer.deploy(DeployRequest{Chain: chain}) if err != nil { - return fmt.Errorf("failed to deploy KeystoneForwarder: %w", err) + return nil, fmt.Errorf("failed to deploy KeystoneForwarder: %w", err) } err = ab.Save(chain.Selector, forwarderResp.Address.String(), forwarderResp.Tv) if err != nil { - return fmt.Errorf("failed to save KeystoneForwarder: %w", err) + return nil, fmt.Errorf("failed to save KeystoneForwarder: %w", err) + } + return forwarderResp, nil +} + +// DeployFeedsConsumer deploys the KeystoneFeedsConsumer contract to the chain +// and saves the address in the address book. This mutates the address book. +func DeployFeedsConsumer(chain deployment.Chain, ab deployment.AddressBook) (*DeployResponse, error) { + consumerDeploy, err := NewKeystoneFeedsConsumerDeployer() + if err != nil { + return nil, err + } + consumerResp, err := consumerDeploy.deploy(DeployRequest{Chain: chain}) + if err != nil { + return nil, fmt.Errorf("failed to deploy FeedsConsumer: %w", err) + } + err = ab.Save(chain.Selector, consumerResp.Address.String(), consumerResp.Tv) + if err != nil { + + return nil, fmt.Errorf("failed to save FeedsConsumer: %w", err) } - lggr.Infof("Deployed %s chain selector %d addr %s", forwarderResp.Tv.String(), chain.Selector, forwarderResp.Address.String()) - return nil + return consumerResp, nil } diff --git a/deployment/keystone/deploy.go b/deployment/keystone/deploy.go index 874b98600ae..7b304c1ba0c 100644 --- a/deployment/keystone/deploy.go +++ b/deployment/keystone/deploy.go @@ -25,6 +25,7 @@ import ( capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" "github.com/smartcontractkit/chainlink-common/pkg/values" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" kf "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/forwarder" @@ -75,7 +76,7 @@ func ConfigureContracts(ctx context.Context, lggr logger.Logger, req ConfigureCo addrBook := req.Env.ExistingAddresses if req.DoContractDeploy { - contractDeployCS, err := DeployContracts(lggr, req.Env, req.RegistryChainSel) + contractDeployCS, err := DeployContracts(req.Env, req.RegistryChainSel) if err != nil { return nil, fmt.Errorf("failed to deploy contracts: %w", err) } @@ -121,12 +122,13 @@ func ConfigureContracts(ctx context.Context, lggr logger.Logger, req ConfigureCo } // DeployContracts deploys the all the keystone contracts on all chains and returns the address book in the changeset -func DeployContracts(lggr logger.Logger, e *deployment.Environment, chainSel uint64) (*deployment.ChangesetOutput, error) { +func DeployContracts(e *deployment.Environment, chainSel uint64) (*deployment.ChangesetOutput, error) { + lggr := e.Logger adbook := deployment.NewMemoryAddressBook() // deploy contracts on all chains and track the registry and ocr3 contracts for _, chain := range e.Chains { lggr.Infow("deploying contracts", "chain", chain.Selector) - deployResp, err := deployContractsToChain(lggr, deployContractsRequest{ + deployResp, err := deployContractsToChain(deployContractsRequest{ chain: chain, isRegistryChain: chain.Selector == chainSel, }, diff --git a/deployment/keystone/deploy_test.go b/deployment/keystone/deploy_test.go index a3931550cfa..b02497c22fa 100644 --- a/deployment/keystone/deploy_test.go +++ b/deployment/keystone/deploy_test.go @@ -13,6 +13,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/smartcontractkit/chainlink/deployment" "github.com/smartcontractkit/chainlink/deployment/environment/clo" "github.com/smartcontractkit/chainlink/deployment/environment/clo/models" @@ -113,7 +114,7 @@ func TestDeploy(t *testing.T) { } // explicitly deploy the contracts - cs, err := keystone.DeployContracts(lggr, &env, sepoliaChainSel) + cs, err := keystone.DeployContracts(&env, sepoliaChainSel) require.NoError(t, err) env.ExistingAddresses = cs.AddressBook deployReq := keystone.ConfigureContractsRequest{ @@ -311,7 +312,7 @@ func TestDeployCLO(t *testing.T) { ctx := tests.Context(t) // explicitly deploy the contracts - cs, err := keystone.DeployContracts(lggr, env, registryChainSel) + cs, err := keystone.DeployContracts(env, registryChainSel) require.NoError(t, err) // Deploy successful these are now part of our env. require.NoError(t, env.ExistingAddresses.Merge(cs.AddressBook)) diff --git a/deployment/keystone/forwarder_deployer.go b/deployment/keystone/forwarder_deployer.go index b7fde675973..cf29b20c693 100644 --- a/deployment/keystone/forwarder_deployer.go +++ b/deployment/keystone/forwarder_deployer.go @@ -5,6 +5,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink/deployment" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/forwarder" ) @@ -14,6 +15,13 @@ type KeystoneForwarderDeployer struct { contract *forwarder.KeystoneForwarder } +func NewKeystoneForwarderDeployer() (*KeystoneForwarderDeployer, error) { + lggr, err := logger.New() + if err != nil { + return nil, err + } + return &KeystoneForwarderDeployer{lggr: lggr}, nil +} func (c *KeystoneForwarderDeployer) deploy(req DeployRequest) (*DeployResponse, error) { est, err := estimateDeploymentGas(req.Chain.Client, forwarder.KeystoneForwarderABI) if err != nil { diff --git a/deployment/keystone/ocr3_deployer.go b/deployment/keystone/ocr3_deployer.go index 5f2ba34f42c..227894f7c67 100644 --- a/deployment/keystone/ocr3_deployer.go +++ b/deployment/keystone/ocr3_deployer.go @@ -5,6 +5,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink/deployment" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/ocr3_capability" ) @@ -14,6 +15,14 @@ type OCR3Deployer struct { contract *ocr3_capability.OCR3Capability } +func NewOCR3Deployer() (*OCR3Deployer, error) { + lggr, err := logger.New() + if err != nil { + return nil, err + } + return &OCR3Deployer{lggr: lggr}, nil +} + func (c *OCR3Deployer) deploy(req DeployRequest) (*DeployResponse, error) { est, err := estimateDeploymentGas(req.Chain.Client, ocr3_capability.OCR3CapabilityABI) if err != nil { diff --git a/deployment/keystone/types.go b/deployment/keystone/types.go index b7bf636c3e2..4b00e5e4dc0 100644 --- a/deployment/keystone/types.go +++ b/deployment/keystone/types.go @@ -20,10 +20,12 @@ import ( ) var ( - CapabilitiesRegistry deployment.ContractType = "CapabilitiesRegistry" // https://github.com/smartcontractkit/chainlink/blob/50c1b3dbf31bd145b312739b08967600a5c67f30/contracts/src/v0.8/keystone/CapabilitiesRegistry.sol#L392 - KeystoneForwarder deployment.ContractType = "KeystoneForwarder" // https://github.com/smartcontractkit/chainlink/blob/50c1b3dbf31bd145b312739b08967600a5c67f30/contracts/src/v0.8/keystone/KeystoneForwarder.sol#L90 - OCR3Capability deployment.ContractType = "OCR3Capability" // https://github.com/smartcontractkit/chainlink/blob/50c1b3dbf31bd145b312739b08967600a5c67f30/contracts/src/v0.8/keystone/OCR3Capability.sol#L12 - FeedConsumer deployment.ContractType = "FeedConsumer" // no type and a version in contract https://github.com/smartcontractkit/chainlink/blob/89183a8a5d22b1aeca0ade3b76d16aa84067aa57/contracts/src/v0.8/keystone/KeystoneFeedsConsumer.sol#L1 + CapabilitiesRegistry deployment.ContractType = "CapabilitiesRegistry" // https://github.com/smartcontractkit/chainlink/blob/50c1b3dbf31bd145b312739b08967600a5c67f30/contracts/src/v0.8/keystone/CapabilitiesRegistry.sol#L392 + KeystoneForwarder deployment.ContractType = "KeystoneForwarder" // https://github.com/smartcontractkit/chainlink/blob/50c1b3dbf31bd145b312739b08967600a5c67f30/contracts/src/v0.8/keystone/KeystoneForwarder.sol#L90 + OCR3Capability deployment.ContractType = "OCR3Capability" // https://github.com/smartcontractkit/chainlink/blob/50c1b3dbf31bd145b312739b08967600a5c67f30/contracts/src/v0.8/keystone/OCR3Capability.sol#L12 + FeedConsumer deployment.ContractType = "FeedConsumer" // no type and a version in contract https://github.com/smartcontractkit/chainlink/blob/89183a8a5d22b1aeca0ade3b76d16aa84067aa57/contracts/src/v0.8/keystone/KeystoneFeedsConsumer.sol#L1 + RBACTimelock deployment.ContractType = "RBACTimelock" // no type and a version in contract https://github.com/smartcontractkit/ccip-owner-contracts/blob/main/src/RBACTimelock.sol + ProposerManyChainMultiSig deployment.ContractType = "ProposerManyChainMultiSig" // no type and a version in contract https://github.com/smartcontractkit/ccip-owner-contracts/blob/main/src/ManyChainMultiSig.sol ) type DeployResponse struct { diff --git a/integration-tests/smoke/ccip/ccip_gas_price_updates_test.go b/integration-tests/smoke/ccip/ccip_gas_price_updates_test.go new file mode 100644 index 00000000000..221d35bd992 --- /dev/null +++ b/integration-tests/smoke/ccip/ccip_gas_price_updates_test.go @@ -0,0 +1,126 @@ +package smoke + +import ( + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/exp/maps" + + "github.com/smartcontractkit/chainlink-common/pkg/config" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" + testsetups "github.com/smartcontractkit/chainlink/integration-tests/testsetups/ccip" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/fee_quoter" + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + +// Test_CCIPGasPriceUpdates tests that chain fee price updates are propagated correctly when +// price reaches some deviation threshold or when the price has expired. +func Test_CCIPGasPriceUpdates(t *testing.T) { + lggr := logger.TestLogger(t) + ctx := changeset.Context(t) + callOpts := &bind.CallOpts{Context: ctx} + + var gasPriceExpiry = 5 * time.Second + e, _, _ := testsetups.NewLocalDevEnvironmentWithDefaultPrice(t, lggr, &changeset.TestConfigs{ + OCRConfigOverride: func(params changeset.CCIPOCRParams) changeset.CCIPOCRParams { + params.CommitOffChainConfig.RemoteGasPriceBatchWriteFrequency = *config.MustNewDuration(gasPriceExpiry) + return params + }, + }) + state, err := changeset.LoadOnchainState(e.Env) + require.NoError(t, err) + require.NoError(t, changeset.AddLanesForAll(e.Env, state)) + + allChainSelectors := maps.Keys(e.Env.Chains) + assert.GreaterOrEqual(t, len(allChainSelectors), 2, "test requires at least 2 chains") + + sourceChain1 := allChainSelectors[0] + sourceChain2 := allChainSelectors[1] + + feeQuoter1 := state.Chains[sourceChain1].FeeQuoter + feeQuoter2 := state.Chains[sourceChain2].FeeQuoter + + // get initial chain fees + initialChain2Fee, err := feeQuoter1.GetDestinationChainGasPrice(callOpts, sourceChain2) + require.NoError(t, err) + initialChain1Fee, err := feeQuoter2.GetDestinationChainGasPrice(callOpts, sourceChain1) + require.NoError(t, err) + t.Logf("initial chain1 fee (stored in chain2): %v", initialChain1Fee) + t.Logf("initial chain2 fee (stored in chain1): %v", initialChain2Fee) + + // get latest price updates sequence number from the offRamps + offRampChain1 := state.Chains[sourceChain1].OffRamp + offRampChain2 := state.Chains[sourceChain2].OffRamp + priceUpdatesSeqNumChain1, err := offRampChain1.GetLatestPriceSequenceNumber(callOpts) + require.NoError(t, err) + priceUpdatesSeqNumChain2, err := offRampChain2.GetLatestPriceSequenceNumber(callOpts) + require.NoError(t, err) + t.Logf("priceUpdatesSeqNumChain1: %v", priceUpdatesSeqNumChain1) + t.Logf("priceUpdatesSeqNumChain2: %v", priceUpdatesSeqNumChain2) + + // update the price of chain2 + tx, err := feeQuoter1.UpdatePrices(e.Env.Chains[sourceChain1].DeployerKey, fee_quoter.InternalPriceUpdates{ + TokenPriceUpdates: nil, + GasPriceUpdates: []fee_quoter.InternalGasPriceUpdate{ + {DestChainSelector: sourceChain2, UsdPerUnitGas: big.NewInt(5123)}, + }, + }) + require.NoError(t, err) + _, err = deployment.ConfirmIfNoError(e.Env.Chains[sourceChain1], tx, err) + require.NoError(t, err) + + // assert that the chain fees are updated by the commit plugin reports + priceDeviationChecked := false // flag to check if price deviation condition was met + assert.Eventually(t, func() bool { + // offRamps should have updated the sequence number + priceUpdatesSeqNumChain1Now, err := offRampChain1.GetLatestPriceSequenceNumber(callOpts) + require.NoError(t, err) + priceUpdatesSeqNumChain2Now, err := offRampChain2.GetLatestPriceSequenceNumber(callOpts) + require.NoError(t, err) + t.Logf("priceUpdatesSeqNumChain1: %v", priceUpdatesSeqNumChain1Now) + t.Logf("priceUpdatesSeqNumChain2: %v", priceUpdatesSeqNumChain2Now) + if priceUpdatesSeqNumChain1Now <= priceUpdatesSeqNumChain1 { + return false + } + if priceUpdatesSeqNumChain2Now <= priceUpdatesSeqNumChain2 { + return false + } + + chain2FeeNow, err := feeQuoter1.GetDestinationChainGasPrice(callOpts, sourceChain2) + require.NoError(t, err) + chain1FeeNow, err := feeQuoter2.GetDestinationChainGasPrice(callOpts, sourceChain1) + require.NoError(t, err) + t.Logf("chainFee1 (stored in chain2): %v", chain1FeeNow) + t.Logf("chainFee2 (stored in chain1): %v", chain2FeeNow) + + if !priceDeviationChecked { + // make sure there was a price update for chain2 when price deviation was reached + if chain2FeeNow.Value.Cmp(initialChain2Fee.Value) == 0 { + t.Logf("chainFee2 not updated: %v original=%v", chain2FeeNow, initialChain2Fee.Value) + return false + } + require.NotEqual(t, chain2FeeNow.Timestamp, initialChain2Fee.Timestamp) + priceDeviationChecked = true + } + + // make sure there was a price update for chain1 but with the same price - when expiration is reached + if chain1FeeNow.Timestamp == initialChain1Fee.Timestamp { + t.Logf("chainFee1 timestamp not updated: %v original=%v", chain1FeeNow, initialChain1Fee.Timestamp) + initialChain1Fee = chain1FeeNow + return false + } + if chain1FeeNow.Value.Cmp(initialChain1Fee.Value) != 0 { + t.Logf("chainFee1 changed: %v prev:%v", chain1FeeNow, initialChain1Fee.Value) + initialChain1Fee = chain1FeeNow + return false + } + + return priceDeviationChecked + }, tests.WaitTimeout(t), 500*time.Millisecond) +} diff --git a/integration-tests/smoke/ccip/ccip_message_limitations_test.go b/integration-tests/smoke/ccip/ccip_message_limitations_test.go new file mode 100644 index 00000000000..a86644bae7e --- /dev/null +++ b/integration-tests/smoke/ccip/ccip_message_limitations_test.go @@ -0,0 +1,183 @@ +package smoke + +import ( + "math/big" + "slices" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "golang.org/x/exp/maps" + + "github.com/smartcontractkit/chainlink-testing-framework/lib/utils/testcontext" + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" + testsetups "github.com/smartcontractkit/chainlink/integration-tests/testsetups/ccip" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/router" + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + +func Test_CCIPMessageLimitations(t *testing.T) { + lggr := logger.TestLogger(t) + ctx := testcontext.Get(t) + callOpts := &bind.CallOpts{Context: ctx} + + testEnv, _, _ := testsetups.NewLocalDevEnvironmentWithDefaultPrice(t, lggr, &changeset.TestConfigs{}) + chains := maps.Keys(testEnv.Env.Chains) + + onChainState, err := changeset.LoadOnchainState(testEnv.Env) + require.NoError(t, err) + + require.NoError(t, changeset.AddLanesForAll(testEnv.Env, onChainState)) + + srcToken, _ := setupTokens( + t, + onChainState, + testEnv, + chains[0], + chains[1], + deployment.E18Mult(10_000), + deployment.E18Mult(10_000), + ) + + chain0DestConfig, err := onChainState.Chains[chains[0]].FeeQuoter.GetDestChainConfig(callOpts, chains[1]) + require.NoError(t, err) + t.Logf("0->1 destination config: %+v", chain0DestConfig) + + testMsgs := []struct { + name string + fromChain uint64 + toChain uint64 + msg router.ClientEVM2AnyMessage + expRevert bool + }{ + { + name: "hit limit on data", + fromChain: chains[0], + toChain: chains[1], + msg: router.ClientEVM2AnyMessage{ + Receiver: common.LeftPadBytes(onChainState.Chains[chains[1]].Receiver.Address().Bytes(), 32), + Data: []byte(strings.Repeat("0", int(chain0DestConfig.MaxDataBytes))), + FeeToken: common.HexToAddress("0x0"), + }, + }, + { + name: "hit limit on tokens", + fromChain: chains[0], + toChain: chains[1], + msg: router.ClientEVM2AnyMessage{ + Receiver: common.LeftPadBytes(onChainState.Chains[chains[1]].Receiver.Address().Bytes(), 32), + TokenAmounts: slices.Repeat([]router.ClientEVMTokenAmount{ + {Token: srcToken.Address(), Amount: big.NewInt(1)}, + }, int(chain0DestConfig.MaxNumberOfTokensPerMsg)), + FeeToken: common.HexToAddress("0x0"), + }, + }, + { + name: "hit limit on gas limit", + fromChain: chains[0], + toChain: chains[1], + msg: router.ClientEVM2AnyMessage{ + Receiver: common.LeftPadBytes(onChainState.Chains[chains[1]].Receiver.Address().Bytes(), 32), + Data: []byte(strings.Repeat("0", int(chain0DestConfig.MaxDataBytes))), + FeeToken: common.HexToAddress("0x0"), + ExtraArgs: changeset.MakeEVMExtraArgsV2(uint64(chain0DestConfig.MaxPerMsgGasLimit), true), + }, + }, + //{ // TODO: exec plugin never executed this message. CCIP-4471 + // name: "hit limit on maxDataBytes, tokens, gasLimit should succeed", + // fromChain: chains[0], + // toChain: chains[1], + // msg: router.ClientEVM2AnyMessage{ + // Receiver: common.LeftPadBytes(onChainState.Chains[chains[1]].Receiver.Address().Bytes(), 32), + // Data: []byte(strings.Repeat("0", int(chain0DestConfig.MaxDataBytes))), + // TokenAmounts: slices.Repeat([]router.ClientEVMTokenAmount{ + // {Token: srcToken.Address(), Amount: big.NewInt(1)}, + // }, int(chain0DestConfig.MaxNumberOfTokensPerMsg)), + // FeeToken: common.HexToAddress("0x0"), + // ExtraArgs: changeset.MakeEVMExtraArgsV2(uint64(chain0DestConfig.MaxPerMsgGasLimit), true), + // }, + //}, + { + name: "exceeding maxDataBytes", + fromChain: chains[0], + toChain: chains[1], + msg: router.ClientEVM2AnyMessage{ + Receiver: common.LeftPadBytes(onChainState.Chains[chains[1]].Receiver.Address().Bytes(), 32), + Data: []byte(strings.Repeat("0", int(chain0DestConfig.MaxDataBytes)+1)), + TokenAmounts: []router.ClientEVMTokenAmount{}, + FeeToken: common.HexToAddress("0x0"), + ExtraArgs: nil, + }, + expRevert: true, + }, + { + name: "exceeding number of tokens", + fromChain: chains[0], + toChain: chains[1], + msg: router.ClientEVM2AnyMessage{ + Receiver: common.LeftPadBytes(onChainState.Chains[chains[1]].Receiver.Address().Bytes(), 32), + Data: []byte("abc"), + TokenAmounts: slices.Repeat([]router.ClientEVMTokenAmount{ + {Token: srcToken.Address(), Amount: big.NewInt(1)}, + }, int(chain0DestConfig.MaxNumberOfTokensPerMsg)+1), + FeeToken: common.HexToAddress("0x0"), + ExtraArgs: nil, + }, + expRevert: true, + }, + { + name: "exceeding gas limit", + fromChain: chains[0], + toChain: chains[1], + msg: router.ClientEVM2AnyMessage{ + Receiver: common.LeftPadBytes(onChainState.Chains[chains[1]].Receiver.Address().Bytes(), 32), + Data: []byte("abc"), + TokenAmounts: []router.ClientEVMTokenAmount{}, + FeeToken: common.HexToAddress("0x0"), + ExtraArgs: changeset.MakeEVMExtraArgsV2(uint64(chain0DestConfig.MaxPerMsgGasLimit)+1, true), + }, + expRevert: true, + }, + } + + // Need to keep track of the block number for each chain so that event subscription can be done from that block. + startBlocks := make(map[uint64]*uint64) + // Send a message from each chain to every other chain. + expectedSeqNum := make(map[changeset.SourceDestPair]uint64) + expectedSeqNumExec := make(map[changeset.SourceDestPair][]uint64) + for _, msg := range testMsgs { + t.Logf("Sending msg: %s", msg.name) + require.NotEqual(t, msg.fromChain, msg.toChain, "fromChain and toChain cannot be the same") + startBlocks[msg.toChain] = nil + msgSentEvent, err := changeset.DoSendRequest( + t, testEnv.Env, onChainState, msg.fromChain, msg.toChain, false, msg.msg) + + if msg.expRevert { + t.Logf("Message reverted as expected") + require.Error(t, err) + require.Contains(t, err.Error(), "execution reverted") + continue + } + require.NoError(t, err) + + t.Logf("Message not reverted as expected") + + expectedSeqNum[changeset.SourceDestPair{ + SourceChainSelector: msg.fromChain, + DestChainSelector: msg.toChain, + }] = msgSentEvent.SequenceNumber + + expectedSeqNumExec[changeset.SourceDestPair{ + SourceChainSelector: msg.fromChain, + DestChainSelector: msg.toChain, + }] = []uint64{msgSentEvent.SequenceNumber} + } + + // Wait for all commit reports to land. + changeset.ConfirmCommitForAllWithExpectedSeqNums(t, testEnv.Env, onChainState, expectedSeqNum, startBlocks) + // Wait for all exec reports to land + changeset.ConfirmExecWithSeqNrsForAll(t, testEnv.Env, onChainState, expectedSeqNumExec, startBlocks) +} diff --git a/integration-tests/smoke/ccip/ccip_token_price_updates_test.go b/integration-tests/smoke/ccip/ccip_token_price_updates_test.go new file mode 100644 index 00000000000..6a193397d7e --- /dev/null +++ b/integration-tests/smoke/ccip/ccip_token_price_updates_test.go @@ -0,0 +1,152 @@ +package smoke + +import ( + "context" + "math" + "math/big" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/exp/maps" + + "github.com/smartcontractkit/chainlink-common/pkg/config" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/node" + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" + testsetups "github.com/smartcontractkit/chainlink/integration-tests/testsetups/ccip" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/fee_quoter" + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + +func Test_CCIPTokenPriceUpdates(t *testing.T) { + lggr := logger.TestLogger(t) + ctx := changeset.Context(t) + callOpts := &bind.CallOpts{Context: ctx} + + var tokenPriceExpiry = 5 * time.Second + e, _, _ := testsetups.NewLocalDevEnvironmentWithDefaultPrice(t, lggr, &changeset.TestConfigs{ + OCRConfigOverride: func(params changeset.CCIPOCRParams) changeset.CCIPOCRParams { + params.CommitOffChainConfig.TokenPriceBatchWriteFrequency = *config.MustNewDuration(tokenPriceExpiry) + return params + }, + }) + state, err := changeset.LoadOnchainState(e.Env) + require.NoError(t, err) + require.NoError(t, changeset.AddLanesForAll(e.Env, state)) + + allChainSelectors := maps.Keys(e.Env.Chains) + assert.GreaterOrEqual(t, len(allChainSelectors), 2, "test requires at least 2 chains") + + sourceChain1 := allChainSelectors[0] + + feeQuoter1 := state.Chains[sourceChain1].FeeQuoter + + feeTokensChain1, err := feeQuoter1.GetFeeTokens(callOpts) + require.NoError(t, err) + t.Logf("feeTokens: %v", feeTokensChain1) + + tokenPricesBefore, err := feeQuoter1.GetTokenPrices(callOpts, feeTokensChain1) + require.NoError(t, err) + t.Logf("tokenPrices: %v", tokenPricesBefore) + + // assert token prices updated due to time expiration + assert.Eventually(t, func() bool { + tokenPricesNow, err := feeQuoter1.GetTokenPrices(callOpts, feeTokensChain1) + require.NoError(t, err) + t.Logf("tokenPrices: %v", tokenPricesNow) + + // both tokens should have same price but different timestamp since there was an update due to time deviation + for i, price := range tokenPricesNow { + if tokenPricesBefore[i].Timestamp == price.Timestamp { + tokenPricesBefore = tokenPricesNow + return false // timestamp is the same + } + if tokenPricesBefore[i].Value.Cmp(price.Value) != 0 { + tokenPricesBefore = tokenPricesNow + return false // price was updated + } + } + t.Log("time expiration assertions complete") + return true + }, tests.WaitTimeout(t), 500*time.Millisecond) + + // disable oracles to prevent price updates while we manually edit token prices + disabledOracleIDs := disableOracles(ctx, t, e.Env.Offchain) + + assert.Eventually(t, func() bool { + // manually update token prices by setting values to maxUint64 and 0 + tx, err := feeQuoter1.UpdatePrices(e.Env.Chains[sourceChain1].DeployerKey, fee_quoter.InternalPriceUpdates{ + TokenPriceUpdates: []fee_quoter.InternalTokenPriceUpdate{ + {SourceToken: feeTokensChain1[0], UsdPerToken: big.NewInt(0).SetUint64(math.MaxUint64)}, + {SourceToken: feeTokensChain1[1], UsdPerToken: big.NewInt(0)}, + }, + }) + require.NoError(t, err) + + _, err = deployment.ConfirmIfNoError(e.Env.Chains[sourceChain1], tx, err) + require.NoError(t, err) + t.Logf("manually editing token prices") + + tokenPricesNow, err := feeQuoter1.GetTokenPrices(callOpts, feeTokensChain1) + require.NoError(t, err) + t.Logf("tokenPrices straight after: %v", tokenPricesNow) + + if uint64(math.MaxUint64) != tokenPricesNow[0].Value.Uint64() { + return false + } + if uint64(0) != tokenPricesNow[1].Value.Uint64() { + return false + } + return true + + // retry because there might've been a commit report inflight + }, tests.WaitTimeout(t), 200*time.Millisecond) + + enableOracles(ctx, t, e.Env.Offchain, disabledOracleIDs) + + // wait until price goes back to the original + assert.Eventually(t, func() bool { + tokenPricesNow, err := feeQuoter1.GetTokenPrices(callOpts, feeTokensChain1) + require.NoError(t, err) + t.Logf("tokenPrices: %v tokenPricesBefore: %v", tokenPricesNow, tokenPricesBefore) + + if tokenPricesNow[0].Value.Cmp(tokenPricesBefore[0].Value) != 0 { + return false + } + if tokenPricesNow[1].Value.Cmp(tokenPricesBefore[1].Value) != 0 { + return false + } + return true + }, tests.WaitTimeout(t), 500*time.Millisecond) +} + +func disableOracles(ctx context.Context, t *testing.T, client deployment.OffchainClient) []string { + var disabledOracleIDs []string + listNodesResp, err := client.ListNodes(ctx, &node.ListNodesRequest{}) + require.NoError(t, err) + + for _, n := range listNodesResp.Nodes { + if strings.HasPrefix(n.Name, "bootstrap") { + continue + } + _, err := client.DisableNode(ctx, &node.DisableNodeRequest{Id: n.Id}) + require.NoError(t, err) + disabledOracleIDs = append(disabledOracleIDs, n.Id) + t.Logf("node %s disabled", n.Id) + } + + return disabledOracleIDs +} + +func enableOracles(ctx context.Context, t *testing.T, client deployment.OffchainClient, oracleIDs []string) { + for _, n := range oracleIDs { + _, err := client.EnableNode(ctx, &node.EnableNodeRequest{Id: n}) + require.NoError(t, err) + t.Logf("node %s enabled", n) + } +} diff --git a/integration-tests/smoke/ccip/ccip_token_transfer_test.go b/integration-tests/smoke/ccip/ccip_token_transfer_test.go index 870648508e0..c1405314ab4 100644 --- a/integration-tests/smoke/ccip/ccip_token_transfer_test.go +++ b/integration-tests/smoke/ccip/ccip_token_transfer_test.go @@ -103,6 +103,7 @@ func TestTokenTransfer(t *testing.T) { tokenAmounts []router.ClientEVMTokenAmount receiver common.Address data []byte + extraData []byte expectedTokenBalances map[common.Address]*big.Int expectedExecutionState int }{ @@ -156,13 +157,59 @@ func TestTokenTransfer(t *testing.T) { Amount: oneE18, }, }, - receiver: state.Chains[sourceChain].Receiver.Address(), + receiver: state.Chains[sourceChain].Receiver.Address(), + extraData: changeset.MakeEVMExtraArgsV2(300_000, false), expectedTokenBalances: map[common.Address]*big.Int{ selfServeSrcToken.Address(): new(big.Int).Add(oneE18, oneE18), srcToken.Address(): oneE18, }, expectedExecutionState: changeset.EXECUTION_STATE_SUCCESS, }, + { + name: "Sending token transfer with custom gasLimits to the EOA is successful", + srcChain: destChain, + dstChain: sourceChain, + tokenAmounts: []router.ClientEVMTokenAmount{ + { + Token: selfServeDestToken.Address(), + Amount: oneE18, + }, + { + Token: destToken.Address(), + Amount: new(big.Int).Add(oneE18, oneE18), + }, + }, + receiver: utils.RandomAddress(), + extraData: changeset.MakeEVMExtraArgsV2(1, false), + expectedTokenBalances: map[common.Address]*big.Int{ + selfServeSrcToken.Address(): oneE18, + srcToken.Address(): new(big.Int).Add(oneE18, oneE18), + }, + expectedExecutionState: changeset.EXECUTION_STATE_SUCCESS, + }, + { + name: "Sending PTT with too low gas limit leads to the revert when receiver is a contract", + srcChain: destChain, + dstChain: sourceChain, + tokenAmounts: []router.ClientEVMTokenAmount{ + { + Token: selfServeDestToken.Address(), + Amount: oneE18, + }, + { + Token: destToken.Address(), + Amount: oneE18, + }, + }, + receiver: state.Chains[sourceChain].Receiver.Address(), + data: []byte("this should be reverted because gasLimit is too low, no tokens are transferred as well"), + extraData: changeset.MakeEVMExtraArgsV2(1, false), + expectedTokenBalances: map[common.Address]*big.Int{ + selfServeSrcToken.Address(): big.NewInt(0), + srcToken.Address(): big.NewInt(0), + }, + expectedExecutionState: changeset.EXECUTION_STATE_FAILURE, + }, } for _, tt := range tcs { @@ -184,6 +231,7 @@ func TestTokenTransfer(t *testing.T) { tt.receiver, tt.data, tt.expectedExecutionState, + tt.extraData, ) for token, balance := range tt.expectedTokenBalances { diff --git a/integration-tests/smoke/ccip/ccip_usdc_test.go b/integration-tests/smoke/ccip/ccip_usdc_test.go index f478392c0f7..97673d59aac 100644 --- a/integration-tests/smoke/ccip/ccip_usdc_test.go +++ b/integration-tests/smoke/ccip/ccip_usdc_test.go @@ -201,6 +201,7 @@ func TestUSDCTokenTransfer(t *testing.T) { tt.receiver, tt.data, tt.expectedExecutionState, + nil, ) for token, balance := range tt.expectedTokenBalances {