Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@
return b.builder
}

// AddAdditionalMCM appends an additional MCM instance to the chain metadata of the given
// selector. Requires the chain metadata to already exist (set via AddChainMetadata).
// Multi-MCM proposals must use version v2 (SetVersion("v2")).
func (b *BaseProposalBuilder[T]) AddAdditionalMCM(selector types.ChainSelector, metadata types.ChainMetadata) T {
entry := b.baseProposal.ChainMetadata[selector]
entry.AdditionalMCMs = append(entry.AdditionalMCMs, metadata)
b.baseProposal.ChainMetadata[selector] = entry
Comment on lines +54 to +56
return b.builder

Check failure on line 57 in builder.go

View workflow job for this annotation

GitHub Actions / Lint E2E tests

return with no blank line before (nlreturn)

Check failure on line 57 in builder.go

View workflow job for this annotation

GitHub Actions / Lint

return with no blank line before (nlreturn)
}

// SetChainMetadata sets the chain metadata of the BaseProposal.
func (b *BaseProposalBuilder[T]) SetChainMetadata(metadata map[types.ChainSelector]types.ChainMetadata) T {
b.baseProposal.ChainMetadata = metadata
Expand Down
115 changes: 100 additions & 15 deletions executable.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ import (
// information required to call SetRoot and Execute on the various chains that the proposal
// targets.
type Executable struct {
proposal *Proposal
executors map[types.ChainSelector]sdk.Executor
encoders map[types.ChainSelector]sdk.Encoder
tree *merkle.Tree
txNonces []uint64
proposal *Proposal
executors map[types.ChainSelector]sdk.Executor
encoders map[types.ChainSelector]sdk.Encoder
instanceEncoders map[instanceKey]sdk.Encoder
tree *merkle.Tree
txNonces []uint64
}

// NewExecutable creates a new Executable from a proposal and a map of executors.
Expand All @@ -33,6 +34,12 @@ func NewExecutable(
return nil, err
}

// Generate the per-instance encoders (used for root metadata hashing)
instanceEncoders, err := proposal.GetInstanceEncoders()
if err != nil {
return nil, err
}

// Generate the tx nonces from the proposal
txNonces, err := proposal.TransactionNonces()
if err != nil {
Expand All @@ -46,18 +53,61 @@ func NewExecutable(
}

return &Executable{
proposal: proposal,
executors: executors,
encoders: encoders,
tree: tree,
txNonces: txNonces,
proposal: proposal,
executors: executors,
encoders: encoders,
instanceEncoders: instanceEncoders,
tree: tree,
txNonces: txNonces,
}, nil
}

// MCMAddresses returns the MCM instance addresses for a chain selector: the primary MCM
// followed by any additional MCM instances. Callers should call SetRoot once per address.
func (e *Executable) MCMAddresses(chainSelector types.ChainSelector) []string {
metadata, ok := e.proposal.ChainMetadata[chainSelector]
if !ok {
return nil
}

instances := metadata.AllMCMs()
addresses := make([]string, 0, len(instances))
for _, instance := range instances {
addresses = append(addresses, instance.MCMAddress)
}

return addresses
}

// SetRoot calls SetRoot on the chain's primary MCM instance. For chains with multiple
// MCM instances, use SetRootForMCM to target a specific instance.
func (e *Executable) SetRoot(ctx context.Context, chainSelector types.ChainSelector) (types.TransactionResult, error) {
metadata := e.proposal.ChainMetadata[chainSelector]
return e.SetRootForMCM(ctx, chainSelector, "")
}

metadataHash, err := e.encoders[chainSelector].HashMetadata(metadata)
// SetRootForMCM calls SetRoot on the MCM instance identified by mcmAddress. An empty
// mcmAddress targets the chain's primary MCM instance.
func (e *Executable) SetRootForMCM(
ctx context.Context, chainSelector types.ChainSelector, mcmAddress string,
) (types.TransactionResult, error) {
metadata, ok := e.proposal.ChainMetadata[chainSelector]
if !ok {
return types.TransactionResult{}, NewChainMetadataNotFoundError(chainSelector)
}

instanceMetadata, ok := metadata.GetMCM(mcmAddress)
if !ok {
return types.TransactionResult{}, fmt.Errorf(
"chain %d: mcmAddress %q does not match the chain's primary MCM or any additional MCM instance",
chainSelector, mcmAddress)
}

metadata = instanceMetadata

// Use the per-instance encoder so the metadata leaf hashes this instance's own
// postOpCount (StartingOpCount + instance op count).
metadataHash, err := e.instanceEncoders[instanceKey{chainSelector: chainSelector, mcmAddress: metadata.MCMAddress}].
HashMetadata(metadata)
if err != nil {
return types.TransactionResult{}, err
}
Expand All @@ -81,11 +131,42 @@ func (e *Executable) SetRoot(ctx context.Context, chainSelector types.ChainSelec
return recoveredSignerA.Cmp(recoveredSignerB)
})

return e.executors[chainSelector].SetRoot(
root := [32]byte(e.tree.Root.Bytes())
executor := e.executors[chainSelector]

// For chains with multiple MCM instances, the on-chain root metadata must carry the
// instance's own postOpCount (matching the metadata leaf hashed above). Executors
// derive postOpCount from their chain-wide tx count by default, so they must
// implement sdk.InstanceExecutor to support multi-instance set-root.
if len(e.proposal.ChainMetadata[chainSelector].AdditionalMCMs) > 0 {
instanceExecutor, ok := executor.(sdk.InstanceExecutor)
if !ok {
return types.TransactionResult{}, fmt.Errorf(
"chain %d: executor %T does not support multiple MCM instances (sdk.InstanceExecutor)",
chainSelector, executor)
}

instanceOpCount := e.proposal.TransactionCountsByInstance()[instanceKey{
chainSelector: chainSelector,
mcmAddress: metadata.MCMAddress,
}]

return instanceExecutor.SetRootForInstance(
ctx,
metadata,
instanceOpCount,
proof,
root,
e.proposal.ValidUntil,
sortedSignatures,
)
}

return executor.SetRoot(
ctx,
metadata,
proof,
[32]byte(e.tree.Root.Bytes()),
root,
e.proposal.ValidUntil,
sortedSignatures,
)
Expand All @@ -94,7 +175,11 @@ func (e *Executable) SetRoot(ctx context.Context, chainSelector types.ChainSelec
func (e *Executable) Execute(ctx context.Context, index int) (types.TransactionResult, error) {
op := e.proposal.Operations[index]
chainSelector := op.ChainSelector
metadata := e.proposal.ChainMetadata[chainSelector]

metadata, err := e.proposal.mcmMetadataForOp(op)
if err != nil {
return types.TransactionResult{}, err
}

txNonce, err := safecast.Uint64ToUint32(e.txNonces[index])
if err != nil {
Expand Down
141 changes: 141 additions & 0 deletions executable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -732,3 +732,144 @@
})
}
}

// TestExecutor_MultiMCM_SetRootForMCM_And_Execute covers a v2 proposal spanning two MCM
// instances on the same chain: one signing ceremony, one global Merkle root, one
// SetRootForMCM per instance, and per-instance execution with per-instance nonces.
func TestExecutor_MultiMCM_SetRootForMCM_And_Execute(t *testing.T) {
t.Parallel()

ctx := context.Background()
sim := evmsim.NewSimulatedChain(t, 1)

// Deploy two MCM instances on the same chain, both configured with the same signers
mcmPrimary, _ := sim.DeployMCMContract(t, sim.Signers[0])
mcmSecond, _ := sim.DeployMCMContract(t, sim.Signers[0])
sim.SetMCMSConfig(t, sim.Signers[0], mcmPrimary)
sim.SetMCMSConfig(t, sim.Signers[0], mcmSecond)

// Each instance administers its own timelock so both grantRole ops can succeed
timelockPrimary, _ := sim.DeployRBACTimelock(t, sim.Signers[0], mcmPrimary.Address(), []common.Address{}, []common.Address{}, []common.Address{}, []common.Address{})
timelockSecond, _ := sim.DeployRBACTimelock(t, sim.Signers[0], mcmSecond.Address(), []common.Address{}, []common.Address{}, []common.Address{}, []common.Address{})

role, err := timelockPrimary.PROPOSERROLE(&bind.CallOpts{})
require.NoError(t, err)
timelockAbi, err := bindings.RBACTimelockMetaData.GetAbi()
require.NoError(t, err)
grantPrimaryData, err := timelockAbi.Pack("grantRole", role, mcmPrimary.Address())
require.NoError(t, err)
grantSecondData, err := timelockAbi.Pack("grantRole", role, mcmSecond.Address())
require.NoError(t, err)

// One v2 proposal covering both instances: op0 is governed by the primary instance
// (no mcmAddress), op1 by the second instance.
proposal := Proposal{
BaseProposal: BaseProposal{
Version: "v2",
Description: "Grants RBACTimelock 'Proposer' role via two MCM instances",
Kind: types.KindProposal,
ValidUntil: 2004259681,
Signatures: []types.Signature{},
OverridePreviousRoot: false,
ChainMetadata: map[types.ChainSelector]types.ChainMetadata{
chaintest.Chain1Selector: {
StartingOpCount: 0,
MCMAddress: mcmPrimary.Address().Hex(),
AdditionalMCMs: []types.ChainMetadata{
{StartingOpCount: 0, MCMAddress: mcmSecond.Address().Hex()},
},
},
},
},
Operations: []types.Operation{
{
ChainSelector: chaintest.Chain1Selector,
Transaction: evm.NewTransaction(
timelockPrimary.Address(),
grantPrimaryData,
big.NewInt(0),
"RBACTimelock",
[]string{"RBACTimelock", "GrantRole"},
),
},
{
ChainSelector: chaintest.Chain1Selector,
McmAddress: mcmSecond.Address().Hex(),
Transaction: evm.NewTransaction(
timelockSecond.Address(),
grantSecondData,
big.NewInt(0),
"RBACTimelock",
[]string{"RBACTimelock", "GrantRole"},
),
},
},
}
proposal.UseSimulatedBackend(true)

require.NoError(t, proposal.Validate())

tree, err := proposal.MerkleTree()
require.NoError(t, err)

inspectors := map[types.ChainSelector]sdk.Inspector{
chaintest.Chain1Selector: evm.NewInspector(sim.Backend.Client()),
}
signable, err := NewSignable(&proposal, inspectors)
require.NoError(t, err)

_, err = signable.SignAndAppend(NewPrivateKeySigner(sim.Signers[0].PrivateKey))
require.NoError(t, err)

// Quorum must hold on every instance
quorumMet, err := signable.ValidateSignatures(ctx)
require.NoError(t, err)
require.True(t, quorumMet)

encoders, err := proposal.GetEncoders()
require.NoError(t, err)
executors := map[types.ChainSelector]sdk.Executor{
chaintest.Chain1Selector: evm.NewExecutor(
encoders[chaintest.Chain1Selector].(*evm.Encoder),
sim.Backend.Client(),
sim.Signers[0].NewTransactOpts(t),
),
}
executable, err := NewExecutable(&proposal, executors)
require.NoError(t, err)

// Set the same global root on both instances; each call carries only that instance's
// metadata proof and per-instance postOpCount.
for _, mcmAddress := range executable.MCMAddresses(chaintest.Chain1Selector) {
tx, err := executable.SetRootForMCM(ctx, chaintest.Chain1Selector, mcmAddress)

Check failure on line 844 in executable_test.go

View workflow job for this annotation

GitHub Actions / Lint E2E tests

shadow: declaration of "err" shadows declaration at line 755 (govet)

Check failure on line 844 in executable_test.go

View workflow job for this annotation

GitHub Actions / Lint

shadow: declaration of "err" shadows declaration at line 755 (govet)
require.NoError(t, err)
require.NotEmpty(t, tx.Hash)
sim.Backend.Commit()
}

for _, mcmC := range []*bindings.ManyChainMultiSig{mcmPrimary, mcmSecond} {
root, err := mcmC.GetRoot(&bind.CallOpts{})

Check failure on line 851 in executable_test.go

View workflow job for this annotation

GitHub Actions / Lint E2E tests

shadow: declaration of "err" shadows declaration at line 755 (govet)

Check failure on line 851 in executable_test.go

View workflow job for this annotation

GitHub Actions / Lint

shadow: declaration of "err" shadows declaration at line 755 (govet)
require.NoError(t, err)
require.Equal(t, [32]byte(tree.Root.Bytes()), root.Root)
require.Equal(t, proposal.ValidUntil, root.ValidUntil)
}

// Execute each instance's op; nonces sequence per instance (both are nonce 0).
tx, err := executable.Execute(ctx, 0)
require.NoError(t, err)
require.NotEmpty(t, tx.Hash)
sim.Backend.Commit()

tx, err = executable.Execute(ctx, 1)
require.NoError(t, err)
require.NotEmpty(t, tx.Hash)
sim.Backend.Commit()

opCountPrimary, err := mcmPrimary.GetOpCount(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, uint64(1), opCountPrimary.Uint64())

opCountSecond, err := mcmSecond.GetOpCount(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, uint64(1), opCountSecond.Uint64())
}
Loading
Loading