Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
495 changes: 471 additions & 24 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ envy = { version = "0.4", default-features = false }
vergen-pretty = { version = "1.0", default-features = false, features = ["trace"] }
vergen = { version = "9", default-features = false, features = ["build", "cargo", "rustc", "si"] }
clap = { version = "4.5", features = ["derive", "env"] }
aws-sdk-kms = { version = "1.96", default-features = false }

[workspace.lints.clippy]
all = { level = "deny", priority = -1 }
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
260203.0
260205.0
6 changes: 5 additions & 1 deletion crates/admin_cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ publish = false

[dependencies]
# Local
wcn_cluster = { workspace = true, features = ["evm"] }
wcn_cluster = { workspace = true, features = ["evm", "kms"] }

# Derives
derive_more = { workspace = true, features = ["as_ref"] }
Expand All @@ -21,6 +21,10 @@ futures = { workspace = true }
# Other
clap = { workspace = true }
libp2p-identity = { workspace = true }
aws-sdk-kms = { workspace = true }
aws-config = { version = "1.8", default-features = false }
aws-smithy-http-client = { version = "1.0", default-features = false, features = ["rustls-ring"] }
aws-smithy-async = { version = "1.0", default-features = false, features = ["rt-tokio"] }

[lints]
workspace = true
50 changes: 41 additions & 9 deletions crates/admin_cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use {
anyhow::Context,
clap::{Args, Parser, Subcommand},
clap::{ArgGroup, Args, Parser, Subcommand},
derive_more::AsRef,
std::io::{self, Write as _},
wcn_cluster::NodeOperator,
wcn_cluster::{NodeOperator, smart_contract::evm},
};

mod deploy;
Expand Down Expand Up @@ -49,14 +49,20 @@ enum Command {
}

#[derive(Debug, Args)]
#[command(group(
ArgGroup::new("signer")
.required(true)
.multiple(false)
.args(["private_key", "kms_key_arn"])
))]
struct ClusterArgs {
/// Private key of the WCN Cluster Smart-Contract owner
#[arg(
id = "PRIVATE_KEY",
long = "private-key",
env = "WCN_CLUSTER_SMART_CONTRACT_OWNER_PRIVATE_KEY"
)]
signer: wcn_cluster::smart_contract::evm::Signer,
#[arg(long, env = "WCN_CLUSTER_SMART_CONTRACT_OWNER_PRIVATE_KEY")]
private_key: Option<String>,

/// KMS key id of the WCN Cluster Smart-Contract owner
#[arg(long, env = "WCN_CLUSTER_SMART_CONTRACT_OWNER_KMS_KEY_ARN")]
kms_key_arn: Option<String>,

/// WCN Cluster Smart-Contract encryption key
#[arg(
Expand All @@ -82,8 +88,34 @@ impl ClusterArgs {
encryption_key: self.encryption_key,
};

let signer = if let Some(pk) = self.private_key {
evm::Signer::try_from_private_key(&pk)?
} else if let Some(key_arn) = self.kms_key_arn {
use aws_smithy_http_client::tls;

// Force `aws_sdk` to use `ring` instead of `aws-lc`, otherwise we get a runtime
// conflict in `rustls`.
let client = aws_smithy_http_client::Builder::new()
.tls_provider(tls::Provider::Rustls(
tls::rustls_provider::CryptoMode::Ring,
))
.build_https();

let config = aws_config::defaults(aws_config::BehaviorVersion::latest())
.http_client(client)
.sleep_impl(aws_smithy_async::rt::sleep::TokioSleep::new())
.load()
.await;

let client = aws_sdk_kms::Client::new(&config);
evm::Signer::try_from_kms(client, key_arn).await?
} else {
// `clap` validates that exactly one required argument is present
unreachable!()
};

let connector =
wcn_cluster::smart_contract::evm::RpcProvider::new(self.rpc_provider_url, self.signer)
wcn_cluster::smart_contract::evm::RpcProvider::new(self.rpc_provider_url, signer)
.await
.context("RpcProvider::new")?;

Expand Down
2 changes: 2 additions & 0 deletions crates/cluster/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ publish = false
default = ["evm"]
testing = []
evm = ["dep:alloy"]
kms = ["dep:aws-sdk-kms", "alloy/signer-aws"]

[lints]
workspace = true
Expand Down Expand Up @@ -67,6 +68,7 @@ alloy = { workspace = true, optional = true, features = [
"provider-ws",
"rpc-types",
] }
aws-sdk-kms = { workspace = true, optional = true }

[dev-dependencies]
libp2p-identity = { workspace = true, features = ["rand"] }
Expand Down
34 changes: 29 additions & 5 deletions crates/cluster/src/smart_contract/evm.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! EVM implementation of [`SmartContract`](super::SmartContract).

#[cfg(feature = "kms")]
use alloy::signers::aws::AwsSigner;
use {
super::*,
crate::{migration, node_operator, smart_contract},
Expand Down Expand Up @@ -47,6 +49,8 @@ impl RpcProvider {
pub async fn new(url: RpcUrl, signer: Signer) -> Result<Self, RpcProviderCreationError> {
let wallet: EthereumWallet = match &signer.kind {
SignerKind::PrivateKey(key) => key.clone().into(),
#[cfg(feature = "kms")]
SignerKind::Kms(key) => key.clone().into(),
};

let builder = ProviderBuilder::new().wallet(wallet);
Expand Down Expand Up @@ -679,17 +683,34 @@ pub struct Signer {
}

impl FromStr for Signer {
type Err = InvalidPrivateKeyError;
type Err = InvalidSignerError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_from_private_key(s)
}
}

impl Signer {
pub fn try_from_private_key(hex: &str) -> Result<Self, InvalidPrivateKeyError> {
#[cfg(feature = "kms")]
pub async fn try_from_kms(
kms: aws_sdk_kms::Client,
key_id: String,
) -> Result<Self, InvalidSignerError> {
use alloy::{network::TxSigner, signers::Signature};

let aws_signer = AwsSigner::new(kms, key_id, None)
.await
.map_err(|err| InvalidSignerError(format!("KMS: {err:?}")))?;

Ok(Self {
address: TxSigner::<Signature>::address(&aws_signer).into(),
kind: SignerKind::Kms(aws_signer),
})
}

pub fn try_from_private_key(hex: &str) -> Result<Self, InvalidSignerError> {
let private_key = PrivateKeySigner::from_str(hex)
.map_err(|err| InvalidPrivateKeyError(format!("{err:?}")))?;
.map_err(|err| InvalidSignerError(format!("Private key: {err:?}")))?;

Ok(Self {
address: private_key.address().into(),
Expand All @@ -706,8 +727,11 @@ impl Signer {
#[derive(Clone)]
enum SignerKind {
PrivateKey(PrivateKeySigner),

#[cfg(feature = "kms")]
Kms(AwsSigner),
}

#[derive(Debug, thiserror::Error)]
#[error("Invalid private key: {0:?}")]
pub struct InvalidPrivateKeyError(String);
#[error("Invalid signer: {0:?}")]
pub struct InvalidSignerError(String);
11 changes: 10 additions & 1 deletion env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@ secrets=$(sops -d $1)
smart_contract_address=$(echo $secrets | jq -r '.["smart_contract_address_unencrypted"]')
smart_contract_encryption_key=$(echo $secrets | jq -r '.["smart_contract_encryption_key"]')
ecdsa_private_key=$(echo $secrets | jq -r '.["ecdsa_private_key"]')
kms_key_arn=$(echo $secrets | jq -r '.["kms_key_arn_unencrypted"]')
rpc_provider_url=$(echo $secrets | jq -r '.["rpc_provider_url"]')


if [ "$ecdsa_private_key" != "null" ]; then
export WCN_CLUSTER_SMART_CONTRACT_OWNER_PRIVATE_KEY=$ecdsa_private_key
fi

if [ "$kms_key_arn" != "null" ]; then
export WCN_CLUSTER_SMART_CONTRACT_OWNER_KMS_KEY_ARN=$kms_key_arn
fi

export WCN_CLUSTER_SMART_CONTRACT_ADDRESS=$smart_contract_address
export WCN_CLUSTER_SMART_CONTRACT_ENCRYPTION_KEY=$smart_contract_encryption_key
export WCN_CLUSTER_SMART_CONTRACT_OWNER_PRIVATE_KEY=$ecdsa_private_key
export WCN_NODE_OPERATOR_PRIVATE_KEY=$ecdsa_private_key
Comment thread
xDarksome marked this conversation as resolved.
export OPTIMISM_RPC_PROVIDER_URL=$rpc_provider_url
28 changes: 28 additions & 0 deletions infra/mainnet/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,34 @@ module "sops-encryption-key" {
source = "../modules/sops-encryption-key"
}

module "admin-key-eu" {
source = "../modules/admin-key"
providers = {
aws = aws.eu
}
}

module "admin-key-us" {
source = "../modules/admin-key"
providers = {
aws = aws.us
}
}

module "admin-key-ap" {
source = "../modules/admin-key"
providers = {
aws = aws.ap
}
}

module "admin-key-sa" {
source = "../modules/admin-key"
providers = {
aws = aws.sa
}
}

resource "aws_route53_zone" "this" {
name = "mainnet.walletconnect.network"
}
Expand Down
52 changes: 52 additions & 0 deletions infra/modules/admin-key/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
data "aws_caller_identity" "current" {}

resource "aws_kms_key" "this" {
description = "WCN Cluster Smart-Contract admin (owner) key"
key_usage = "SIGN_VERIFY"
customer_master_key_spec = "ECC_SECG_P256K1"
multi_region = true
}

resource "aws_kms_key_policy" "this" {
key_id = aws_kms_key.this.id

policy = jsonencode({
Version = "2012-10-17"
Id = "key-default-1"
Statement = [
{
Effect = "Allow"
Principal = {
AWS = [
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:root",
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/TerraformCloud"
]
},
Action = "kms:*"
Resource = "*"
},
{
Effect = "Allow"
Principal = {
AWS = "*"
},
actions = [
Comment thread
xDarksome marked this conversation as resolved.
Outdated
"kms:Sign",
"kms:Verify",
"kms:GetPublicKey",
"kms:DescribeKey"
]
Resource = "*",
"Condition" : {
"ArnLike" : {
"aws:PrincipalArn" = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/aws-reserved/sso.amazonaws.com/*AWSReservedSSO_Administrator_*"
}
}
}
]
})
}

output "arn" {
value = aws_kms_key.this.arn
}
2 changes: 1 addition & 1 deletion infra/modules/sops-encryption-key/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ resource "aws_kms_key_policy" "this" {
Resource = "*",
"Condition" : {
"ArnLike" : {
"aws:PrincipalArn" = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/aws-reserved/sso.amazonaws.com/*AWSReservedSSO_Read-Only_*"
"aws:PrincipalArn" = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/aws-reserved/sso.amazonaws.com/*AWSReservedSSO_Administrator_*"
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions infra/testnet/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ module "sops-encryption-key" {
source = "../modules/sops-encryption-key"
}

module "admin-key-eu" {
source = "../modules/admin-key"
providers = {
aws = aws.eu
}
}

resource "aws_route53_zone" "this" {
name = "testnet.walletconnect.network"
}
Expand Down
6 changes: 3 additions & 3 deletions infra/testnet/sops/admin.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"ecdsa_private_key": "ENC[AES256_GCM,data:7CabaechT+0PbaFOaPPWUB04PJGLZWnp9k/bfE7Z4RRc5ONatrgRP8Ffv7lFXoWS0i3k9DaVBFewIXEUzS9FoM4s,iv:gXC7QyHk3GpU46CfYLimelH/3NzIUymBByCy9dIq3xw=,tag:YbBe5LateVcAJVI21mWCYg==,type:str]",
"kms_key_arn_unencrypted": "arn:aws:kms:eu-central-1:260477932319:key/mrk-2d757d57eea144a58aeb03f162793103",
"smart_contract_address_unencrypted": "0x31551311408e4428b82e1acf042217a5446ff490",
"smart_contract_encryption_key": "ENC[AES256_GCM,data:ILQVdF18cdDg7AyaFks4f1GBQ2B3GfzYBSqyz2s5ANcZly9v07yJkr7roFMX1vYfuFtKcWHl5fqQWcbEZ0o7LQ==,iv:MorAAGzt46ue4svn+t02AcecYSDyvLRWGZKR4Xog5qE=,tag:9QS+ZKIGdScqgaw5oceiWA==,type:str]",
"rpc_provider_url": "ENC[AES256_GCM,data:6BFFka9TWP0gBWJtFtUoFnYawaQhGEPppXsmNvHWUESWVyy+7iXqH9Rk5YBeEavT482RsEVUyYZTXzIL8x0xZDLl4ltckYBa/wU8pvc4GgM+8egUQ/ue35T1AQ==,iv:PBcTvMBVnwOADi/5BaqF0gw2h/pOdBL6rx9njShFyT0=,tag:PwCRHWOD+WgElLW4F15IBQ==,type:str]",
Expand All @@ -12,8 +12,8 @@
"aws_profile": ""
}
],
"lastmodified": "2026-01-13T13:28:42Z",
"mac": "ENC[AES256_GCM,data:XHKy9XYUyO2JhtcZPs5XyrF57P88OJTSVuXRc5qJoVrwTxF2ktRgzXQmxrS0udO/6RSNjVDLmOw/5wyhOQTYGkDhAMil3D0F7JBNc/oU3mC1Y6zqMI+RSFLD08xDsXUGgQNPI/iMtOaXKrtKqim7naYrikTeJn3WPKcSWNwJIfs=,iv:RiiImAiWSLv9smXsfs/5fWE2ttiv1Bim0yFPxz1m088=,tag:Fy/okHzYD5GRbPwcqVoMgw==,type:str]",
"lastmodified": "2026-02-04T16:31:08Z",
"mac": "ENC[AES256_GCM,data:0O8RTOCdk/N1pd2FNV+eHpbc9zms6HPOhm64vcvTEzS525K6Wwu/3amlS251XaLCkIypjxiuJDsVzgDQY/xnWgFEQUO28Ge6uAVO23xWXAqgAM9DxHneceXXjE6N3vOTgjWUBrw60r5uqY5XzfJwpABJmWZ97j2X/JaBJJZ/Iws=,iv:DzQAzAp9T8ASmZ4olAKsxaiyLCy/Y5YKhtev+09obQI=,tag:e11p8y3dwVIMOpafr9W7Qg==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
Expand Down