Skip to content
Open
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
5 changes: 5 additions & 0 deletions crates/contract/src/foreign_chain_rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ impl AllowedProviders {
.map(|(c, e)| (*c, e.clone().into()))
.collect()
}

/// Whether `chain` is currently whitelisted (has a voted-in `ChainEntry`).
pub fn is_whitelisted(&self, chain: &ForeignChain) -> bool {
self.entries.contains_key(chain)
}
}

#[near(serializers=[borsh])]
Expand Down
415 changes: 415 additions & 0 deletions crates/contract/src/lib.rs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,23 @@ BorshSchemaContainer {
],
),
},
"AvailableForeignChains": Struct {
fields: UnnamedFields(
[
"BTreeSet<ForeignChain>",
],
),
},
"AvailableForeignChainsByNode": Struct {
fields: NamedFields(
[
(
"available_foreign_chain_by_node",
"IterableMap",
),
],
),
},
"BTreeMap<AuthenticatedAccountId, ThresholdParameters>": Sequence {
length_width: 4,
length_range: 0..=4294967295,
Expand Down Expand Up @@ -181,6 +198,11 @@ BorshSchemaContainer {
length_range: 0..=4294967295,
elements: "AuthenticatedParticipantId",
},
"BTreeSet<ForeignChain>": Sequence {
length_width: 4,
length_range: 0..=4294967295,
elements: "ForeignChain",
},
"Bls12381G2PublicKey": Struct {
fields: UnnamedFields(
[
Expand Down Expand Up @@ -375,6 +397,61 @@ BorshSchemaContainer {
],
),
},
"ForeignChain": Enum {
tag_width: 1,
variants: [
(
0,
"Solana",
"ForeignChain__Solana",
),
(
1,
"Bitcoin",
"ForeignChain__Bitcoin",
),
(
2,
"Ethereum",
"ForeignChain__Ethereum",
),
(
3,
"Base",
"ForeignChain__Base",
),
(
4,
"Bnb",
"ForeignChain__Bnb",
),
(
5,
"Arbitrum",
"ForeignChain__Arbitrum",
),
(
6,
"Abstract",
"ForeignChain__Abstract",
),
(
7,
"Starknet",
"ForeignChain__Starknet",
),
(
8,
"Polygon",
"ForeignChain__Polygon",
),
(
9,
"HyperEvm",
"ForeignChain__HyperEvm",
),
],
},
"ForeignChainRpcWhitelist": Struct {
fields: NamedFields(
[
Expand All @@ -389,6 +466,36 @@ BorshSchemaContainer {
],
),
},
"ForeignChain__Abstract": Struct {
fields: Empty,
},
"ForeignChain__Arbitrum": Struct {
fields: Empty,
},
"ForeignChain__Base": Struct {
fields: Empty,
},
"ForeignChain__Bitcoin": Struct {
fields: Empty,
},
"ForeignChain__Bnb": Struct {
fields: Empty,
},
"ForeignChain__Ethereum": Struct {
fields: Empty,
},
"ForeignChain__HyperEvm": Struct {
fields: Empty,
},
"ForeignChain__Polygon": Struct {
fields: Empty,
},
"ForeignChain__Solana": Struct {
fields: Empty,
},
"ForeignChain__Starknet": Struct {
fields: Empty,
},
"HashSet<AuthenticatedAccountId>": Sequence {
length_width: 4,
length_range: 0..=4294967295,
Expand Down Expand Up @@ -702,6 +809,14 @@ BorshSchemaContainer {
"foreign_chain_rpc_whitelist",
"ForeignChainRpcWhitelist",
),
(
"available_foreign_chains_by_node",
"AvailableForeignChainsByNode",
),
(
"available_foreign_chains",
"AvailableForeignChains",
),
],
),
},
Expand Down
1 change: 1 addition & 0 deletions crates/contract/src/storage_keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ pub enum StorageKey {
AllowedForeignChainProvidersV1,
ForeignChainProviderVotesByVoterV1,
ForeignChainProviderVotesByProposalV1,
AvailableForeignChainsByNode,
}
4 changes: 4 additions & 0 deletions crates/contract/src/v3_11_2_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ impl From<MpcContract> for crate::MpcContract {
node_migrations: old.node_migrations,
metrics: old.metrics,
foreign_chain_rpc_whitelist: old.foreign_chain_rpc_whitelist,
// New in this revision; self-populates on the first
// `register_available_foreign_chain_config` after upgrade.
available_foreign_chains_by_node: Default::default(),
available_foreign_chains: Default::default(),
}
}
}
55 changes: 55 additions & 0 deletions crates/contract/tests/snapshots/abi__abi_has_not_changed.snap
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,31 @@ expression: abi
}
}
},
{
"name": "get_available_foreign_chain_by_node",
"doc": " Per-participant view of which foreign chains each node currently covers. Feeds the\n available-set computation ([`Self::get_available_foreign_chains`]) and coverage alerting.",
"kind": "view",
"result": {
"serialization_type": "json",
"type_schema": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/AvailableForeignChains"
}
}
}
},
{
"name": "get_available_foreign_chains",
"doc": " The **available** foreign chains: whitelisted chains that are supported\n by at least the signing threshold of active participants.",
"kind": "view",
"result": {
"serialization_type": "json",
"type_schema": {
"$ref": "#/definitions/AvailableForeignChains"
}
}
},
{
"name": "get_foreign_chain_support_by_node",
"kind": "view",
Expand Down Expand Up @@ -1081,6 +1106,28 @@ expression: abi
}
}
},
{
"name": "register_available_foreign_chain_config",
"doc": " (Re)registers the foreign chains this participant currently covers, feeding the available\n set ([`Self::get_available_foreign_chains`]). Idempotent.\n\n Note: `voter_or_panic` accepts callers in `Initializing` and `Resharing` phases, so a\n registration in those phases will be written to `available_foreign_chains_by_node` but\n the cache (`available_foreign_chains`) is not refreshed until the next `Running`-state\n recompute (e.g. `clean_foreign_chain_data` after `vote_reshared`, or the next call to\n this method once the contract is `Running`).",
"kind": "call",
"params": {
"serialization_type": "json",
"args": [
{
"name": "available_foreign_chains",
"type_schema": {
"$ref": "#/definitions/AvailableForeignChains"
}
}
]
},
"result": {
"serialization_type": "json",
"type_schema": {
"type": "null"
}
}
},
{
"name": "register_backup_service",
"doc": " Registers or updates the backup service information for the caller account.\n\n The caller (`signer_account_id`) must be an existing or prospective participant.\n Otherwise, the transaction will fail.\n\n # Notes\n - A deposit requirement may be added in the future.",
Expand Down Expand Up @@ -2244,6 +2291,14 @@ expression: abi
}
]
},
"AvailableForeignChains": {
"description": "The set of foreign chains a node reports it can currently serve (cover). The available-set analog of [`SupportedForeignChains`]; submitted by nodes via `register_available_foreign_chain_config`.",
"type": "array",
"items": {
"$ref": "#/definitions/ForeignChain"
},
"uniqueItems": true
},
"BackupServiceInfo": {
"type": "object",
"required": [
Expand Down
12 changes: 12 additions & 0 deletions crates/e2e-tests/src/cluster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,18 @@ impl MpcCluster {
.await
}

/// View the per-node available-foreign-chain registrations on the contract (the set each
/// node reported via `register_available_foreign_chain_config`).
pub async fn view_available_foreign_chain_by_node(
&self,
) -> anyhow::Result<
HashMap<ContractAccountId, near_mpc_contract_interface::types::AvailableForeignChains>,
> {
self.contract
.view(method_names::GET_AVAILABLE_FOREIGN_CHAIN_BY_NODE)
.await
}

/// Register foreign chain support on the contract for a specific node.
pub async fn register_foreign_chain_config(
&self,
Expand Down
1 change: 1 addition & 0 deletions crates/e2e-tests/tests/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub const CONTRACT_UPGRADE_COMPATIBILITY_MAINNET_PORT_SEED: u16 = 18;
pub const CONTRACT_UPGRADE_COMPATIBILITY_TESTNET_PORT_SEED: u16 = 19;
pub const TIMEOUT_METRIC_PORT_SEED: u16 = 20;
pub const MIGRATION_BACK_PORT_SEED: u16 = 21;
pub const AVAILABLE_FOREIGN_CHAINS_BY_NODE_PORT_SEED: u16 = 22;

/// Start a cluster, wait for Running state and presignatures to buffer.
///
Expand Down
63 changes: 62 additions & 1 deletion crates/e2e-tests/tests/foreign_chain_configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ use backon::{ConstantBuilder, Retryable};
use e2e_tests::CLUSTER_WAIT_TIMEOUT;
use mpc_node_config::{ForeignChainConfig, ForeignChainProviderConfig, ForeignChainsConfig};
use near_mpc_bounded_collections::NonEmptyBTreeMap;
use near_mpc_contract_interface::types::{ForeignChain, SupportedForeignChains};
use near_mpc_contract_interface::types::{
AvailableForeignChains, ForeignChain, SupportedForeignChains,
};

const SOLANA_PROVIDER_NAME: &str = "public";
const SOLANA_RPC_URL: &str = "https://rpc.public.example.com";
Expand Down Expand Up @@ -130,3 +132,62 @@ async fn supported_foreign_chains__should_require_all_participants_to_register()
.await
.expect("timed out waiting for Solana to be reported as supported");
}

/// Each node auto-registers the chains it covers via `register_available_foreign_chain_config`
/// on startup; `get_available_foreign_chain_by_node` should reflect those per-node sets.
///
/// 3-node cluster: nodes 0 and 1 are configured with Solana, node 2 has none.
#[tokio::test]
#[expect(non_snake_case)]
async fn available_foreign_chain_by_node__should_reflect_auto_registered_configs() {
// given — 3-node cluster with Solana on nodes 0 and 1 only
let (cluster, _running) =
common::must_setup_cluster(common::AVAILABLE_FOREIGN_CHAINS_BY_NODE_PORT_SEED, |c| {
c.node_foreign_chains_configs = vec![
solana_foreign_chains_config(), // node 0
solana_foreign_chains_config(), // node 1
ForeignChainsConfig::default(), // node 2 — no foreign chains
];
})
.await;

// then — every node auto-registers (node 2 with an empty set), and the per-node available
// view reflects it: nodes 0 and 1 cover exactly Solana, node 2 is empty.
let expected_node_available: AvailableForeignChains =
BTreeSet::from([ForeignChain::Solana]).into();
(|| async {
let by_node = cluster
.view_available_foreign_chain_by_node()
.await
.expect("failed to view available foreign chains by node");

anyhow::ensure!(
by_node.len() == 3,
"expected exactly 3 registrations, got {}",
by_node.len()
);
let covering_solana = by_node
.values()
.filter(|chains| **chains == expected_node_available)
.count();
anyhow::ensure!(
covering_solana == 2,
"expected exactly 2 nodes covering Solana, got {covering_solana}"
);
let empty = by_node.values().filter(|chains| chains.is_empty()).count();
anyhow::ensure!(
empty == 1,
"expected exactly 1 empty registration (node 2), got {empty}"
);
Ok(())
})
.retry(
ConstantBuilder::default()
.with_delay(common::POLL_INTERVAL)
.with_max_times(
(CLUSTER_WAIT_TIMEOUT.as_millis() / common::POLL_INTERVAL.as_millis()) as usize,
),
)
.await
.expect("timed out waiting for available-foreign-chain registrations");
}
3 changes: 3 additions & 0 deletions crates/near-mpc-contract-interface/src/method_names.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub const VOTE_RESHARED: &str = "vote_reshared";
pub const VOTE_NEW_PARAMETERS: &str = "vote_new_parameters";
pub const VOTE_ADD_DOMAINS: &str = "vote_add_domains";
pub const REGISTER_FOREIGN_CHAIN_SUPPORT: &str = "register_foreign_chain_support";
pub const REGISTER_AVAILABLE_FOREIGN_CHAIN_CONFIG: &str = "register_available_foreign_chain_config";
pub const VOTE_CODE_HASH: &str = "vote_code_hash";
pub const VOTE_ADD_LAUNCHER_HASH: &str = "vote_add_launcher_hash";
pub const VOTE_REMOVE_LAUNCHER_HASH: &str = "vote_remove_launcher_hash";
Expand Down Expand Up @@ -75,6 +76,8 @@ pub const GET_TEE_ACCOUNTS: &str = "get_tee_accounts";
pub const GET_ATTESTATION: &str = "get_attestation";
pub const GET_SUPPORTED_FOREIGN_CHAINS: &str = "get_supported_foreign_chains";
pub const GET_FOREIGN_CHAIN_SUPPORT_BY_NODE: &str = "get_foreign_chain_support_by_node";
pub const GET_AVAILABLE_FOREIGN_CHAINS: &str = "get_available_foreign_chains";
pub const GET_AVAILABLE_FOREIGN_CHAIN_BY_NODE: &str = "get_available_foreign_chain_by_node";
pub const ALLOWED_FOREIGN_CHAIN_PROVIDERS: &str = "allowed_foreign_chain_providers";
pub const ALLOWED_DOCKER_IMAGE_HASHES: &str = "allowed_docker_image_hashes";
pub const ALLOWED_LAUNCHER_COMPOSE_HASHES: &str = "allowed_launcher_compose_hashes";
Expand Down
Loading
Loading