Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor helper to use in cli in CLD #15647

Merged
merged 8 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions deployment/ccip/changeset/accept_ownership_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
)

func Test_NewAcceptOwnershipChangeset(t *testing.T) {
t.Parallel()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ci is timing out. parallelize many ccip tests

e := NewMemoryEnvironment(t)
state, err := LoadOnchainState(e.Env)
require.NoError(t, err)
Expand Down
1 change: 1 addition & 0 deletions deployment/ccip/changeset/cs_add_chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
)

func TestAddChainInbound(t *testing.T) {
t.Parallel()
// 4 chains where the 4th is added after initial deployment.
e := NewMemoryEnvironment(t,
WithChains(4),
Expand Down
1 change: 1 addition & 0 deletions deployment/ccip/changeset/cs_add_lane_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
)

func TestAddLanesWithTestRouter(t *testing.T) {
t.Parallel()
e := NewMemoryEnvironment(t)
// Here we have CR + nodes set up, but no CCIP contracts deployed.
state, err := LoadOnchainState(e.Env)
Expand Down
2 changes: 1 addition & 1 deletion deployment/ccip/changeset/cs_ccip_home_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (

func TestActiveCandidate(t *testing.T) {
t.Skipf("to be enabled after latest cl-ccip is compatible")

t.Parallel()
tenv := NewMemoryEnvironment(t,
WithChains(3),
WithNodes(5))
Expand Down
1 change: 1 addition & 0 deletions deployment/ccip/changeset/cs_deploy_chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
)

func TestDeployChainContractsChangeset(t *testing.T) {
t.Parallel()
lggr := logger.TestLogger(t)
e := memory.NewMemoryEnvironment(t, lggr, zapcore.InfoLevel, memory.MemoryEnvironmentConfig{
Bootstraps: 1,
Expand Down
1 change: 1 addition & 0 deletions deployment/ccip/changeset/cs_home_chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
)

func TestDeployHomeChain(t *testing.T) {
t.Parallel()
lggr := logger.TestLogger(t)
e := memory.NewMemoryEnvironment(t, lggr, zapcore.InfoLevel, memory.MemoryEnvironmentConfig{
Bootstraps: 1,
Expand Down
1 change: 1 addition & 0 deletions deployment/ccip/changeset/cs_initial_add_chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
)

func TestInitialAddChainAppliedTwice(t *testing.T) {
t.Parallel()
// This already applies the initial add chain changeset.
e := NewMemoryEnvironment(t)

Expand Down
1 change: 1 addition & 0 deletions deployment/ccip/changeset/cs_jobspec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
)

func TestJobSpecChangeset(t *testing.T) {
t.Parallel()
lggr := logger.TestLogger(t)
e := memory.NewMemoryEnvironment(t, lggr, zapcore.InfoLevel, memory.MemoryEnvironmentConfig{
Chains: 1,
Expand Down
1 change: 1 addition & 0 deletions deployment/ccip/changeset/view_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
)

func TestSmokeView(t *testing.T) {
t.Parallel()
tenv := NewMemoryEnvironment(t, WithChains(3))
_, err := ViewCCIP(tenv.Env)
require.NoError(t, err)
Expand Down
177 changes: 177 additions & 0 deletions deployment/common/changeset/mcms_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package changeset
krehermann marked this conversation as resolved.
Show resolved Hide resolved

import (
"bytes"
"encoding/json"
"fmt"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
owner_helpers "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers"
"github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/mcms"
"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/smartcontractkit/chainlink/deployment"
"github.com/smartcontractkit/chainlink/deployment/common/types"
)

// TimelockExecutionContracts is a helper struct for executing timelock proposals. it contains
// the timelock and call proxy contracts.
type TimelockExecutionContracts struct {
Timelock *owner_helpers.RBACTimelock
CallProxy *owner_helpers.CallProxy
}

// NewTimelockExecutionContracts creates a new TimelockExecutionContracts struct.
krehermann marked this conversation as resolved.
Show resolved Hide resolved
// If there are multiple timelocks or call proxy on the chain, an error is returned.
// If there is a missing timelocks or call proxy on the chain, an error is returned.
func NewTimelockExecutionContracts(env deployment.Environment, chainSelector uint64) (*TimelockExecutionContracts, error) {
addrTypeVer, err := env.ExistingAddresses.AddressesForChain(chainSelector)
if err != nil {
return nil, fmt.Errorf("error getting addresses for chain: %w", err)
}
var timelock *owner_helpers.RBACTimelock
var callProxy *owner_helpers.CallProxy
for addr, tv := range addrTypeVer {
if tv.Type == types.RBACTimelock {
if timelock != nil {
return nil, fmt.Errorf("multiple timelocks found on chain %d", chainSelector)
}
var err error
timelock, err = owner_helpers.NewRBACTimelock(common.HexToAddress(addr), env.Chains[chainSelector].Client)
if err != nil {
return nil, fmt.Errorf("error creating timelock: %w", err)
}
}
if tv.Type == types.CallProxy {
if callProxy != nil {
return nil, fmt.Errorf("multiple call proxies found on chain %d", chainSelector)
}
var err error
callProxy, err = owner_helpers.NewCallProxy(common.HexToAddress(addr), env.Chains[chainSelector].Client)
if err != nil {
return nil, fmt.Errorf("error creating call proxy: %w", err)
}
}
}
if timelock == nil || callProxy == nil {
return nil, fmt.Errorf("missing timelock (%T) or call proxy(%T) on chain %d", timelock == nil, callProxy == nil, chainSelector)
}
return &TimelockExecutionContracts{
Timelock: timelock,
CallProxy: callProxy,
}, nil
}

type RunTimelockExecutorConfig struct {
Executor *mcms.Executor
TimelockContracts *TimelockExecutionContracts
ChainSelector uint64
// BlockStart is optional. It filter the timelock scheduled events.
// If not provided, the executor assumes that the operations have not been executed yet
// executes all the operations for the given chain.
BlockStart *uint64
BlockEnd *uint64
}

func (cfg RunTimelockExecutorConfig) Validate() error {
if cfg.Executor == nil {
krehermann marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("executor is nil")
}
if cfg.TimelockContracts == nil {
return fmt.Errorf("timelock contracts is nil")
}
if cfg.ChainSelector == 0 {
return fmt.Errorf("chain selector is 0")
}
if cfg.BlockStart != nil && cfg.BlockEnd == nil {
if *cfg.BlockStart > *cfg.BlockEnd {
return fmt.Errorf("block start is greater than block end")
}
}
if cfg.BlockStart == nil && cfg.BlockEnd != nil {
return fmt.Errorf("block start must not be nil when block end is not nil")
}

if len(cfg.Executor.Operations[mcms.ChainIdentifier(cfg.ChainSelector)]) == 0 {
return fmt.Errorf("no operations for chain %d", cfg.ChainSelector)
}
return nil
}

// RunTimelockExecutor runs the scheduled operations for the given chain.
// If the block start is not provided, it assumes that the operations have not been scheduled yet
// and executes all the operations for the given chain.
// It is an error if there are no operations for the given chain.
func RunTimelockExecutor(env deployment.Environment, cfg RunTimelockExecutorConfig) error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sigh. It makes me nervous having this all accessible to changesets, as I want to avoid changesets themselves from ever executing this logic. But I guess this is fine for now - I agree, it probably ought to go into MCMS library though. We can move it there later.

Copy link
Contributor Author

@krehermann krehermann Dec 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do either of these make it better:

  • in a real env (with non zero timelock) it doesn't matter b/c the duration must pass while in the timelock
  • moving pkgs out of changeset

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think both help, yeah.

// TODO: This sort of helper probably should move to the MCMS lib.
// Execute all the transactions in the proposal which are for this chain.
if err := cfg.Validate(); err != nil {
return fmt.Errorf("error validating config: %w", err)
}
for _, chainOp := range cfg.Executor.Operations[mcms.ChainIdentifier(cfg.ChainSelector)] {
for idx, op := range cfg.Executor.ChainAgnosticOps {
start := cfg.BlockStart
end := cfg.BlockEnd
if bytes.Equal(op.Data, chainOp.Data) && op.To == chainOp.To {
if start == nil {
opTx, err2 := cfg.Executor.ExecuteOnChain(env.Chains[cfg.ChainSelector].Client, env.Chains[cfg.ChainSelector].DeployerKey, idx)
if err2 != nil {
return fmt.Errorf("error executing on chain: %w", err2)
}
block, err2 := env.Chains[cfg.ChainSelector].Confirm(opTx)
if err2 != nil {
return fmt.Errorf("error confirming on chain: %w", err2)
}
start = &block
end = &block
}

it, err2 := cfg.TimelockContracts.Timelock.FilterCallScheduled(&bind.FilterOpts{
Start: *start,
End: end,
Context: env.GetContext(),
}, nil, nil)
if err2 != nil {
return fmt.Errorf("error filtering call scheduled: %w", err2)
}
var calls []owner_helpers.RBACTimelockCall
var pred, salt [32]byte
for it.Next() {
// Note these are the same for the whole batch, can overwrite
pred = it.Event.Predecessor
salt = it.Event.Salt
verboseDebug(env.Logger, it.Event)
env.Logger.Info("scheduled", "event", it.Event)
calls = append(calls, owner_helpers.RBACTimelockCall{
Target: it.Event.Target,
Data: it.Event.Data,
Value: it.Event.Value,
})
}

timelockExecutorProxy, err := owner_helpers.NewRBACTimelock(cfg.TimelockContracts.CallProxy.Address(), env.Chains[cfg.ChainSelector].Client)
if err != nil {
return fmt.Errorf("error creating timelock executor proxy: %w", err)
}
tx, err := timelockExecutorProxy.ExecuteBatch(
env.Chains[cfg.ChainSelector].DeployerKey, calls, pred, salt)
if err != nil {
return fmt.Errorf("error executing batch: %w", err)
}
_, err = env.Chains[cfg.ChainSelector].Confirm(tx)
if err != nil {
return fmt.Errorf("error confirming batch: %w", err)
}
}
}
}
return nil
}

func verboseDebug(lggr logger.Logger, event *owner_helpers.RBACTimelockCallScheduled) {
b, err := json.Marshal(event)
if err != nil {
panic(err)
}
lggr.Debug("scheduled", "event", string(b))
}
61 changes: 12 additions & 49 deletions deployment/common/changeset/mcms_test_helpers.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
package changeset

import (
"bytes"
"context"
"crypto/ecdsa"
"testing"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/smartcontractkit/ccip-owner-contracts/pkg/config"
owner_helpers "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"
chainsel "github.com/smartcontractkit/chain-selectors"
Expand All @@ -25,13 +21,6 @@ var (
TestXXXMCMSSigner *ecdsa.PrivateKey
)

// TimelockExecutionContracts is a helper struct for executing timelock proposals. it contains
// the timelock and call proxy contracts.
type TimelockExecutionContracts struct {
Timelock *owner_helpers.RBACTimelock
CallProxy *owner_helpers.CallProxy
}

func init() {
key, err := crypto.GenerateKey()
if err != nil {
Expand Down Expand Up @@ -79,45 +68,19 @@ func ExecuteProposal(t *testing.T, env deployment.Environment, executor *mcms.Ex
if err2 != nil {
require.NoError(t, deployment.MaybeDataErr(err2))
}
_, err2 = env.Chains[sel].Confirm(tx)
require.NoError(t, err2)

// TODO: This sort of helper probably should move to the MCMS lib.
// Execute all the transactions in the proposal which are for this chain.
for _, chainOp := range executor.Operations[mcms.ChainIdentifier(sel)] {
for idx, op := range executor.ChainAgnosticOps {
if bytes.Equal(op.Data, chainOp.Data) && op.To == chainOp.To {
opTx, err3 := executor.ExecuteOnChain(env.Chains[sel].Client, env.Chains[sel].DeployerKey, idx)
require.NoError(t, err3)
block, err3 := env.Chains[sel].Confirm(opTx)
require.NoError(t, err3)
t.Log("executed", chainOp)
it, err3 := timelockContracts.Timelock.FilterCallScheduled(&bind.FilterOpts{
Start: block,
End: &block,
Context: context.Background(),
}, nil, nil)
require.NoError(t, err3)
var calls []owner_helpers.RBACTimelockCall
var pred, salt [32]byte
for it.Next() {
// Note these are the same for the whole batch, can overwrite
pred = it.Event.Predecessor
salt = it.Event.Salt
t.Log("scheduled", it.Event)
calls = append(calls, owner_helpers.RBACTimelockCall{
Target: it.Event.Target,
Data: it.Event.Data,
Value: it.Event.Value,
})
}
timelockExecutorProxy, err := owner_helpers.NewRBACTimelock(timelockContracts.CallProxy.Address(), env.Chains[sel].Client)
tx, err := timelockExecutorProxy.ExecuteBatch(
env.Chains[sel].DeployerKey, calls, pred, salt)
require.NoError(t, err)
_, err = env.Chains[sel].Confirm(tx)
require.NoError(t, err)
/*
if env.GetContext == nil {
env.GetContext = func() context.Context {
return tests.Context(t)
}
}
*/
_, err2 = env.Chains[sel].Confirm(tx)
require.NoError(t, err2)
cfg := RunTimelockExecutorConfig{
Executor: executor,
TimelockContracts: timelockContracts,
ChainSelector: sel,
}
require.NoError(t, RunTimelockExecutor(env, cfg))
}
1 change: 1 addition & 0 deletions deployment/common/changeset/test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ func ApplyChangesets(t *testing.T, e deployment.Environment, timelockContractsPe
NodeIDs: e.NodeIDs,
Offchain: e.Offchain,
OCRSecrets: e.OCRSecrets,
GetContext: e.GetContext,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dammit, another one. Shouldn't we be using NewEnvironment here instead of priming the struct anyway? Sigh.

(Not a review, just drive-by frustration)

}
}
return currentEnv, nil
Expand Down
Loading