Skip to content

Commit

Permalink
Solana mcm signers in ConfigSet event [NONEVM-1068] (#393)
Browse files Browse the repository at this point in the history
  • Loading branch information
jadepark-dev authored Dec 23, 2024
1 parent 196ae10 commit f25773d
Show file tree
Hide file tree
Showing 10 changed files with 131 additions and 90 deletions.
2 changes: 1 addition & 1 deletion chains/solana/contracts/programs/mcm/src/constant.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Business-logic constants
pub const MAX_NUM_SIGNERS: usize = 200; // This has to be below u8 limit (255). Value copied from EVM reference contract
pub const MAX_NUM_SIGNERS: usize = 180; // maximum number of signers supported
pub const NUM_GROUPS: usize = 32; // Value copied from EVM reference contract

// fixed size msig name for distinguishing different multisig instances
Expand Down
5 changes: 2 additions & 3 deletions chains/solana/contracts/programs/mcm/src/event.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use anchor_lang::prelude::*;

use crate::constant::*;
use crate::{constant::*, state::config::*};

#[event]
/// @dev Emitted when a new root is set.
Expand All @@ -22,8 +22,7 @@ pub struct ConfigSet {
pub group_parents: [u8; NUM_GROUPS],
pub group_quorums: [u8; NUM_GROUPS],
pub is_root_cleared: bool,
// todo: emitting all signers causes a memory overflow, need to find a way to emit them
// pub signers: Vec<McmSigner>,
pub signers: Vec<McmSigner>,
}

#[event]
Expand Down
164 changes: 85 additions & 79 deletions chains/solana/contracts/programs/mcm/src/instructions/set_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,89 +28,92 @@ pub fn set_config(
signer_addresses.len() == signer_groups.len(),
McmError::MismatchedInputSignerVectorsLength
);

// count the number of children for each group while validating group structure
let mut group_children_counts = signer_groups.iter().try_fold(
[0u8; NUM_GROUPS],
|mut acc, &group| -> Result<[u8; NUM_GROUPS]> {
// make sure the specified signer group is in bound
require!(
(group as usize) < NUM_GROUPS,
McmError::MismatchedInputGroupArraysLength
);
acc[group as usize] = acc[group as usize]
.checked_add(1)
.ok_or(McmError::Overflow)?;

Ok(acc)
},
)?;

const ROOT_GROUP: usize = 0;
// check if the group structure is a tree
for i in (0..NUM_GROUPS).rev() {
// validate group structure in backwards(root is 0)

match i {
// root should have itself as parent
ROOT_GROUP => require!(
group_parents[ROOT_GROUP] == ROOT_GROUP as u8,
McmError::GroupTreeNotWellFormed
),
// make sure the parent group is at a higher level(lower index) than the current group
_ => require!(group_parents[i] < i as u8, McmError::GroupTreeNotWellFormed),
}

let disabled: bool = group_quorums[i] == 0;

match disabled {
true => {
// validate disabled group has no children
{
// count the number of children for each group while validating group structure
let mut group_children_counts = signer_groups.iter().try_fold(
[0u8; NUM_GROUPS],
|mut acc, &group| -> Result<[u8; NUM_GROUPS]> {
// make sure the specified signer group is in bound
require!(
group_children_counts[i] == 0,
McmError::SignerInDisabledGroup
(group as usize) < NUM_GROUPS,
McmError::MismatchedInputGroupArraysLength
);
}
false => {
// ensure the group quorum can be met(i.e. have more signers than the quorum)
require!(
group_children_counts[i] >= group_quorums[i],
McmError::OutOfBoundsGroupQuorum
);

// increase the parent group's children count
let parent_index = group_parents[i] as usize;
group_children_counts[parent_index] = group_children_counts[parent_index]
acc[group as usize] = acc[group as usize]
.checked_add(1)
.ok_or(McmError::Overflow)?;

Ok(acc)
},
)?;

const ROOT_GROUP: usize = 0;
// check if the group structure is a tree
for i in (0..NUM_GROUPS).rev() {
// validate group structure in backwards(root is 0)

match i {
// root should have itself as parent
ROOT_GROUP => require!(
group_parents[ROOT_GROUP] == ROOT_GROUP as u8,
McmError::GroupTreeNotWellFormed
),
// make sure the parent group is at a higher level(lower index) than the current group
_ => require!(group_parents[i] < i as u8, McmError::GroupTreeNotWellFormed),
}

let disabled: bool = group_quorums[i] == 0;

match disabled {
true => {
// validate disabled group has no children
require!(
group_children_counts[i] == 0,
McmError::SignerInDisabledGroup
);
}
false => {
// ensure the group quorum can be met(i.e. have more signers than the quorum)
require!(
group_children_counts[i] >= group_quorums[i],
McmError::OutOfBoundsGroupQuorum
);

// increase the parent group's children count
let parent_index = group_parents[i] as usize;
group_children_counts[parent_index] = group_children_counts[parent_index]
.checked_add(1)
.ok_or(McmError::Overflow)?;
}
}
}
}

let config = &mut ctx.accounts.multisig_config;
let mut signers: Vec<McmSigner> = Vec::with_capacity(signer_addresses.len());
let mut prev_signer = [0u8; EVM_ADDRESS_BYTES];
config.group_quorums = group_quorums;
config.group_parents = group_parents;

for (index, &evm_addr) in signer_addresses.iter().enumerate() {
require!(
evm_addr > prev_signer,
McmError::SignersAddressesMustBeStrictlyIncreasing
);
{
let mut signers: Vec<McmSigner> = Vec::with_capacity(signer_addresses.len());
let mut prev_signer = [0u8; EVM_ADDRESS_BYTES];

for (index, &evm_addr) in signer_addresses.iter().enumerate() {
require!(
evm_addr > prev_signer,
McmError::SignersAddressesMustBeStrictlyIncreasing
);

// update prev signer
prev_signer = evm_addr;
// update prev signer
prev_signer = evm_addr;

signers.push(McmSigner {
evm_address: evm_addr,
index: u8::try_from(index).unwrap(), // This is safe due to previous check on signer_addresses length
group: signer_groups[index],
})
signers.push(McmSigner {
evm_address: evm_addr,
index: u8::try_from(index).unwrap(), // This is safe due to previous check on signer_addresses length
group: signer_groups[index],
})
}
config.signers = signers;
}

config.signers = signers;
config.group_quorums = group_quorums;
config.group_parents = group_parents;

// clear_root is equivalent to overriding with a completely empty root
if clear_root {
let expiring_root = &mut ctx.accounts.expiring_root_and_op_count;
Expand All @@ -120,24 +123,27 @@ pub fn set_config(
let current_op_count = expiring_root.op_count;

// clear the expiring root while preserving op_count
expiring_root.root = [0u8; 32]; // clear root (equivalent to bytes32(0) in Solidity)
expiring_root.valid_until = 0; // clear timestamp
expiring_root.op_count = current_op_count;
expiring_root.set_inner(ExpiringRootAndOpCount {
root: [0u8; 32], // clear root (equivalent to bytes32(0) in Solidity)
valid_until: 0, // clear timestamp
op_count: current_op_count,
});

// set root metadata to a cleared state
root_metadata.chain_id = ctx.accounts.multisig_config.chain_id;
root_metadata.multisig = ctx.accounts.multisig_config.key();
root_metadata.pre_op_count = current_op_count;
root_metadata.post_op_count = current_op_count;
root_metadata.override_previous_root = true;
root_metadata.set_inner(RootMetadata {
chain_id: config.chain_id,
multisig: config.key(),
pre_op_count: current_op_count,
post_op_count: current_op_count,
override_previous_root: true,
});
}

emit!(ConfigSet {
group_parents,
group_quorums,
is_root_cleared: clear_root,
// todo: memory inefficient, finding workaround
// signers: config.signers.clone(),
signers: config.signers.clone(),
});

Ok(())
Expand Down
9 changes: 9 additions & 0 deletions chains/solana/contracts/target/idl/mcm.json
Original file line number Diff line number Diff line change
Expand Up @@ -1102,6 +1102,15 @@
"name": "isRootCleared",
"type": "bool",
"index": false
},
{
"name": "signers",
"type": {
"vec": {
"defined": "McmSigner"
}
},
"index": false
}
]
},
Expand Down
14 changes: 9 additions & 5 deletions chains/solana/contracts/tests/config/mcm_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@ import (

var (
McmProgram = solana.MustPublicKeyFromBase58("6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX")

// For testing CPIs made by other programs (with actual business logic).
ExternalCpiStubProgram = solana.MustPublicKeyFromBase58("4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ")
StubAccountPDA, _, _ = solana.FindProgramAddress([][]byte{[]byte("u8_value")}, ExternalCpiStubProgram)

// todo: update chain id following the latest discussion(genesis hash)
// Last 8 bytes (uint64) of keccak256("solana:localnet") as big-endian
// This is 0x4808e31713a26612 --> in little-endian, it is "1266a21317e30848"
// ChainID Configuration
// --------------------
// Note: This is an arbitrary value used only for localnet testing
// Value (0x4808e31713a26612) derived from keccak256("solana:localnet")
//
// Note: CCIP chain-selector uses genesis hash of each Solana network
// (mainnet-beta, devnet, testnet) to determine their chain IDs.
// See: chain-selector specification
TestChainID uint64 = 5190648258797659666
TestChainIDPaddedBuffer = [32]byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x66, 0xa2, 0x13, 0x17, 0xe3, 0x08, 0x48,
Expand All @@ -23,7 +27,7 @@ var (
TestMsigNamePaddedBuffer = [32]byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x74, 0x65, 0x73, 0x74, 0x2d, 0x6d, 0x63, 0x6d,
}
MaxNumSigners = 200
MaxNumSigners = 180
MaxAppendSignerBatchSize = 45
MaxAppendSignatureBatchSize = 13

Expand Down
11 changes: 11 additions & 0 deletions chains/solana/contracts/tests/mcms/mcm_set_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,12 @@ func TestMcmSetConfig(t *testing.T) {
require.Equal(t, mcmConfig.GroupQuorums, event.GroupQuorums)
require.Equal(t, mcmConfig.ClearRoot, event.IsRootCleared)

for i, signer := range event.Signers {
require.Equal(t, mcmConfig.SignerAddresses[i], signer.EvmAddress)
require.Equal(t, uint8(i), signer.Index)
require.Equal(t, mcmConfig.SignerGroups[i], signer.Group)
}

// get config and validate
var configAccount mcm.MultisigConfig
err = common.GetAccountDataBorshInto(ctx, solanaGoClient, multisigConfigPDA, config.DefaultCommitment, &configAccount)
Expand Down Expand Up @@ -417,6 +423,11 @@ func TestMcmSetConfig(t *testing.T) {
require.Equal(t, mcmConfig.GroupParents, event.GroupParents)
require.Equal(t, mcmConfig.GroupQuorums, event.GroupQuorums)
require.Equal(t, mcmConfig.ClearRoot, event.IsRootCleared)
for i, signer := range event.Signers {
require.Equal(t, mcmConfig.SignerAddresses[i], signer.EvmAddress)
require.Equal(t, uint8(i), signer.Index)
require.Equal(t, mcmConfig.SignerGroups[i], signer.Group)
}

// get config and validate
var configAccount mcm.MultisigConfig
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,11 @@ func TestMcmSetRootAndExecute(t *testing.T) {
require.Equal(t, newMcmConfig.GroupParents, event.GroupParents)
require.Equal(t, newMcmConfig.GroupQuorums, event.GroupQuorums)
require.Equal(t, true, event.IsRootCleared)
for i, signer := range event.Signers {
require.Equal(t, newMcmConfig.SignerAddresses[i], signer.EvmAddress)
require.Equal(t, uint8(i), signer.Index)
require.Equal(t, newMcmConfig.SignerGroups[i], signer.Group)
}

// get config and validate
var configAccount mcm.MultisigConfig
Expand Down
5 changes: 5 additions & 0 deletions chains/solana/contracts/tests/mcms/mcm_timelock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,11 @@ func TestMcmWithTimelock(t *testing.T) {
require.Equal(t, msig.RawConfig.GroupParents, event.GroupParents)
require.Equal(t, msig.RawConfig.GroupQuorums, event.GroupQuorums)
require.Equal(t, msig.RawConfig.ClearRoot, event.IsRootCleared)
for i, signer := range event.Signers {
require.Equal(t, msig.RawConfig.SignerAddresses[i], signer.EvmAddress)
require.Equal(t, uint8(i), signer.Index)
require.Equal(t, msig.RawConfig.SignerGroups[i], signer.Group)
}

// get config and validate
var configAccount mcm.MultisigConfig
Expand Down
4 changes: 3 additions & 1 deletion chains/solana/utils/mcms/mcm_events.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package mcms

import (
"github.com/gagliardetto/solana-go"

"github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/mcm"
)

// Events - temporary event struct to decode
Expand All @@ -25,10 +27,10 @@ const numGroups = 32

// ConfigSet represents an event emitted when a new config is set
type ConfigSet struct {
// Note: Rust comment indicates signers are omitted due to memory overflow
GroupParents [numGroups]byte // group_parents
GroupQuorums [numGroups]byte // group_quorums
IsRootCleared bool // is_root_cleared
Signers []mcm.McmSigner // data: Vec<u8>
}

// OpExecuted represents an event emitted when an op is successfully executed
Expand Down
2 changes: 1 addition & 1 deletion chains/solana/utils/mcms/merkle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/config" // todo: make utils pure
"github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/config"
)

func TestMcmMerkle(t *testing.T) {
Expand Down

0 comments on commit f25773d

Please sign in to comment.