From 10f90763c2316936985dba9f3a5555189bf4a7a5 Mon Sep 17 00:00:00 2001 From: elizabeth Date: Mon, 3 Feb 2025 14:46:41 -0500 Subject: [PATCH 01/19] create Signer trait, begin frost signer --- Cargo.lock | 25 ++--- crates/astria-bridge-withdrawer/Cargo.toml | 1 + .../src/bridge_withdrawer/mod.rs | 9 +- .../bridge_withdrawer/submitter/builder.rs | 16 ++-- .../submitter/frost_signer.rs | 95 +++++++++++++++++++ .../src/bridge_withdrawer/submitter/mod.rs | 8 +- .../src/bridge_withdrawer/submitter/signer.rs | 17 +++- 7 files changed, 141 insertions(+), 30 deletions(-) create mode 100644 crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/frost_signer.rs diff --git a/Cargo.lock b/Cargo.lock index cf483afff4..882274e382 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -528,6 +528,7 @@ dependencies = [ "astria-telemetry", "axum", "ethers", + "frost-ed25519", "futures", "hex", "http 0.2.12", @@ -2538,9 +2539,9 @@ dependencies = [ [[package]] name = "derive-getters" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6433aac097572ea8ccc60b3f2e756c661c9aeed9225cdd4d0cb119cb7ff6ba" +checksum = "74ef43543e701c01ad77d3a5922755c6a1d71b22d942cb8042be4994b380caff" dependencies = [ "proc-macro2 1.0.92", "quote", @@ -3402,9 +3403,9 @@ dependencies = [ [[package]] name = "frost-core" -version = "2.0.0-rc.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed1383227a6606aacf5df9a17ff57824c6971a0ab225b69b911bec0ba7bbb869" +checksum = "1858230cabb6792a5020daf4b0074f57b7d1e2a520ac544c77f102babee62ff4" dependencies = [ "byteorder", "const-crc32-nostd", @@ -3412,12 +3413,12 @@ dependencies = [ "derive-getters", "document-features", "hex", - "itertools 0.13.0", + "itertools 0.14.0", "postcard", "rand_core 0.6.4", "serde", "serdect", - "thiserror 1.0.63", + "thiserror 2.0.8", "thiserror-nostd-notrait", "visibility", "zeroize", @@ -3425,9 +3426,9 @@ dependencies = [ [[package]] name = "frost-ed25519" -version = "2.0.0-rc.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab23316e09987113dc8a2a8f0b656d7f1b24dc2afdc8c34df98276d1158c97d" +checksum = "c350ac3d0463a009a061aba12b67920acee94338951c849bb4c492d55223dece" dependencies = [ "curve25519-dalek", "document-features", @@ -3439,9 +3440,9 @@ dependencies = [ [[package]] name = "frost-rerandomized" -version = "2.0.0-rc.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb14a6054f9ce5aa4912c60c11392d42c43acec8295ee1df1f67a9d0b7a73ee" +checksum = "e8a3b10d9c1e9f298522510940b5b8c3d55040420517ec8d2bb86c4c2d1ae3ee" dependencies = [ "derive-getters", "document-features", @@ -4780,9 +4781,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] diff --git a/crates/astria-bridge-withdrawer/Cargo.toml b/crates/astria-bridge-withdrawer/Cargo.toml index 103f23ae8e..9040aa75a6 100644 --- a/crates/astria-bridge-withdrawer/Cargo.toml +++ b/crates/astria-bridge-withdrawer/Cargo.toml @@ -10,6 +10,7 @@ homepage = "https://astria.org" [dependencies] http = "0.2.9" +frost-ed25519 = { version = "2.1.0", features = [] } axum = { workspace = true } futures = { workspace = true } diff --git a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/mod.rs b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/mod.rs index aeb0f1a721..a470f5bfd5 100644 --- a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/mod.rs +++ b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/mod.rs @@ -119,13 +119,18 @@ impl BridgeWithdrawer { let startup_handle = startup::InfoHandle::new(state.subscribe()); // make submitter object + let signer = crate::bridge_withdrawer::submitter::signer::SequencerKey::builder() + .path(sequencer_key_path) + .prefix(sequencer_address_prefix) + .try_build() + .wrap_err("failed to load sequencer private key")?; + let (submitter, submitter_handle) = submitter::Builder { shutdown_token: shutdown_handle.token(), startup_handle: startup_handle.clone(), sequencer_cometbft_client, sequencer_grpc_client, - sequencer_key_path, - sequencer_address_prefix: sequencer_address_prefix.clone(), + signer: Box::new(signer), state: state.clone(), metrics, } diff --git a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/builder.rs b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/builder.rs index 62308d1572..a4326537be 100644 --- a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/builder.rs +++ b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/builder.rs @@ -12,7 +12,10 @@ use tracing::{ instrument, }; -use super::state::State; +use super::{ + signer::Signer, + state::State, +}; use crate::{ bridge_withdrawer::{ startup, @@ -46,8 +49,7 @@ impl Handle { pub(crate) struct Builder { pub(crate) shutdown_token: CancellationToken, pub(crate) startup_handle: startup::InfoHandle, - pub(crate) sequencer_key_path: String, - pub(crate) sequencer_address_prefix: String, + pub(crate) signer: Box, pub(crate) sequencer_cometbft_client: sequencer_client::HttpClient, pub(crate) sequencer_grpc_client: SequencerServiceClient, pub(crate) state: Arc, @@ -60,19 +62,13 @@ impl Builder { let Self { shutdown_token, startup_handle, - sequencer_key_path, - sequencer_address_prefix, + signer, sequencer_cometbft_client, sequencer_grpc_client, state, metrics, } = self; - let signer = super::signer::SequencerKey::builder() - .path(sequencer_key_path) - .prefix(sequencer_address_prefix) - .try_build() - .wrap_err("failed to load sequencer private key")?; info!(address = %signer.address(), "loaded sequencer signer"); let (batches_tx, batches_rx) = tokio::sync::mpsc::channel(BATCH_QUEUE_SIZE); diff --git a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/frost_signer.rs b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/frost_signer.rs new file mode 100644 index 0000000000..724dbca1ce --- /dev/null +++ b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/frost_signer.rs @@ -0,0 +1,95 @@ +use astria_core::{ + crypto::VerificationKey, + primitive::v1::Address, + protocol::transaction::v1::{ + Transaction, + TransactionBody, + }, +}; +use astria_eyre::eyre::{ + self, + eyre, + Context, +}; +use frost_ed25519::keys::{ + KeyPackage, + PublicKeyPackage, +}; + +use super::Signer; + +pub(crate) struct FrostSignerBuilder { + key_package: Option, + public_key_package: Option, + prefix: Option, +} + +impl FrostSignerBuilder { + pub(crate) fn key_package(self, key_package: KeyPackage) -> Self { + Self { + key_package: Some(key_package), + ..self + } + } + + pub(crate) fn public_key_package(self, public_key_package: PublicKeyPackage) -> Self { + Self { + public_key_package: Some(public_key_package), + ..self + } + } + + pub(crate) fn prefix(self, prefix: String) -> Self { + Self { + prefix: Some(prefix), + ..self + } + } + + pub(crate) fn try_build(self) -> eyre::Result { + let key_package = self + .key_package + .ok_or_else(|| eyre!("key package is required"))?; + let public_key_package = self + .public_key_package + .ok_or_else(|| eyre!("public key package is required"))?; + let verifying_key_bytes: [u8; 32] = public_key_package + .verifying_key() + .serialize() + .wrap_err("failed to serialize verifying key")? + .try_into() + .map_err(|_| eyre!("failed to convert verifying key to 32 bytes"))?; + let verifying_key: VerificationKey = VerificationKey::try_from(verifying_key_bytes) + .wrap_err("failed to build verification key")?; + let address = Address::builder() + .array(*verifying_key.address_bytes()) + .prefix( + self.prefix + .ok_or_else(|| eyre!("astria address prefix is required"))?, + ) + .try_build() + .wrap_err("failed to build address")?; + + Ok(FrostSigner { + key_package, + public_key_package, + address, + }) + } +} + +pub(crate) struct FrostSigner { + key_package: KeyPackage, + public_key_package: PublicKeyPackage, + address: Address, +} + +impl Signer for FrostSigner { + fn sign(&self, tx: TransactionBody) -> Transaction { + todo!() + } + + fn address(&self) -> &Address { + &self.address + } +} diff --git a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/mod.rs b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/mod.rs index 78a38bf50b..6078e7f42a 100644 --- a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/mod.rs +++ b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/mod.rs @@ -34,7 +34,6 @@ use sequencer_client::{ SequencerClientExt, Transaction, }; -use signer::SequencerKey; use state::State; use tokio::{ select, @@ -61,8 +60,11 @@ use super::{ use crate::metrics::Metrics; mod builder; +pub(crate) mod frost_signer; pub(crate) mod signer; +use crate::bridge_withdrawer::submitter::signer::Signer; + pub(super) struct Submitter { shutdown_token: CancellationToken, startup_handle: startup::InfoHandle, @@ -70,7 +72,7 @@ pub(super) struct Submitter { batches_rx: mpsc::Receiver, sequencer_cometbft_client: sequencer_client::HttpClient, sequencer_grpc_client: SequencerServiceClient, - signer: SequencerKey, + signer: Box, metrics: &'static Metrics, } @@ -178,7 +180,7 @@ impl Submitter { .wrap_err("failed to build unsigned transaction")?; // sign transaction - let signed = unsigned.sign(signer.signing_key()); + let signed = signer.sign(unsigned); debug!(transaction_id = %&signed.id(), "signed transaction"); // submit transaction and handle response diff --git a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/signer.rs b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/signer.rs index d5a0c8be23..a2f32354ff 100644 --- a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/signer.rs +++ b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/signer.rs @@ -9,6 +9,10 @@ use std::{ use astria_core::{ crypto::SigningKey, primitive::v1::Address, + protocol::transaction::v1::{ + Transaction, + TransactionBody, + }, }; use astria_eyre::eyre::{ self, @@ -17,6 +21,11 @@ use astria_eyre::eyre::{ Context, }; +pub(crate) trait Signer: Send + Sync { + fn address(&self) -> &Address; + fn sign(&self, tx: TransactionBody) -> Transaction; +} + pub(crate) struct SequencerKey { address: Address, signing_key: SigningKey, @@ -96,12 +105,14 @@ impl SequencerKey { prefix: None, } } +} - pub(crate) fn address(&self) -> &Address { +impl Signer for SequencerKey { + fn address(&self) -> &Address { &self.address } - pub(crate) fn signing_key(&self) -> &SigningKey { - &self.signing_key + fn sign(&self, tx: TransactionBody) -> Transaction { + tx.sign(&self.signing_key) } } From c96247167dfdc4f92d42f86c9f01c589de3146eb Mon Sep 17 00:00:00 2001 From: elizabeth Date: Mon, 3 Feb 2025 18:06:11 -0500 Subject: [PATCH 02/19] add frost protos; initial impl of coordinator with participant clients --- Cargo.lock | 1 + buf.yaml | 16 + crates/astria-bridge-withdrawer/Cargo.toml | 1 + .../submitter/frost_signer.rs | 215 +++++- .../src/bridge_withdrawer/submitter/mod.rs | 6 +- .../src/bridge_withdrawer/submitter/signer.rs | 9 +- .../src/generated/astria.signer.v1.rs | 553 ++++++++++++++ .../src/generated/astria.signer.v1.serde.rs | 690 ++++++++++++++++++ crates/astria-core/src/generated/mod.rs | 6 + crates/astria-core/src/lib.rs | 1 + crates/astria-core/src/signer/mod.rs | 1 + crates/astria-core/src/signer/v1/mod.rs | 1 + proto/signerapis/astria/signer/v1/frost.proto | 46 ++ 13 files changed, 1533 insertions(+), 13 deletions(-) create mode 100644 crates/astria-core/src/generated/astria.signer.v1.rs create mode 100644 crates/astria-core/src/generated/astria.signer.v1.serde.rs create mode 100644 crates/astria-core/src/signer/mod.rs create mode 100644 crates/astria-core/src/signer/v1/mod.rs create mode 100644 proto/signerapis/astria/signer/v1/frost.proto diff --git a/Cargo.lock b/Cargo.lock index 882274e382..38114a9866 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -535,6 +535,7 @@ dependencies = [ "humantime", "hyper 0.14.30", "ibc-types", + "pbjson-types", "pin-project-lite", "prost", "reqwest", diff --git a/buf.yaml b/buf.yaml index d31ad1e385..5a696e2f2c 100644 --- a/buf.yaml +++ b/buf.yaml @@ -83,6 +83,22 @@ modules: use: - WIRE_JSON ignore_unstable_packages: true + - path: proto/signerapis + name: buf.build/astria/signer-apis + lint: + use: + - BASIC + - ENUM_VALUE_PREFIX + - ENUM_ZERO_VALUE_SUFFIX + - FILE_LOWER_SNAKE_CASE + - PACKAGE_VERSION_SUFFIX + - RPC_REQUEST_STANDARD_NAME + - SERVICE_SUFFIX + disallow_comment_ignores: true + breaking: + use: + - WIRE_JSON + ignore_unstable_packages: true - path: proto/vendored name: buf.build/astria/vendored lint: diff --git a/crates/astria-bridge-withdrawer/Cargo.toml b/crates/astria-bridge-withdrawer/Cargo.toml index 9040aa75a6..eae0ab04f0 100644 --- a/crates/astria-bridge-withdrawer/Cargo.toml +++ b/crates/astria-bridge-withdrawer/Cargo.toml @@ -19,6 +19,7 @@ ethers = { workspace = true, features = ["ws"] } hyper = { workspace = true } humantime = { workspace = true } ibc-types = { workspace = true } +pbjson-types = { workspace = true } pin-project-lite = { workspace = true } prost = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/frost_signer.rs b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/frost_signer.rs index 724dbca1ce..7a1e155f87 100644 --- a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/frost_signer.rs +++ b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/frost_signer.rs @@ -1,27 +1,90 @@ +use std::collections::{ + BTreeMap, + HashMap, +}; + use astria_core::{ crypto::VerificationKey, + generated::astria::{ + protocol::transaction, + signer::v1::{ + frost_participant_service_client::FrostParticipantServiceClient, + CommitmentWithIdentifier, + GetVerifyingShareRequest, + Part1Request, + Part2Request, + }, + }, primitive::v1::Address, protocol::transaction::v1::{ Transaction, TransactionBody, }, + Protobuf, }; use astria_eyre::eyre::{ self, + ensure, eyre, Context, }; -use frost_ed25519::keys::{ - KeyPackage, - PublicKeyPackage, +use ethers::types::Sign; +use frost_ed25519::{ + keys::{ + KeyPackage, + PublicKeyPackage, + VerifyingShare, + }, + round1, + Identifier, }; use super::Signer; +pub(crate) async fn initialize_frost_participant_clients( + endpoints: Vec, + public_key_package: PublicKeyPackage, +) -> eyre::Result>> { + // TODO: maybe remove this check, and just check we have >= min_signers in build() + ensure!( + endpoints.len() == public_key_package.verifying_shares().len(), + "number of endpoints does not match number of participants" + ); + + let mut participant_clients = HashMap::new(); + for endpoint in endpoints { + let mut client = FrostParticipantServiceClient::connect(endpoint) + .await + .wrap_err("failed to connect to participant")?; + let resp = client + .get_verifying_share(GetVerifyingShareRequest {}) + .await + .wrap_err("failed to get verifying share")?; + let verifying_share = VerifyingShare::deserialize(&resp.into_inner().verifying_share) + .wrap_err("failed to deserialize verifying share")?; + let identifier = public_key_package + .verifying_shares() + .iter() + .find(|(_, vs)| vs == &&verifying_share) + .map(|(id, _)| id) + .ok_or_else(|| eyre!("failed to find identifier for verifying share"))?; + participant_clients.insert(identifier.to_owned(), client); + } + + ensure!( + participant_clients.len() == public_key_package.verifying_shares().len(), + "failed to initialize all participant clients; are there duplicate endpoints?" + ); + + Ok(participant_clients) +} + pub(crate) struct FrostSignerBuilder { key_package: Option, public_key_package: Option, - prefix: Option, + address_prefix: Option, + participant_clients: + HashMap>, } impl FrostSignerBuilder { @@ -39,9 +102,22 @@ impl FrostSignerBuilder { } } - pub(crate) fn prefix(self, prefix: String) -> Self { + pub(crate) fn address_prefix(self, address_prefix: String) -> Self { Self { - prefix: Some(prefix), + address_prefix: Some(address_prefix), + ..self + } + } + + pub(crate) fn participant_clients( + self, + participant_clients: HashMap< + Identifier, + FrostParticipantServiceClient, + >, + ) -> Self { + Self { + participant_clients, ..self } } @@ -64,7 +140,7 @@ impl FrostSignerBuilder { let address = Address::builder() .array(*verifying_key.address_bytes()) .prefix( - self.prefix + self.address_prefix .ok_or_else(|| eyre!("astria address prefix is required"))?, ) .try_build() @@ -74,6 +150,7 @@ impl FrostSignerBuilder { key_package, public_key_package, address, + participant_clients: self.participant_clients, }) } } @@ -82,11 +159,131 @@ pub(crate) struct FrostSigner { key_package: KeyPackage, public_key_package: PublicKeyPackage, address: Address, + participant_clients: + HashMap>, } +#[tonic::async_trait] impl Signer for FrostSigner { - fn sign(&self, tx: TransactionBody) -> Transaction { - todo!() + async fn sign(&self, tx: TransactionBody) -> eyre::Result { + use futures::StreamExt as _; + use prost::{ + Message as _, + Name as _, + }; + + // TODO: -1 for min_other_signers; or do we want all participants + // to be separate processes? + let min_signers: usize = (*self.key_package.min_signers()).into(); + + // part 1 + let stream = futures::stream::FuturesUnordered::new(); + for (id, client) in &self.participant_clients { + let mut client = client.clone(); + stream.push(async move { + let resp = client.part1(Part1Request {}).await.wrap_err(format!( + "failed to get part 1 commitment for participant with id {:?}", + id + ))?; + Ok((id, resp.into_inner())) + }); + } + let results: Vec> = stream.collect::>().await; + let mut commitments = Vec::new(); + let mut signing_package_commitments: BTreeMap = + BTreeMap::new(); + + for res in results { + if let Ok((id, part1)) = res { + let signing_commitment = round1::SigningCommitments::deserialize(&part1.commitment) + .wrap_err("failed to deserialize signing commitment")?; + signing_package_commitments.insert(*id, signing_commitment); + commitments.push((id, part1.commitment, part1.request_identifier)); + }; + } + ensure!( + commitments.len() >= min_signers, + "not enough part 1 commitments" + ); + + // part 2 + let stream = futures::stream::FuturesUnordered::new(); + let request_commitments: Vec = commitments + .iter() + .map(|(id, commitment, _)| CommitmentWithIdentifier { + commitment: commitment.clone(), + participant_identifier: id.serialize().to_vec().into(), + }) + .collect(); + let tx_bytes = tx.to_raw().encode_to_vec(); + for (id, _, request_identifier) in commitments { + let mut client = self + .participant_clients + .get(&id) + .ok_or_else(|| eyre!("failed to find participant client"))? + .clone(); + let request_commitments = request_commitments.clone(); + let tx = tx.clone(); + stream.push(async move { + let resp = client + .part2(Part2Request { + request_identifier, + transaction_body: Some(tx.into_raw()), /* TODO: this needs to + * be bytes for + * determinism */ + commitments: request_commitments, + }) + .await + .wrap_err(format!( + "failed to get part 2 response for participant with id {:?}", + id + ))?; + Ok((id, resp.into_inner())) + }); + } + let results: Vec> = stream.collect::>().await; + let mut sig_shares: BTreeMap = + BTreeMap::new(); + for res in results { + if let Ok((id, part2)) = res { + sig_shares.insert( + *id, + frost_ed25519::round2::SignatureShare::deserialize(&part2.signature_share) + .wrap_err("failed to deserialize signature share")?, + ); + }; + } + ensure!( + sig_shares.len() >= min_signers, + "not enough part 2 signature shares" + ); + + // aggregate and create signature + let signing_package = + frost_ed25519::SigningPackage::new(signing_package_commitments, &tx_bytes); + let signature = + frost_ed25519::aggregate(&signing_package, &sig_shares, &self.public_key_package) + .wrap_err("failed to aggregate")?; + + let raw_transaction = astria_core::generated::astria::protocol::transaction::v1::Transaction { + body: Some(pbjson_types::Any { + type_url: astria_core::generated::astria::protocol::transaction::v1::TransactionBody::type_url(), + value: tx_bytes.into(), + }), + signature: signature + .serialize() + .wrap_err("failed to serialize signature")? + .into(), + public_key: self.public_key_package + .verifying_key() + .serialize() + .wrap_err("failed to serialize verifying key")? + .into(), + }; + let transaction = Transaction::try_from_raw(raw_transaction) + .wrap_err("failed to convert to transaction")?; + + Ok(transaction) } fn address(&self) -> &Address { diff --git a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/mod.rs b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/mod.rs index 6078e7f42a..cfccace352 100644 --- a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/mod.rs +++ b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/mod.rs @@ -180,7 +180,11 @@ impl Submitter { .wrap_err("failed to build unsigned transaction")?; // sign transaction - let signed = signer.sign(unsigned); + // TODO: how to handle failure? + let signed = signer + .sign(unsigned) + .await + .wrap_err("failed to sign transaction")?; debug!(transaction_id = %&signed.id(), "signed transaction"); // submit transaction and handle response diff --git a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/signer.rs b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/signer.rs index a2f32354ff..0d14e55092 100644 --- a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/signer.rs +++ b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/signer.rs @@ -20,10 +20,12 @@ use astria_eyre::eyre::{ eyre, Context, }; +use tonic::async_trait; +#[async_trait] pub(crate) trait Signer: Send + Sync { fn address(&self) -> &Address; - fn sign(&self, tx: TransactionBody) -> Transaction; + async fn sign(&self, tx: TransactionBody) -> eyre::Result; } pub(crate) struct SequencerKey { @@ -107,12 +109,13 @@ impl SequencerKey { } } +#[async_trait] impl Signer for SequencerKey { fn address(&self) -> &Address { &self.address } - fn sign(&self, tx: TransactionBody) -> Transaction { - tx.sign(&self.signing_key) + async fn sign(&self, tx: TransactionBody) -> eyre::Result { + Ok(tx.sign(&self.signing_key)) } } diff --git a/crates/astria-core/src/generated/astria.signer.v1.rs b/crates/astria-core/src/generated/astria.signer.v1.rs new file mode 100644 index 0000000000..76746e8e74 --- /dev/null +++ b/crates/astria-core/src/generated/astria.signer.v1.rs @@ -0,0 +1,553 @@ +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetVerifyingShareRequest {} +impl ::prost::Name for GetVerifyingShareRequest { + const NAME: &'static str = "GetVerifyingShareRequest"; + const PACKAGE: &'static str = "astria.signer.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.signer.v1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetVerifyingShareResponse { + /// the verifying share (partial public key) of the participant. + /// this is used for the coordinator to determine the identifier of the participant. + #[prost(bytes = "bytes", tag = "1")] + pub verifying_share: ::prost::bytes::Bytes, +} +impl ::prost::Name for GetVerifyingShareResponse { + const NAME: &'static str = "GetVerifyingShareResponse"; + const PACKAGE: &'static str = "astria.signer.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.signer.v1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CommitmentWithIdentifier { + #[prost(bytes = "bytes", tag = "1")] + pub commitment: ::prost::bytes::Bytes, + #[prost(bytes = "bytes", tag = "2")] + pub participant_identifier: ::prost::bytes::Bytes, +} +impl ::prost::Name for CommitmentWithIdentifier { + const NAME: &'static str = "CommitmentWithIdentifier"; + const PACKAGE: &'static str = "astria.signer.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.signer.v1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Part1Request {} +impl ::prost::Name for Part1Request { + const NAME: &'static str = "Part1Request"; + const PACKAGE: &'static str = "astria.signer.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.signer.v1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Part1Response { + #[prost(bytes = "bytes", tag = "1")] + pub commitment: ::prost::bytes::Bytes, + /// required for the participant to internally track the nonce + /// corresponding to the commitment. + #[prost(uint32, tag = "2")] + pub request_identifier: u32, +} +impl ::prost::Name for Part1Response { + const NAME: &'static str = "Part1Response"; + const PACKAGE: &'static str = "astria.signer.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.signer.v1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Part2Request { + #[prost(message, repeated, tag = "1")] + pub commitments: ::prost::alloc::vec::Vec, + #[prost(message, optional, tag = "2")] + pub transaction_body: ::core::option::Option< + super::super::protocol::transaction::v1::TransactionBody, + >, + #[prost(uint32, tag = "3")] + pub request_identifier: u32, +} +impl ::prost::Name for Part2Request { + const NAME: &'static str = "Part2Request"; + const PACKAGE: &'static str = "astria.signer.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.signer.v1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Part2Response { + #[prost(bytes = "bytes", tag = "1")] + pub signature_share: ::prost::bytes::Bytes, +} +impl ::prost::Name for Part2Response { + const NAME: &'static str = "Part2Response"; + const PACKAGE: &'static str = "astria.signer.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.signer.v1.{}", Self::NAME) + } +} +/// Generated client implementations. +#[cfg(feature = "client")] +pub mod frost_participant_service_client { + #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + #[derive(Debug, Clone)] + pub struct FrostParticipantServiceClient { + inner: tonic::client::Grpc, + } + impl FrostParticipantServiceClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl FrostParticipantServiceClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + Send + 'static, + ::Error: Into + Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> FrostParticipantServiceClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + Send + Sync, + { + FrostParticipantServiceClient::new( + InterceptedService::new(inner, interceptor), + ) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + pub async fn get_verifying_share( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/astria.signer.v1.FrostParticipantService/GetVerifyingShare", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "astria.signer.v1.FrostParticipantService", + "GetVerifyingShare", + ), + ); + self.inner.unary(req, path, codec).await + } + pub async fn part1( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/astria.signer.v1.FrostParticipantService/Part1", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("astria.signer.v1.FrostParticipantService", "Part1"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn part2( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/astria.signer.v1.FrostParticipantService/Part2", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("astria.signer.v1.FrostParticipantService", "Part2"), + ); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +#[cfg(feature = "server")] +pub mod frost_participant_service_server { + #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with FrostParticipantServiceServer. + #[async_trait] + pub trait FrostParticipantService: Send + Sync + 'static { + async fn get_verifying_share( + self: std::sync::Arc, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn part1( + self: std::sync::Arc, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + async fn part2( + self: std::sync::Arc, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + } + #[derive(Debug)] + pub struct FrostParticipantServiceServer { + inner: _Inner, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + struct _Inner(Arc); + impl FrostParticipantServiceServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + let inner = _Inner(inner); + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> + for FrostParticipantServiceServer + where + T: FrostParticipantService, + B: Body + Send + 'static, + B::Error: Into + Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + let inner = self.inner.clone(); + match req.uri().path() { + "/astria.signer.v1.FrostParticipantService/GetVerifyingShare" => { + #[allow(non_camel_case_types)] + struct GetVerifyingShareSvc(pub Arc); + impl< + T: FrostParticipantService, + > tonic::server::UnaryService + for GetVerifyingShareSvc { + type Response = super::GetVerifyingShareResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_verifying_share( + inner, + request, + ) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = GetVerifyingShareSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/astria.signer.v1.FrostParticipantService/Part1" => { + #[allow(non_camel_case_types)] + struct Part1Svc(pub Arc); + impl< + T: FrostParticipantService, + > tonic::server::UnaryService for Part1Svc { + type Response = super::Part1Response; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::part1(inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = Part1Svc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/astria.signer.v1.FrostParticipantService/Part2" => { + #[allow(non_camel_case_types)] + struct Part2Svc(pub Arc); + impl< + T: FrostParticipantService, + > tonic::server::UnaryService for Part2Svc { + type Response = super::Part2Response; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::part2(inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = Part2Svc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => { + Box::pin(async move { + Ok( + http::Response::builder() + .status(200) + .header("grpc-status", "12") + .header("content-type", "application/grpc") + .body(empty_body()) + .unwrap(), + ) + }) + } + } + } + } + impl Clone for FrostParticipantServiceServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + impl Clone for _Inner { + fn clone(&self) -> Self { + Self(Arc::clone(&self.0)) + } + } + impl std::fmt::Debug for _Inner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0) + } + } + impl tonic::server::NamedService + for FrostParticipantServiceServer { + const NAME: &'static str = "astria.signer.v1.FrostParticipantService"; + } +} diff --git a/crates/astria-core/src/generated/astria.signer.v1.serde.rs b/crates/astria-core/src/generated/astria.signer.v1.serde.rs new file mode 100644 index 0000000000..9b91af5104 --- /dev/null +++ b/crates/astria-core/src/generated/astria.signer.v1.serde.rs @@ -0,0 +1,690 @@ +impl serde::Serialize for CommitmentWithIdentifier { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.commitment.is_empty() { + len += 1; + } + if !self.participant_identifier.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.signer.v1.CommitmentWithIdentifier", len)?; + if !self.commitment.is_empty() { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("commitment", pbjson::private::base64::encode(&self.commitment).as_str())?; + } + if !self.participant_identifier.is_empty() { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("participantIdentifier", pbjson::private::base64::encode(&self.participant_identifier).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for CommitmentWithIdentifier { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "commitment", + "participant_identifier", + "participantIdentifier", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Commitment, + ParticipantIdentifier, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "commitment" => Ok(GeneratedField::Commitment), + "participantIdentifier" | "participant_identifier" => Ok(GeneratedField::ParticipantIdentifier), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = CommitmentWithIdentifier; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.signer.v1.CommitmentWithIdentifier") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut commitment__ = None; + let mut participant_identifier__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Commitment => { + if commitment__.is_some() { + return Err(serde::de::Error::duplicate_field("commitment")); + } + commitment__ = + Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) + ; + } + GeneratedField::ParticipantIdentifier => { + if participant_identifier__.is_some() { + return Err(serde::de::Error::duplicate_field("participantIdentifier")); + } + participant_identifier__ = + Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) + ; + } + } + } + Ok(CommitmentWithIdentifier { + commitment: commitment__.unwrap_or_default(), + participant_identifier: participant_identifier__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("astria.signer.v1.CommitmentWithIdentifier", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for GetVerifyingShareRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let len = 0; + let struct_ser = serializer.serialize_struct("astria.signer.v1.GetVerifyingShareRequest", len)?; + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for GetVerifyingShareRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + Err(serde::de::Error::unknown_field(value, FIELDS)) + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GetVerifyingShareRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.signer.v1.GetVerifyingShareRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + while map_.next_key::()?.is_some() { + let _ = map_.next_value::()?; + } + Ok(GetVerifyingShareRequest { + }) + } + } + deserializer.deserialize_struct("astria.signer.v1.GetVerifyingShareRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for GetVerifyingShareResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.verifying_share.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.signer.v1.GetVerifyingShareResponse", len)?; + if !self.verifying_share.is_empty() { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("verifyingShare", pbjson::private::base64::encode(&self.verifying_share).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for GetVerifyingShareResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "verifying_share", + "verifyingShare", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + VerifyingShare, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "verifyingShare" | "verifying_share" => Ok(GeneratedField::VerifyingShare), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GetVerifyingShareResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.signer.v1.GetVerifyingShareResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut verifying_share__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::VerifyingShare => { + if verifying_share__.is_some() { + return Err(serde::de::Error::duplicate_field("verifyingShare")); + } + verifying_share__ = + Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) + ; + } + } + } + Ok(GetVerifyingShareResponse { + verifying_share: verifying_share__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("astria.signer.v1.GetVerifyingShareResponse", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for Part1Request { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let len = 0; + let struct_ser = serializer.serialize_struct("astria.signer.v1.Part1Request", len)?; + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for Part1Request { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + Err(serde::de::Error::unknown_field(value, FIELDS)) + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = Part1Request; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.signer.v1.Part1Request") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + while map_.next_key::()?.is_some() { + let _ = map_.next_value::()?; + } + Ok(Part1Request { + }) + } + } + deserializer.deserialize_struct("astria.signer.v1.Part1Request", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for Part1Response { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.commitment.is_empty() { + len += 1; + } + if self.request_identifier != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.signer.v1.Part1Response", len)?; + if !self.commitment.is_empty() { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("commitment", pbjson::private::base64::encode(&self.commitment).as_str())?; + } + if self.request_identifier != 0 { + struct_ser.serialize_field("requestIdentifier", &self.request_identifier)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for Part1Response { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "commitment", + "request_identifier", + "requestIdentifier", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Commitment, + RequestIdentifier, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "commitment" => Ok(GeneratedField::Commitment), + "requestIdentifier" | "request_identifier" => Ok(GeneratedField::RequestIdentifier), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = Part1Response; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.signer.v1.Part1Response") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut commitment__ = None; + let mut request_identifier__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Commitment => { + if commitment__.is_some() { + return Err(serde::de::Error::duplicate_field("commitment")); + } + commitment__ = + Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) + ; + } + GeneratedField::RequestIdentifier => { + if request_identifier__.is_some() { + return Err(serde::de::Error::duplicate_field("requestIdentifier")); + } + request_identifier__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + } + } + Ok(Part1Response { + commitment: commitment__.unwrap_or_default(), + request_identifier: request_identifier__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("astria.signer.v1.Part1Response", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for Part2Request { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.commitments.is_empty() { + len += 1; + } + if self.transaction_body.is_some() { + len += 1; + } + if self.request_identifier != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.signer.v1.Part2Request", len)?; + if !self.commitments.is_empty() { + struct_ser.serialize_field("commitments", &self.commitments)?; + } + if let Some(v) = self.transaction_body.as_ref() { + struct_ser.serialize_field("transactionBody", v)?; + } + if self.request_identifier != 0 { + struct_ser.serialize_field("requestIdentifier", &self.request_identifier)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for Part2Request { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "commitments", + "transaction_body", + "transactionBody", + "request_identifier", + "requestIdentifier", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Commitments, + TransactionBody, + RequestIdentifier, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "commitments" => Ok(GeneratedField::Commitments), + "transactionBody" | "transaction_body" => Ok(GeneratedField::TransactionBody), + "requestIdentifier" | "request_identifier" => Ok(GeneratedField::RequestIdentifier), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = Part2Request; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.signer.v1.Part2Request") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut commitments__ = None; + let mut transaction_body__ = None; + let mut request_identifier__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Commitments => { + if commitments__.is_some() { + return Err(serde::de::Error::duplicate_field("commitments")); + } + commitments__ = Some(map_.next_value()?); + } + GeneratedField::TransactionBody => { + if transaction_body__.is_some() { + return Err(serde::de::Error::duplicate_field("transactionBody")); + } + transaction_body__ = map_.next_value()?; + } + GeneratedField::RequestIdentifier => { + if request_identifier__.is_some() { + return Err(serde::de::Error::duplicate_field("requestIdentifier")); + } + request_identifier__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + } + } + Ok(Part2Request { + commitments: commitments__.unwrap_or_default(), + transaction_body: transaction_body__, + request_identifier: request_identifier__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("astria.signer.v1.Part2Request", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for Part2Response { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.signature_share.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.signer.v1.Part2Response", len)?; + if !self.signature_share.is_empty() { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("signatureShare", pbjson::private::base64::encode(&self.signature_share).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for Part2Response { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "signature_share", + "signatureShare", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + SignatureShare, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "signatureShare" | "signature_share" => Ok(GeneratedField::SignatureShare), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = Part2Response; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.signer.v1.Part2Response") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut signature_share__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::SignatureShare => { + if signature_share__.is_some() { + return Err(serde::de::Error::duplicate_field("signatureShare")); + } + signature_share__ = + Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) + ; + } + } + } + Ok(Part2Response { + signature_share: signature_share__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("astria.signer.v1.Part2Response", FIELDS, GeneratedVisitor) + } +} diff --git a/crates/astria-core/src/generated/mod.rs b/crates/astria-core/src/generated/mod.rs index 1fef28c5e7..a3981dd43e 100644 --- a/crates/astria-core/src/generated/mod.rs +++ b/crates/astria-core/src/generated/mod.rs @@ -188,6 +188,12 @@ pub mod astria { #[path = "astria.composer.v1.rs"] pub mod v1; } + + #[path = ""] + pub mod signer { + #[path = "astria.signer.v1.rs"] + pub mod v1; + } } #[path = ""] diff --git a/crates/astria-core/src/lib.rs b/crates/astria-core/src/lib.rs index fbf02402dc..033cedef57 100644 --- a/crates/astria-core/src/lib.rs +++ b/crates/astria-core/src/lib.rs @@ -18,6 +18,7 @@ pub mod execution; pub mod primitive; pub mod protocol; pub mod sequencerblock; +pub mod signer; #[cfg(feature = "brotli")] pub mod brotli; diff --git a/crates/astria-core/src/signer/mod.rs b/crates/astria-core/src/signer/mod.rs new file mode 100644 index 0000000000..a3a6d96c3f --- /dev/null +++ b/crates/astria-core/src/signer/mod.rs @@ -0,0 +1 @@ +pub mod v1; diff --git a/crates/astria-core/src/signer/v1/mod.rs b/crates/astria-core/src/signer/v1/mod.rs new file mode 100644 index 0000000000..99fb7f4ecf --- /dev/null +++ b/crates/astria-core/src/signer/v1/mod.rs @@ -0,0 +1 @@ +use crate::generated::astria::signer::v1 as raw; diff --git a/proto/signerapis/astria/signer/v1/frost.proto b/proto/signerapis/astria/signer/v1/frost.proto new file mode 100644 index 0000000000..0443861a8b --- /dev/null +++ b/proto/signerapis/astria/signer/v1/frost.proto @@ -0,0 +1,46 @@ +syntax = 'proto3'; + +package astria.signer.v1; + +import "astria/protocol/transaction/v1/transaction.proto"; + +message GetVerifyingShareRequest {} + +message GetVerifyingShareResponse { + // the verifying share (partial public key) of the participant. + // this is used for the coordinator to determine the identifier of the participant. + // TODO: do we need to verify this (ie. have the server send back a signed message + // with the verifying share)? + bytes verifying_share = 1; +} + +message CommitmentWithIdentifier { + bytes commitment = 1; + bytes participant_identifier = 2; +} + +message Part1Request {} + +message Part1Response { + bytes commitment = 1; + // required for the participant to internally track the nonce + // corresponding to the commitment. + uint32 request_identifier = 2; +} + +message Part2Request { + repeated CommitmentWithIdentifier commitments = 1; + astria.protocol.transaction.v1.TransactionBody transaction_body = 2; + uint32 request_identifier = 3; +} + +message Part2Response { + bytes signature_share = 1; +} + +service FrostParticipantService { + rpc GetVerifyingShare(GetVerifyingShareRequest) returns (GetVerifyingShareResponse) {} + rpc Part1(Part1Request) returns (Part1Response) {} + rpc Part2(Part2Request) returns (Part2Response) {} +} + From 151308c2e4b55d9e0a73671af23bcc2aa6154341 Mon Sep 17 00:00:00 2001 From: elizabeth Date: Tue, 4 Feb 2025 17:21:11 -0500 Subject: [PATCH 03/19] proto updates and update config --- .../local.env.example | 8 +++ .../src/bridge_withdrawer/mod.rs | 34 +++++++++-- .../submitter/frost_signer.rs | 58 +++++++++---------- crates/astria-bridge-withdrawer/src/config.rs | 9 +++ crates/astria-bridge-withdrawer/src/main.rs | 2 +- .../helpers/test_bridge_withdrawer.rs | 6 +- .../src/generated/astria.signer.v1.rs | 8 +-- .../src/generated/astria.signer.v1.serde.rs | 28 ++++----- crates/astria-core/src/lib.rs | 1 - crates/astria-core/src/signer/mod.rs | 1 - crates/astria-core/src/signer/v1/mod.rs | 1 - proto/signerapis/astria/signer/v1/frost.proto | 4 +- 12 files changed, 101 insertions(+), 59 deletions(-) delete mode 100644 crates/astria-core/src/signer/mod.rs delete mode 100644 crates/astria-core/src/signer/v1/mod.rs diff --git a/crates/astria-bridge-withdrawer/local.env.example b/crates/astria-bridge-withdrawer/local.env.example index 3cd1747f4c..5fde429ff4 100644 --- a/crates/astria-bridge-withdrawer/local.env.example +++ b/crates/astria-bridge-withdrawer/local.env.example @@ -31,10 +31,18 @@ ASTRIA_BRIDGE_WITHDRAWER_SEQUENCER_COMETBFT_ENDPOINT="http://127.0.0.1:26657" # Chain ID of the sequencer chain which transactions are submitted to. ASTRIA_BRIDGE_WITHDRAWER_SEQUENCER_CHAIN_ID="astria" +ASTRIA_BRIDGE_WITHDRAWER_FROST_THRESHOLD_SIGNING_ENABLED=false + # The path to the file storing the private key for the sequencer account used for signing # transactions. The file should contain a hex-encoded Ed25519 secret key. ASTRIA_BRIDGE_WITHDRAWER_SEQUENCER_KEY_PATH=/path/to/priv_sequencer_key.json +ASTRIA_BRIDGE_WITHDRAWER_FROST_MIN_SIGNERS=0 + +ASTRIA_BRIDGE_WITHDRAWER_FROST_PUBLIC_KEY_PACKAGE_PATH="" + +ASTRIA_BRIDGE_WITHDRAWER_FROST_PARTICIPANT_ENDPOINTS=[] + # The prefix that will be used to construct bech32m sequencer addresses. ASTRIA_BRIDGE_WITHDRAWER_SEQUENCER_ADDRESS_PREFIX=astria diff --git a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/mod.rs b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/mod.rs index a470f5bfd5..e98c717bdd 100644 --- a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/mod.rs +++ b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/mod.rs @@ -18,6 +18,7 @@ use ethereum::watcher::Watcher; use http::Uri; use hyper::server::conn::AddrIncoming; use startup::Startup; +use submitter::signer::Signer; use tokio::{ select, sync::oneshot::{ @@ -36,6 +37,8 @@ use tracing::{ info, instrument, }; +use crate::bridge_withdrawer::submitter::frost_signer::initialize_frost_participant_clients; +use crate::bridge_withdrawer::submitter::frost_signer::FrostSignerBuilder; pub(crate) use self::state::StateSnapshot; use self::{ @@ -71,13 +74,17 @@ impl BridgeWithdrawer { /// # Errors /// /// - If the provided `api_addr` string cannot be parsed as a socket address. - pub fn new(cfg: Config, metrics: &'static Metrics) -> eyre::Result<(Self, ShutdownHandle)> { + pub async fn new(cfg: Config, metrics: &'static Metrics) -> eyre::Result<(Self, ShutdownHandle)> { let shutdown_handle = ShutdownHandle::new(); let Config { api_addr, sequencer_cometbft_endpoint, sequencer_chain_id, + frost_threshold_signing_enabled, sequencer_key_path, + frost_min_signers, + frost_public_key_package_path, + frost_participant_endpoints, sequencer_address_prefix, fee_asset_denomination, ethereum_contract_address, @@ -119,18 +126,37 @@ impl BridgeWithdrawer { let startup_handle = startup::InfoHandle::new(state.subscribe()); // make submitter object - let signer = crate::bridge_withdrawer::submitter::signer::SequencerKey::builder() + let signer: Box = if frost_threshold_signing_enabled { + let public_key_package_str= std::fs::read_to_string(frost_public_key_package_path) + .wrap_err("failed to read frost public key package")?; + let public_key_package = + serde_json::from_str::(&public_key_package_str) + .wrap_err("failed to deserialize public key package")?; + + let participant_clients = initialize_frost_participant_clients(frost_participant_endpoints, &public_key_package) + .await + .wrap_err("failed to initialize frost participant clients")?; + Box::new(FrostSignerBuilder::new() + .min_signers(frost_min_signers) + .public_key_package(public_key_package) + .participant_clients(participant_clients) + .address_prefix(sequencer_address_prefix) + .try_build() + .wrap_err("failed to initialize frost signer")?) + } else { + Box::new(crate::bridge_withdrawer::submitter::signer::SequencerKey::builder() .path(sequencer_key_path) .prefix(sequencer_address_prefix) .try_build() - .wrap_err("failed to load sequencer private key")?; + .wrap_err("failed to load sequencer private key")?) + }; let (submitter, submitter_handle) = submitter::Builder { shutdown_token: shutdown_handle.token(), startup_handle: startup_handle.clone(), sequencer_cometbft_client, sequencer_grpc_client, - signer: Box::new(signer), + signer, state: state.clone(), metrics, } diff --git a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/frost_signer.rs b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/frost_signer.rs index 7a1e155f87..0fa3d7fdc4 100644 --- a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/frost_signer.rs +++ b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/frost_signer.rs @@ -5,15 +5,12 @@ use std::collections::{ use astria_core::{ crypto::VerificationKey, - generated::astria::{ - protocol::transaction, - signer::v1::{ - frost_participant_service_client::FrostParticipantServiceClient, - CommitmentWithIdentifier, - GetVerifyingShareRequest, - Part1Request, - Part2Request, - }, + generated::astria::signer::v1::{ + frost_participant_service_client::FrostParticipantServiceClient, + CommitmentWithIdentifier, + GetVerifyingShareRequest, + Part1Request, + Part2Request, }, primitive::v1::Address, protocol::transaction::v1::{ @@ -28,10 +25,8 @@ use astria_eyre::eyre::{ eyre, Context, }; -use ethers::types::Sign; use frost_ed25519::{ keys::{ - KeyPackage, PublicKeyPackage, VerifyingShare, }, @@ -43,7 +38,7 @@ use super::Signer; pub(crate) async fn initialize_frost_participant_clients( endpoints: Vec, - public_key_package: PublicKeyPackage, + public_key_package: &PublicKeyPackage, ) -> eyre::Result>> { // TODO: maybe remove this check, and just check we have >= min_signers in build() ensure!( @@ -80,7 +75,7 @@ pub(crate) async fn initialize_frost_participant_clients( } pub(crate) struct FrostSignerBuilder { - key_package: Option, + min_signers: Option, public_key_package: Option, address_prefix: Option, participant_clients: @@ -88,9 +83,18 @@ pub(crate) struct FrostSignerBuilder { } impl FrostSignerBuilder { - pub(crate) fn key_package(self, key_package: KeyPackage) -> Self { + pub(crate) fn new() -> Self { + Self { + min_signers: None, + public_key_package: None, + address_prefix: None, + participant_clients: HashMap::new(), + } + } + + pub(crate) fn min_signers(self, min_signers: usize) -> Self { Self { - key_package: Some(key_package), + min_signers: Some(min_signers), ..self } } @@ -123,9 +127,9 @@ impl FrostSignerBuilder { } pub(crate) fn try_build(self) -> eyre::Result { - let key_package = self - .key_package - .ok_or_else(|| eyre!("key package is required"))?; + let min_signers = self + .min_signers + .ok_or_else(|| eyre!("minimum number of signers is required"))?; let public_key_package = self .public_key_package .ok_or_else(|| eyre!("public key package is required"))?; @@ -147,7 +151,7 @@ impl FrostSignerBuilder { .wrap_err("failed to build address")?; Ok(FrostSigner { - key_package, + min_signers, public_key_package, address, participant_clients: self.participant_clients, @@ -156,7 +160,7 @@ impl FrostSignerBuilder { } pub(crate) struct FrostSigner { - key_package: KeyPackage, + min_signers: usize, public_key_package: PublicKeyPackage, address: Address, participant_clients: @@ -172,10 +176,6 @@ impl Signer for FrostSigner { Name as _, }; - // TODO: -1 for min_other_signers; or do we want all participants - // to be separate processes? - let min_signers: usize = (*self.key_package.min_signers()).into(); - // part 1 let stream = futures::stream::FuturesUnordered::new(); for (id, client) in &self.participant_clients { @@ -202,7 +202,7 @@ impl Signer for FrostSigner { }; } ensure!( - commitments.len() >= min_signers, + commitments.len() >= self.min_signers, "not enough part 1 commitments" ); @@ -223,14 +223,12 @@ impl Signer for FrostSigner { .ok_or_else(|| eyre!("failed to find participant client"))? .clone(); let request_commitments = request_commitments.clone(); - let tx = tx.clone(); + let tx_bytes = tx_bytes.clone(); stream.push(async move { let resp = client .part2(Part2Request { request_identifier, - transaction_body: Some(tx.into_raw()), /* TODO: this needs to - * be bytes for - * determinism */ + message: tx_bytes.into(), commitments: request_commitments, }) .await @@ -254,7 +252,7 @@ impl Signer for FrostSigner { }; } ensure!( - sig_shares.len() >= min_signers, + sig_shares.len() >= self.min_signers, "not enough part 2 signature shares" ); diff --git a/crates/astria-bridge-withdrawer/src/config.rs b/crates/astria-bridge-withdrawer/src/config.rs index b01ea609b7..251a8755f3 100644 --- a/crates/astria-bridge-withdrawer/src/config.rs +++ b/crates/astria-bridge-withdrawer/src/config.rs @@ -18,8 +18,17 @@ pub struct Config { pub sequencer_cometbft_endpoint: String, // The chain id of the sequencer chain. pub sequencer_chain_id: String, + // Set to true to enable frost threshold signing. + pub frost_threshold_signing_enabled: bool, // The path to the private key used to sign transactions submitted to the sequencer. + // Only used if `frost_threshold_signing_enabled` is false. pub sequencer_key_path: String, + // The minimum number of frost participants required to sign a transaction. + pub frost_min_signers: usize, + // The path to the json-encoded frost public key package. + pub frost_public_key_package_path: String, + // The frost participant gRPC endpoints. + pub frost_participant_endpoints: Vec, // The fee asset denomination to use for the bridge account's transactions. pub fee_asset_denomination: asset::Denom, // The asset denomination being withdrawn from the rollup. diff --git a/crates/astria-bridge-withdrawer/src/main.rs b/crates/astria-bridge-withdrawer/src/main.rs index 546ca608b8..720407d4df 100644 --- a/crates/astria-bridge-withdrawer/src/main.rs +++ b/crates/astria-bridge-withdrawer/src/main.rs @@ -54,7 +54,7 @@ async fn main() -> ExitCode { let mut sigterm = signal(SignalKind::terminate()) .expect("setting a SIGTERM listener should always work on Unix"); - let (withdrawer, shutdown_handle) = match BridgeWithdrawer::new(cfg, metrics) { + let (withdrawer, shutdown_handle) = match BridgeWithdrawer::new(cfg, metrics).await { Err(error) => { error!(%error, "failed initializing bridge withdrawer"); return ExitCode::FAILURE; diff --git a/crates/astria-bridge-withdrawer/tests/blackbox/helpers/test_bridge_withdrawer.rs b/crates/astria-bridge-withdrawer/tests/blackbox/helpers/test_bridge_withdrawer.rs index 154c409e87..c9274572a1 100644 --- a/crates/astria-bridge-withdrawer/tests/blackbox/helpers/test_bridge_withdrawer.rs +++ b/crates/astria-bridge-withdrawer/tests/blackbox/helpers/test_bridge_withdrawer.rs @@ -272,6 +272,10 @@ impl TestBridgeWithdrawerConfig { sequencer_grpc_endpoint: format!("http://{}", sequencer_mock.local_addr), sequencer_chain_id: SEQUENCER_CHAIN_ID.into(), sequencer_key_path, + frost_threshold_signing_enabled: false, + frost_min_signers: 0, + frost_public_key_package_path: String::new(), + frost_participant_endpoints: Vec::new(), fee_asset_denomination: asset_denom.clone(), rollup_asset_denomination: asset_denom.as_trace_prefixed().unwrap().clone(), sequencer_bridge_address: default_bridge_address().to_string(), @@ -297,7 +301,7 @@ impl TestBridgeWithdrawerConfig { let metrics = Box::leak(Box::new(metrics)); let (bridge_withdrawer, bridge_withdrawer_shutdown_handle) = - BridgeWithdrawer::new(config.clone(), metrics).unwrap(); + BridgeWithdrawer::new(config.clone(), metrics).await.unwrap(); let api_address = bridge_withdrawer.local_addr(); let bridge_withdrawer = tokio::task::spawn(bridge_withdrawer.run()); diff --git a/crates/astria-core/src/generated/astria.signer.v1.rs b/crates/astria-core/src/generated/astria.signer.v1.rs index 76746e8e74..1231eb059b 100644 --- a/crates/astria-core/src/generated/astria.signer.v1.rs +++ b/crates/astria-core/src/generated/astria.signer.v1.rs @@ -13,6 +13,8 @@ impl ::prost::Name for GetVerifyingShareRequest { pub struct GetVerifyingShareResponse { /// the verifying share (partial public key) of the participant. /// this is used for the coordinator to determine the identifier of the participant. + /// TODO: do we need to verify this (ie. have the server send back a signed message + /// with the verifying share)? #[prost(bytes = "bytes", tag = "1")] pub verifying_share: ::prost::bytes::Bytes, } @@ -70,10 +72,8 @@ impl ::prost::Name for Part1Response { pub struct Part2Request { #[prost(message, repeated, tag = "1")] pub commitments: ::prost::alloc::vec::Vec, - #[prost(message, optional, tag = "2")] - pub transaction_body: ::core::option::Option< - super::super::protocol::transaction::v1::TransactionBody, - >, + #[prost(bytes = "bytes", tag = "2")] + pub message: ::prost::bytes::Bytes, #[prost(uint32, tag = "3")] pub request_identifier: u32, } diff --git a/crates/astria-core/src/generated/astria.signer.v1.serde.rs b/crates/astria-core/src/generated/astria.signer.v1.serde.rs index 9b91af5104..3fb4ce22e5 100644 --- a/crates/astria-core/src/generated/astria.signer.v1.serde.rs +++ b/crates/astria-core/src/generated/astria.signer.v1.serde.rs @@ -475,7 +475,7 @@ impl serde::Serialize for Part2Request { if !self.commitments.is_empty() { len += 1; } - if self.transaction_body.is_some() { + if !self.message.is_empty() { len += 1; } if self.request_identifier != 0 { @@ -485,8 +485,9 @@ impl serde::Serialize for Part2Request { if !self.commitments.is_empty() { struct_ser.serialize_field("commitments", &self.commitments)?; } - if let Some(v) = self.transaction_body.as_ref() { - struct_ser.serialize_field("transactionBody", v)?; + if !self.message.is_empty() { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("message", pbjson::private::base64::encode(&self.message).as_str())?; } if self.request_identifier != 0 { struct_ser.serialize_field("requestIdentifier", &self.request_identifier)?; @@ -502,8 +503,7 @@ impl<'de> serde::Deserialize<'de> for Part2Request { { const FIELDS: &[&str] = &[ "commitments", - "transaction_body", - "transactionBody", + "message", "request_identifier", "requestIdentifier", ]; @@ -511,7 +511,7 @@ impl<'de> serde::Deserialize<'de> for Part2Request { #[allow(clippy::enum_variant_names)] enum GeneratedField { Commitments, - TransactionBody, + Message, RequestIdentifier, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -535,7 +535,7 @@ impl<'de> serde::Deserialize<'de> for Part2Request { { match value { "commitments" => Ok(GeneratedField::Commitments), - "transactionBody" | "transaction_body" => Ok(GeneratedField::TransactionBody), + "message" => Ok(GeneratedField::Message), "requestIdentifier" | "request_identifier" => Ok(GeneratedField::RequestIdentifier), _ => Err(serde::de::Error::unknown_field(value, FIELDS)), } @@ -557,7 +557,7 @@ impl<'de> serde::Deserialize<'de> for Part2Request { V: serde::de::MapAccess<'de>, { let mut commitments__ = None; - let mut transaction_body__ = None; + let mut message__ = None; let mut request_identifier__ = None; while let Some(k) = map_.next_key()? { match k { @@ -567,11 +567,13 @@ impl<'de> serde::Deserialize<'de> for Part2Request { } commitments__ = Some(map_.next_value()?); } - GeneratedField::TransactionBody => { - if transaction_body__.is_some() { - return Err(serde::de::Error::duplicate_field("transactionBody")); + GeneratedField::Message => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("message")); } - transaction_body__ = map_.next_value()?; + message__ = + Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) + ; } GeneratedField::RequestIdentifier => { if request_identifier__.is_some() { @@ -585,7 +587,7 @@ impl<'de> serde::Deserialize<'de> for Part2Request { } Ok(Part2Request { commitments: commitments__.unwrap_or_default(), - transaction_body: transaction_body__, + message: message__.unwrap_or_default(), request_identifier: request_identifier__.unwrap_or_default(), }) } diff --git a/crates/astria-core/src/lib.rs b/crates/astria-core/src/lib.rs index 033cedef57..fbf02402dc 100644 --- a/crates/astria-core/src/lib.rs +++ b/crates/astria-core/src/lib.rs @@ -18,7 +18,6 @@ pub mod execution; pub mod primitive; pub mod protocol; pub mod sequencerblock; -pub mod signer; #[cfg(feature = "brotli")] pub mod brotli; diff --git a/crates/astria-core/src/signer/mod.rs b/crates/astria-core/src/signer/mod.rs deleted file mode 100644 index a3a6d96c3f..0000000000 --- a/crates/astria-core/src/signer/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod v1; diff --git a/crates/astria-core/src/signer/v1/mod.rs b/crates/astria-core/src/signer/v1/mod.rs deleted file mode 100644 index 99fb7f4ecf..0000000000 --- a/crates/astria-core/src/signer/v1/mod.rs +++ /dev/null @@ -1 +0,0 @@ -use crate::generated::astria::signer::v1 as raw; diff --git a/proto/signerapis/astria/signer/v1/frost.proto b/proto/signerapis/astria/signer/v1/frost.proto index 0443861a8b..f8b60515c0 100644 --- a/proto/signerapis/astria/signer/v1/frost.proto +++ b/proto/signerapis/astria/signer/v1/frost.proto @@ -2,8 +2,6 @@ syntax = 'proto3'; package astria.signer.v1; -import "astria/protocol/transaction/v1/transaction.proto"; - message GetVerifyingShareRequest {} message GetVerifyingShareResponse { @@ -30,7 +28,7 @@ message Part1Response { message Part2Request { repeated CommitmentWithIdentifier commitments = 1; - astria.protocol.transaction.v1.TransactionBody transaction_body = 2; + bytes message = 2; uint32 request_identifier = 3; } From d273726976b1867c0384d73a961a59e4b6b2aa2f Mon Sep 17 00:00:00 2001 From: elizabeth Date: Wed, 5 Feb 2025 13:20:34 -0500 Subject: [PATCH 04/19] clippy --- .../local.env.example | 8 + .../src/bridge_withdrawer/mod.rs | 88 ++++--- .../bridge_withdrawer/submitter/builder.rs | 6 +- .../submitter/frost_signer.rs | 230 ++++++++++++------ .../helpers/test_bridge_withdrawer.rs | 4 +- 5 files changed, 225 insertions(+), 111 deletions(-) diff --git a/crates/astria-bridge-withdrawer/local.env.example b/crates/astria-bridge-withdrawer/local.env.example index 5fde429ff4..214025a241 100644 --- a/crates/astria-bridge-withdrawer/local.env.example +++ b/crates/astria-bridge-withdrawer/local.env.example @@ -31,16 +31,24 @@ ASTRIA_BRIDGE_WITHDRAWER_SEQUENCER_COMETBFT_ENDPOINT="http://127.0.0.1:26657" # Chain ID of the sequencer chain which transactions are submitted to. ASTRIA_BRIDGE_WITHDRAWER_SEQUENCER_CHAIN_ID="astria" +# Set to true to enable frost threshold signing. ASTRIA_BRIDGE_WITHDRAWER_FROST_THRESHOLD_SIGNING_ENABLED=false # The path to the file storing the private key for the sequencer account used for signing # transactions. The file should contain a hex-encoded Ed25519 secret key. +# Only used if `frost_threshold_signing_enabled` is false. ASTRIA_BRIDGE_WITHDRAWER_SEQUENCER_KEY_PATH=/path/to/priv_sequencer_key.json +# The minimum number of frost participants required to sign a transaction. +# Only used if `frost_threshold_signing_enabled` is true. ASTRIA_BRIDGE_WITHDRAWER_FROST_MIN_SIGNERS=0 +# The path to the json-encoded frost public key package. +# Only used if `frost_threshold_signing_enabled` is true. ASTRIA_BRIDGE_WITHDRAWER_FROST_PUBLIC_KEY_PACKAGE_PATH="" +# The frost participant gRPC endpoints. +# Only used if `frost_threshold_signing_enabled` is true. ASTRIA_BRIDGE_WITHDRAWER_FROST_PARTICIPANT_ENDPOINTS=[] # The prefix that will be used to construct bech32m sequencer addresses. diff --git a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/mod.rs b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/mod.rs index e98c717bdd..2113c7e8b8 100644 --- a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/mod.rs +++ b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/mod.rs @@ -37,8 +37,6 @@ use tracing::{ info, instrument, }; -use crate::bridge_withdrawer::submitter::frost_signer::initialize_frost_participant_clients; -use crate::bridge_withdrawer::submitter::frost_signer::FrostSignerBuilder; pub(crate) use self::state::StateSnapshot; use self::{ @@ -48,6 +46,10 @@ use self::{ }; use crate::{ api, + bridge_withdrawer::submitter::frost_signer::{ + initialize_frost_participant_clients, + FrostSignerBuilder, + }, config::Config, metrics::Metrics, }; @@ -74,7 +76,10 @@ impl BridgeWithdrawer { /// # Errors /// /// - If the provided `api_addr` string cannot be parsed as a socket address. - pub async fn new(cfg: Config, metrics: &'static Metrics) -> eyre::Result<(Self, ShutdownHandle)> { + pub async fn new( + cfg: Config, + metrics: &'static Metrics, + ) -> eyre::Result<(Self, ShutdownHandle)> { let shutdown_handle = ShutdownHandle::new(); let Config { api_addr, @@ -126,30 +131,16 @@ impl BridgeWithdrawer { let startup_handle = startup::InfoHandle::new(state.subscribe()); // make submitter object - let signer: Box = if frost_threshold_signing_enabled { - let public_key_package_str= std::fs::read_to_string(frost_public_key_package_path) - .wrap_err("failed to read frost public key package")?; - let public_key_package = - serde_json::from_str::(&public_key_package_str) - .wrap_err("failed to deserialize public key package")?; - - let participant_clients = initialize_frost_participant_clients(frost_participant_endpoints, &public_key_package) - .await - .wrap_err("failed to initialize frost participant clients")?; - Box::new(FrostSignerBuilder::new() - .min_signers(frost_min_signers) - .public_key_package(public_key_package) - .participant_clients(participant_clients) - .address_prefix(sequencer_address_prefix) - .try_build() - .wrap_err("failed to initialize frost signer")?) - } else { - Box::new(crate::bridge_withdrawer::submitter::signer::SequencerKey::builder() - .path(sequencer_key_path) - .prefix(sequencer_address_prefix) - .try_build() - .wrap_err("failed to load sequencer private key")?) - }; + let signer = make_signer( + frost_threshold_signing_enabled, + frost_min_signers, + frost_public_key_package_path, + frost_participant_endpoints, + sequencer_key_path, + sequencer_address_prefix, + ) + .await + .wrap_err("failed to create signer")?; let (submitter, submitter_handle) = submitter::Builder { shutdown_token: shutdown_handle.token(), @@ -160,8 +151,7 @@ impl BridgeWithdrawer { state: state.clone(), metrics, } - .build() - .wrap_err("failed to initialize submitter")?; + .build(); let ethereum_watcher = watcher::Builder { ethereum_contract_address, @@ -291,6 +281,46 @@ impl BridgeWithdrawer { } } +async fn make_signer( + frost_threshold_signing_enabled: bool, + frost_min_signers: usize, + frost_public_key_package_path: String, + frost_participant_endpoints: Vec, + sequencer_key_path: String, + sequencer_address_prefix: String, +) -> eyre::Result> { + let signer: Box = if frost_threshold_signing_enabled { + let public_key_package_str = std::fs::read_to_string(frost_public_key_package_path) + .wrap_err("failed to read frost public key package")?; + let public_key_package = + serde_json::from_str::(&public_key_package_str) + .wrap_err("failed to deserialize public key package")?; + + let participant_clients = + initialize_frost_participant_clients(frost_participant_endpoints, &public_key_package) + .await + .wrap_err("failed to initialize frost participant clients")?; + Box::new( + FrostSignerBuilder::new() + .min_signers(frost_min_signers) + .public_key_package(public_key_package) + .participant_clients(participant_clients) + .address_prefix(sequencer_address_prefix) + .try_build() + .wrap_err("failed to initialize frost signer")?, + ) + } else { + Box::new( + crate::bridge_withdrawer::submitter::signer::SequencerKey::builder() + .path(sequencer_key_path) + .prefix(sequencer_address_prefix) + .try_build() + .wrap_err("failed to load sequencer private key")?, + ) + }; + Ok(signer) +} + #[expect( clippy::struct_field_names, reason = "for parity with the `Shutdown` struct" diff --git a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/builder.rs b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/builder.rs index a4326537be..9394f6bd3c 100644 --- a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/builder.rs +++ b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/builder.rs @@ -58,7 +58,7 @@ pub(crate) struct Builder { impl Builder { /// Instantiates an `Submitter`. - pub(crate) fn build(self) -> eyre::Result<(super::Submitter, Handle)> { + pub(crate) fn build(self) -> (super::Submitter, Handle) { let Self { shutdown_token, startup_handle, @@ -74,7 +74,7 @@ impl Builder { let (batches_tx, batches_rx) = tokio::sync::mpsc::channel(BATCH_QUEUE_SIZE); let handle = Handle::new(batches_tx); - Ok(( + ( super::Submitter { shutdown_token, startup_handle, @@ -86,6 +86,6 @@ impl Builder { metrics, }, handle, - )) + ) } } diff --git a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/frost_signer.rs b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/frost_signer.rs index 0fa3d7fdc4..d2a9a2ee2b 100644 --- a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/frost_signer.rs +++ b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/frost_signer.rs @@ -33,6 +33,11 @@ use frost_ed25519::{ round1, Identifier, }; +use futures::StreamExt as _; +use prost::{ + Message as _, + Name as _, +}; use super::Signer; @@ -40,12 +45,6 @@ pub(crate) async fn initialize_frost_participant_clients( endpoints: Vec, public_key_package: &PublicKeyPackage, ) -> eyre::Result>> { - // TODO: maybe remove this check, and just check we have >= min_signers in build() - ensure!( - endpoints.len() == public_key_package.verifying_shares().len(), - "number of endpoints does not match number of participants" - ); - let mut participant_clients = HashMap::new(); for endpoint in endpoints { let mut client = FrostParticipantServiceClient::connect(endpoint) @@ -150,6 +149,11 @@ impl FrostSignerBuilder { .try_build() .wrap_err("failed to build address")?; + ensure!( + self.participant_clients.len() == min_signers, + "not enough participant clients; need at least {min_signers}" + ); + Ok(FrostSigner { min_signers, public_key_package, @@ -170,57 +174,160 @@ pub(crate) struct FrostSigner { #[tonic::async_trait] impl Signer for FrostSigner { async fn sign(&self, tx: TransactionBody) -> eyre::Result { - use futures::StreamExt as _; - use prost::{ - Message as _, - Name as _, - }; + // part 1: gather commitments from participants + let (commitments, signing_package_commitments) = self.frost_part_1().await; + ensure!( + commitments.len() >= self.min_signers, + "not enough part 1 commitments received; want at least {}, got {}", + self.min_signers, + commitments.len() + ); + + // part 2: get signature shares from participants + // let stream = futures::stream::FuturesUnordered::new(); + // let request_commitments: Vec = commitments + // .iter() + // .map(|(id, commitment, _)| CommitmentWithIdentifier { + // commitment: commitment.clone(), + // participant_identifier: id.serialize().into(), + // }) + // .collect(); + let tx_bytes = tx.to_raw().encode_to_vec(); + let sig_shares = self.frost_part_2(commitments, tx_bytes.clone()).await; + // for (id, _, request_identifier) in commitments { + // let mut client = self + // .participant_clients + // .get(&id) + // .ok_or_else(|| eyre!("failed to find participant client"))? + // .clone(); + // let request_commitments = request_commitments.clone(); + // let tx_bytes = tx_bytes.clone(); + // stream.push(async move { + // let resp = client + // .part2(Part2Request { + // request_identifier, + // message: tx_bytes.into(), + // commitments: request_commitments, + // }) + // .await + // .wrap_err(format!( + // "failed to get part 2 response for participant with id {id:?}" + // ))?; + // Ok((id, resp.into_inner())) + // }); + // } + // let results: Vec> = stream.collect::>().await; + // let sig_shares: BTreeMap = results + // .into_iter() + // .filter_map(|res| match res { + // Ok((id, part2)) => { + // let sig_share = + // + // frost_ed25519::round2::SignatureShare::deserialize(&part2.signature_share) + // .ok()?; + // Some((id, sig_share)) + // } + // Err(_) => None, + // }) + // .collect(); - // part 1 + ensure!( + sig_shares.len() >= self.min_signers, + "not enough part 2 signature shares received; want at least {}, got {}", + self.min_signers, + sig_shares.len() + ); + + // finally, aggregate and create signature + let signing_package = + frost_ed25519::SigningPackage::new(signing_package_commitments, &tx_bytes); + let signature = + frost_ed25519::aggregate(&signing_package, &sig_shares, &self.public_key_package) + .wrap_err("failed to aggregate signature shares")?; + + let raw_transaction = astria_core::generated::astria::protocol::transaction::v1::Transaction { + body: Some(pbjson_types::Any { + type_url: astria_core::generated::astria::protocol::transaction::v1::TransactionBody::type_url(), + value: tx_bytes.into(), + }), + signature: signature + .serialize() + .wrap_err("failed to serialize signature")? + .into(), + public_key: self.public_key_package + .verifying_key() + .serialize() + .wrap_err("failed to serialize verifying key")? + .into(), + }; + let transaction = Transaction::try_from_raw(raw_transaction) + .wrap_err("failed to convert raw transaction to transaction")?; + + Ok(transaction) + } + + fn address(&self) -> &Address { + &self.address + } +} + +impl FrostSigner { + async fn frost_part_1( + &self, + ) -> ( + Vec<(Identifier, axum::body::Bytes, u32)>, + BTreeMap, + ) { let stream = futures::stream::FuturesUnordered::new(); for (id, client) in &self.participant_clients { let mut client = client.clone(); stream.push(async move { let resp = client.part1(Part1Request {}).await.wrap_err(format!( - "failed to get part 1 commitment for participant with id {:?}", - id + "failed to get part 1 response for participant with id {id:?}" ))?; Ok((id, resp.into_inner())) }); } let results: Vec> = stream.collect::>().await; - let mut commitments = Vec::new(); let mut signing_package_commitments: BTreeMap = BTreeMap::new(); - for res in results { - if let Ok((id, part1)) = res { - let signing_commitment = round1::SigningCommitments::deserialize(&part1.commitment) - .wrap_err("failed to deserialize signing commitment")?; - signing_package_commitments.insert(*id, signing_commitment); - commitments.push((id, part1.commitment, part1.request_identifier)); - }; - } - ensure!( - commitments.len() >= self.min_signers, - "not enough part 1 commitments" - ); + let commitments = results + .into_iter() + .filter_map(|res| match res { + Ok((id, part1)) => { + let signing_commitment = + round1::SigningCommitments::deserialize(&part1.commitment).ok()?; + signing_package_commitments.insert(*id, signing_commitment); + Some((*id, part1.commitment, part1.request_identifier)) + } + Err(_) => None, + }) + .collect::>(); + (commitments, signing_package_commitments) + } - // part 2 + async fn frost_part_2( + &self, + commitments: Vec<(Identifier, axum::body::Bytes, u32)>, + tx_bytes: Vec, + ) -> BTreeMap { let stream = futures::stream::FuturesUnordered::new(); let request_commitments: Vec = commitments .iter() .map(|(id, commitment, _)| CommitmentWithIdentifier { commitment: commitment.clone(), - participant_identifier: id.serialize().to_vec().into(), + participant_identifier: id.serialize().into(), }) .collect(); - let tx_bytes = tx.to_raw().encode_to_vec(); for (id, _, request_identifier) in commitments { let mut client = self .participant_clients .get(&id) - .ok_or_else(|| eyre!("failed to find participant client"))? + .expect( + "participant client must exist in mapping, as we received a commitment from \ + them in part 1, meaning we already have their client", + ) .clone(); let request_commitments = request_commitments.clone(); let tx_bytes = tx_bytes.clone(); @@ -233,58 +340,25 @@ impl Signer for FrostSigner { }) .await .wrap_err(format!( - "failed to get part 2 response for participant with id {:?}", - id + "failed to get part 2 response for participant with id {id:?}" ))?; Ok((id, resp.into_inner())) }); } let results: Vec> = stream.collect::>().await; - let mut sig_shares: BTreeMap = - BTreeMap::new(); - for res in results { - if let Ok((id, part2)) = res { - sig_shares.insert( - *id, - frost_ed25519::round2::SignatureShare::deserialize(&part2.signature_share) - .wrap_err("failed to deserialize signature share")?, - ); - }; - } - ensure!( - sig_shares.len() >= self.min_signers, - "not enough part 2 signature shares" - ); - - // aggregate and create signature - let signing_package = - frost_ed25519::SigningPackage::new(signing_package_commitments, &tx_bytes); - let signature = - frost_ed25519::aggregate(&signing_package, &sig_shares, &self.public_key_package) - .wrap_err("failed to aggregate")?; - - let raw_transaction = astria_core::generated::astria::protocol::transaction::v1::Transaction { - body: Some(pbjson_types::Any { - type_url: astria_core::generated::astria::protocol::transaction::v1::TransactionBody::type_url(), - value: tx_bytes.into(), - }), - signature: signature - .serialize() - .wrap_err("failed to serialize signature")? - .into(), - public_key: self.public_key_package - .verifying_key() - .serialize() - .wrap_err("failed to serialize verifying key")? - .into(), - }; - let transaction = Transaction::try_from_raw(raw_transaction) - .wrap_err("failed to convert to transaction")?; - - Ok(transaction) - } + let sig_shares: BTreeMap = results + .into_iter() + .filter_map(|res| match res { + Ok((id, part2)) => { + let sig_share = + frost_ed25519::round2::SignatureShare::deserialize(&part2.signature_share) + .ok()?; + Some((id, sig_share)) + } + Err(_) => None, + }) + .collect(); - fn address(&self) -> &Address { - &self.address + sig_shares } } diff --git a/crates/astria-bridge-withdrawer/tests/blackbox/helpers/test_bridge_withdrawer.rs b/crates/astria-bridge-withdrawer/tests/blackbox/helpers/test_bridge_withdrawer.rs index c9274572a1..17c8d7e3a5 100644 --- a/crates/astria-bridge-withdrawer/tests/blackbox/helpers/test_bridge_withdrawer.rs +++ b/crates/astria-bridge-withdrawer/tests/blackbox/helpers/test_bridge_withdrawer.rs @@ -301,7 +301,9 @@ impl TestBridgeWithdrawerConfig { let metrics = Box::leak(Box::new(metrics)); let (bridge_withdrawer, bridge_withdrawer_shutdown_handle) = - BridgeWithdrawer::new(config.clone(), metrics).await.unwrap(); + BridgeWithdrawer::new(config.clone(), metrics) + .await + .unwrap(); let api_address = bridge_withdrawer.local_addr(); let bridge_withdrawer = tokio::task::spawn(bridge_withdrawer.run()); From aae55ed137d628944bbbc6c3e7d8600b3b0d9ecc Mon Sep 17 00:00:00 2001 From: elizabeth Date: Wed, 5 Feb 2025 13:22:33 -0500 Subject: [PATCH 05/19] update chart --- charts/evm-bridge-withdrawer/Chart.yaml | 2 +- charts/evm-bridge-withdrawer/templates/configmaps.yaml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/charts/evm-bridge-withdrawer/Chart.yaml b/charts/evm-bridge-withdrawer/Chart.yaml index 525e4ae385..9a6b301a8d 100644 --- a/charts/evm-bridge-withdrawer/Chart.yaml +++ b/charts/evm-bridge-withdrawer/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.0.1 +version: 1.0.2 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/charts/evm-bridge-withdrawer/templates/configmaps.yaml b/charts/evm-bridge-withdrawer/templates/configmaps.yaml index 4af64ab8eb..79b9f50e47 100644 --- a/charts/evm-bridge-withdrawer/templates/configmaps.yaml +++ b/charts/evm-bridge-withdrawer/templates/configmaps.yaml @@ -34,6 +34,10 @@ data: OTEL_SERVICE_NAME: "{{ tpl .Values.otel.serviceName . }}" {{- if not .Values.global.dev }} {{- else }} + ASTRIA_BRIDGE_WITHDRAWER_FROST_THRESHOLD_SIGNING_ENABLED="false" + ASTRIA_BRIDGE_WITHDRAWER_FROST_MIN_SIGNERS="0" + ASTRIA_BRIDGE_WITHDRAWER_FROST_PUBLIC_KEY_PACKAGE_PATH="" + ASTRIA_BRIDGE_WITHDRAWER_FROST_PARTICIPANT_ENDPOINTS="[]" {{- end }} --- {{- if not .Values.secretProvider.enabled }} From 1e73f5db7468923756384f1284a075bc6de13679 Mon Sep 17 00:00:00 2001 From: elizabeth Date: Thu, 6 Feb 2025 14:36:33 -0500 Subject: [PATCH 06/19] cleanup --- .../submitter/frost_signer.rs | 46 ------------------- crates/astria-bridge-withdrawer/src/main.rs | 4 +- 2 files changed, 2 insertions(+), 48 deletions(-) diff --git a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/frost_signer.rs b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/frost_signer.rs index d2a9a2ee2b..2bd92f99ef 100644 --- a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/frost_signer.rs +++ b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/frost_signer.rs @@ -183,54 +183,8 @@ impl Signer for FrostSigner { commitments.len() ); - // part 2: get signature shares from participants - // let stream = futures::stream::FuturesUnordered::new(); - // let request_commitments: Vec = commitments - // .iter() - // .map(|(id, commitment, _)| CommitmentWithIdentifier { - // commitment: commitment.clone(), - // participant_identifier: id.serialize().into(), - // }) - // .collect(); let tx_bytes = tx.to_raw().encode_to_vec(); let sig_shares = self.frost_part_2(commitments, tx_bytes.clone()).await; - // for (id, _, request_identifier) in commitments { - // let mut client = self - // .participant_clients - // .get(&id) - // .ok_or_else(|| eyre!("failed to find participant client"))? - // .clone(); - // let request_commitments = request_commitments.clone(); - // let tx_bytes = tx_bytes.clone(); - // stream.push(async move { - // let resp = client - // .part2(Part2Request { - // request_identifier, - // message: tx_bytes.into(), - // commitments: request_commitments, - // }) - // .await - // .wrap_err(format!( - // "failed to get part 2 response for participant with id {id:?}" - // ))?; - // Ok((id, resp.into_inner())) - // }); - // } - // let results: Vec> = stream.collect::>().await; - // let sig_shares: BTreeMap = results - // .into_iter() - // .filter_map(|res| match res { - // Ok((id, part2)) => { - // let sig_share = - // - // frost_ed25519::round2::SignatureShare::deserialize(&part2.signature_share) - // .ok()?; - // Some((id, sig_share)) - // } - // Err(_) => None, - // }) - // .collect(); - ensure!( sig_shares.len() >= self.min_signers, "not enough part 2 signature shares received; want at least {}, got {}", diff --git a/crates/astria-bridge-withdrawer/src/main.rs b/crates/astria-bridge-withdrawer/src/main.rs index 720407d4df..2c74986765 100644 --- a/crates/astria-bridge-withdrawer/src/main.rs +++ b/crates/astria-bridge-withdrawer/src/main.rs @@ -41,7 +41,7 @@ async fn main() -> ExitCode { .wrap_err("failed to setup telemetry") { Err(e) => { - eprintln!("initializing conductor failed:\n{e:?}"); + eprintln!("initializing bridge withdrawer failed:\n{e:?}"); return ExitCode::FAILURE; } Ok(metrics_and_guard) => metrics_and_guard, @@ -49,7 +49,7 @@ async fn main() -> ExitCode { info!( config = serde_json::to_string(&cfg).expect("serializing to a string cannot fail"), - "initializing conductor" + "initializing bridge withdrawer" ); let mut sigterm = signal(SignalKind::terminate()) From 2367e61997548572a4ec15934e233909bbf703c1 Mon Sep 17 00:00:00 2001 From: elizabeth Date: Thu, 6 Feb 2025 15:55:27 -0500 Subject: [PATCH 07/19] update charts --- charts/evm-bridge-withdrawer/values.yaml | 2 +- charts/evm-stack/Chart.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/evm-bridge-withdrawer/values.yaml b/charts/evm-bridge-withdrawer/values.yaml index 4f4bbcf8da..09a08bd661 100644 --- a/charts/evm-bridge-withdrawer/values.yaml +++ b/charts/evm-bridge-withdrawer/values.yaml @@ -13,7 +13,7 @@ images: evmBridgeWithdrawer: repo: ghcr.io/astriaorg/evm-bridge-withdrawer pullPolicy: IfNotPresent - tag: 1.0.1 + tag: 1.0.2 devTag: latest config: diff --git a/charts/evm-stack/Chart.yaml b/charts/evm-stack/Chart.yaml index 17bceddce5..d2cd30a3e4 100644 --- a/charts/evm-stack/Chart.yaml +++ b/charts/evm-stack/Chart.yaml @@ -34,7 +34,7 @@ dependencies: repository: "file://../evm-faucet" condition: evm-faucet.enabled - name: evm-bridge-withdrawer - version: 1.0.1 + version: 1.0.2 repository: "file://../evm-bridge-withdrawer" condition: evm-bridge-withdrawer.enabled - name: postgresql From a6dc906c284be0a1c08feb9c24b23066c97038f8 Mon Sep 17 00:00:00 2001 From: elizabeth Date: Mon, 10 Feb 2025 12:39:08 -0500 Subject: [PATCH 08/19] update Chart.lock --- charts/evm-stack/Chart.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/evm-stack/Chart.lock b/charts/evm-stack/Chart.lock index 297cbc4735..c60fc70e4e 100644 --- a/charts/evm-stack/Chart.lock +++ b/charts/evm-stack/Chart.lock @@ -13,12 +13,12 @@ dependencies: version: 0.1.4 - name: evm-bridge-withdrawer repository: file://../evm-bridge-withdrawer - version: 1.0.1 + version: 1.0.2 - name: postgresql repository: oci://registry-1.docker.io/bitnamicharts version: 15.2.4 - name: blockscout-stack repository: https://blockscout.github.io/helm-charts version: 1.6.8 -digest: sha256:c437d6967341b9bb6e10a809ce13e81130bfb95fb111c8712088ab443adea3f1 -generated: "2025-01-28T23:46:42.687706-05:00" +digest: sha256:7134d1bf040721c7fd84df068c7d89f6b68eb956bf93e37aacf819bc39d233a6 +generated: "2025-02-10T12:38:51.155814856-05:00" From df21731aa2dc06ea22e64a054d4e53b32dc463da Mon Sep 17 00:00:00 2001 From: elizabeth Date: Mon, 10 Feb 2025 12:41:16 -0500 Subject: [PATCH 09/19] changelog update --- crates/astria-bridge-withdrawer/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/astria-bridge-withdrawer/CHANGELOG.md b/crates/astria-bridge-withdrawer/CHANGELOG.md index 792abcab92..e02620342e 100644 --- a/crates/astria-bridge-withdrawer/CHANGELOG.md +++ b/crates/astria-bridge-withdrawer/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Update `idna` dependency to resolve cargo audit warning [#1869](https://github.com/astriaorg/astria/pull/1869). +- Support FROST threshold signing using `astria-bridge-signer` nodes. [#1948](https://github.com/astriaorg/astria/pull/1948). ## [1.0.1] - 2024-11-01 From cacef930ccdea86036b76f8fdd4e7682b2b98466 Mon Sep 17 00:00:00 2001 From: elizabeth Date: Mon, 10 Feb 2025 13:09:46 -0500 Subject: [PATCH 10/19] bump evm-stack chart --- charts/evm-stack/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/evm-stack/Chart.yaml b/charts/evm-stack/Chart.yaml index d2cd30a3e4..dba0072ef6 100644 --- a/charts/evm-stack/Chart.yaml +++ b/charts/evm-stack/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.0.8 +version: 1.0.9 dependencies: - name: celestia-node From e7cf01078bb8a6fd266e620e6fd5531d85f3d462 Mon Sep 17 00:00:00 2001 From: elizabeth Date: Mon, 10 Feb 2025 13:17:53 -0500 Subject: [PATCH 11/19] fix chart config --- charts/evm-bridge-withdrawer/templates/configmaps.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/charts/evm-bridge-withdrawer/templates/configmaps.yaml b/charts/evm-bridge-withdrawer/templates/configmaps.yaml index 79b9f50e47..c28a2b104c 100644 --- a/charts/evm-bridge-withdrawer/templates/configmaps.yaml +++ b/charts/evm-bridge-withdrawer/templates/configmaps.yaml @@ -34,10 +34,10 @@ data: OTEL_SERVICE_NAME: "{{ tpl .Values.otel.serviceName . }}" {{- if not .Values.global.dev }} {{- else }} - ASTRIA_BRIDGE_WITHDRAWER_FROST_THRESHOLD_SIGNING_ENABLED="false" - ASTRIA_BRIDGE_WITHDRAWER_FROST_MIN_SIGNERS="0" - ASTRIA_BRIDGE_WITHDRAWER_FROST_PUBLIC_KEY_PACKAGE_PATH="" - ASTRIA_BRIDGE_WITHDRAWER_FROST_PARTICIPANT_ENDPOINTS="[]" + ASTRIA_BRIDGE_WITHDRAWER_FROST_THRESHOLD_SIGNING_ENABLED: "false" + ASTRIA_BRIDGE_WITHDRAWER_FROST_MIN_SIGNERS: "0" + ASTRIA_BRIDGE_WITHDRAWER_FROST_PUBLIC_KEY_PACKAGE_PATH: "" + ASTRIA_BRIDGE_WITHDRAWER_FROST_PARTICIPANT_ENDPOINTS: "[]" {{- end }} --- {{- if not .Values.secretProvider.enabled }} From cbf8b1dafa3699a4ca4754b74ea15d8408215ac9 Mon Sep 17 00:00:00 2001 From: elizabeth Date: Mon, 10 Feb 2025 13:29:40 -0500 Subject: [PATCH 12/19] update chart config --- charts/evm-bridge-withdrawer/templates/configmaps.yaml | 8 ++++---- charts/evm-bridge-withdrawer/values.yaml | 6 +++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/charts/evm-bridge-withdrawer/templates/configmaps.yaml b/charts/evm-bridge-withdrawer/templates/configmaps.yaml index c28a2b104c..9631a175c0 100644 --- a/charts/evm-bridge-withdrawer/templates/configmaps.yaml +++ b/charts/evm-bridge-withdrawer/templates/configmaps.yaml @@ -34,10 +34,10 @@ data: OTEL_SERVICE_NAME: "{{ tpl .Values.otel.serviceName . }}" {{- if not .Values.global.dev }} {{- else }} - ASTRIA_BRIDGE_WITHDRAWER_FROST_THRESHOLD_SIGNING_ENABLED: "false" - ASTRIA_BRIDGE_WITHDRAWER_FROST_MIN_SIGNERS: "0" - ASTRIA_BRIDGE_WITHDRAWER_FROST_PUBLIC_KEY_PACKAGE_PATH: "" - ASTRIA_BRIDGE_WITHDRAWER_FROST_PARTICIPANT_ENDPOINTS: "[]" + ASTRIA_BRIDGE_WITHDRAWER_FROST_THRESHOLD_SIGNING_ENABLED: "{{ .Values.config.frostThresholdSigningEnabled }}" + ASTRIA_BRIDGE_WITHDRAWER_FROST_MIN_SIGNERS: "{{ .Values.config.frostMinSigners }}" + ASTRIA_BRIDGE_WITHDRAWER_FROST_PUBLIC_KEY_PACKAGE_PATH: "{{ .Values.config.frostPublicKeyPackagePath }}" + ASTRIA_BRIDGE_WITHDRAWER_FROST_PARTICIPANT_ENDPOINTS: "{{ .Values.config.frostParticipantEndpoints }}" {{- end }} --- {{- if not .Values.secretProvider.enabled }} diff --git a/charts/evm-bridge-withdrawer/values.yaml b/charts/evm-bridge-withdrawer/values.yaml index 09a08bd661..6ee5adf22b 100644 --- a/charts/evm-bridge-withdrawer/values.yaml +++ b/charts/evm-bridge-withdrawer/values.yaml @@ -28,7 +28,11 @@ config: rollupAssetDenom: "" evmContractAddress: "0x" evmRpcEndpoint: "" - sequencerPrivateKey: + frostThresholdSigningEnabled: "false" + frostMinSigners: "0", + frostPublicKeyPackagePath: "" + frostParticipantEndpoints: "[]" + sequencerPrivateKey: devContent: "" secret: filename: "key.hex" From cb67d38e18f59c2cca7117bbae9c7aac3686f895 Mon Sep 17 00:00:00 2001 From: elizabeth Date: Mon, 10 Feb 2025 14:13:05 -0500 Subject: [PATCH 13/19] fix chart --- charts/evm-bridge-withdrawer/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/evm-bridge-withdrawer/values.yaml b/charts/evm-bridge-withdrawer/values.yaml index 6ee5adf22b..116771b22b 100644 --- a/charts/evm-bridge-withdrawer/values.yaml +++ b/charts/evm-bridge-withdrawer/values.yaml @@ -29,7 +29,7 @@ config: evmContractAddress: "0x" evmRpcEndpoint: "" frostThresholdSigningEnabled: "false" - frostMinSigners: "0", + frostMinSigners: "0" frostPublicKeyPackagePath: "" frostParticipantEndpoints: "[]" sequencerPrivateKey: From 44bd2a61100981db4dae150a858dcf8d4dd8ad23 Mon Sep 17 00:00:00 2001 From: elizabeth Date: Mon, 10 Feb 2025 14:44:45 -0500 Subject: [PATCH 14/19] fix --- charts/evm-bridge-withdrawer/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/evm-bridge-withdrawer/values.yaml b/charts/evm-bridge-withdrawer/values.yaml index 116771b22b..da33a5910f 100644 --- a/charts/evm-bridge-withdrawer/values.yaml +++ b/charts/evm-bridge-withdrawer/values.yaml @@ -32,7 +32,7 @@ config: frostMinSigners: "0" frostPublicKeyPackagePath: "" frostParticipantEndpoints: "[]" - sequencerPrivateKey: + sequencerPrivateKey: devContent: "" secret: filename: "key.hex" From 1d1a57ac6fee9e133795a5a674598a2b0c68c30e Mon Sep 17 00:00:00 2001 From: elizabeth Date: Mon, 10 Feb 2025 15:15:25 -0500 Subject: [PATCH 15/19] implement sign_tx with retry --- .../local.env.example | 2 +- .../src/bridge_withdrawer/mod.rs | 8 ++--- .../bridge_withdrawer/submitter/builder.rs | 2 +- .../submitter/frost_signer.rs | 2 ++ .../src/bridge_withdrawer/submitter/mod.rs | 35 ++++++++++++++++++- .../src/bridge_withdrawer/submitter/signer.rs | 1 + 6 files changed, 43 insertions(+), 7 deletions(-) diff --git a/crates/astria-bridge-withdrawer/local.env.example b/crates/astria-bridge-withdrawer/local.env.example index 214025a241..104751fa8c 100644 --- a/crates/astria-bridge-withdrawer/local.env.example +++ b/crates/astria-bridge-withdrawer/local.env.example @@ -49,7 +49,7 @@ ASTRIA_BRIDGE_WITHDRAWER_FROST_PUBLIC_KEY_PACKAGE_PATH="" # The frost participant gRPC endpoints. # Only used if `frost_threshold_signing_enabled` is true. -ASTRIA_BRIDGE_WITHDRAWER_FROST_PARTICIPANT_ENDPOINTS=[] +ASTRIA_BRIDGE_WITHDRAWER_FROST_PARTICIPANT_ENDPOINTS="[]" # The prefix that will be used to construct bech32m sequencer addresses. ASTRIA_BRIDGE_WITHDRAWER_SEQUENCER_ADDRESS_PREFIX=astria diff --git a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/mod.rs b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/mod.rs index 2113c7e8b8..5d77000e13 100644 --- a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/mod.rs +++ b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/mod.rs @@ -288,8 +288,8 @@ async fn make_signer( frost_participant_endpoints: Vec, sequencer_key_path: String, sequencer_address_prefix: String, -) -> eyre::Result> { - let signer: Box = if frost_threshold_signing_enabled { +) -> eyre::Result> { + let signer: Arc = if frost_threshold_signing_enabled { let public_key_package_str = std::fs::read_to_string(frost_public_key_package_path) .wrap_err("failed to read frost public key package")?; let public_key_package = @@ -300,7 +300,7 @@ async fn make_signer( initialize_frost_participant_clients(frost_participant_endpoints, &public_key_package) .await .wrap_err("failed to initialize frost participant clients")?; - Box::new( + Arc::new( FrostSignerBuilder::new() .min_signers(frost_min_signers) .public_key_package(public_key_package) @@ -310,7 +310,7 @@ async fn make_signer( .wrap_err("failed to initialize frost signer")?, ) } else { - Box::new( + Arc::new( crate::bridge_withdrawer::submitter::signer::SequencerKey::builder() .path(sequencer_key_path) .prefix(sequencer_address_prefix) diff --git a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/builder.rs b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/builder.rs index 9394f6bd3c..f92c3cc0c7 100644 --- a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/builder.rs +++ b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/builder.rs @@ -49,7 +49,7 @@ impl Handle { pub(crate) struct Builder { pub(crate) shutdown_token: CancellationToken, pub(crate) startup_handle: startup::InfoHandle, - pub(crate) signer: Box, + pub(crate) signer: Arc, pub(crate) sequencer_cometbft_client: sequencer_client::HttpClient, pub(crate) sequencer_grpc_client: SequencerServiceClient, pub(crate) state: Arc, diff --git a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/frost_signer.rs b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/frost_signer.rs index 2bd92f99ef..3d4422a5ac 100644 --- a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/frost_signer.rs +++ b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/frost_signer.rs @@ -163,6 +163,7 @@ impl FrostSignerBuilder { } } +#[derive(Debug, Clone)] pub(crate) struct FrostSigner { min_signers: usize, public_key_package: PublicKeyPackage, @@ -183,6 +184,7 @@ impl Signer for FrostSigner { commitments.len() ); + // part 2: gather signature shares from participants let tx_bytes = tx.to_raw().encode_to_vec(); let sig_shares = self.frost_part_2(commitments, tx_bytes.clone()).await; ensure!( diff --git a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/mod.rs b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/mod.rs index cfccace352..5d072b0f05 100644 --- a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/mod.rs +++ b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/mod.rs @@ -72,7 +72,7 @@ pub(super) struct Submitter { batches_rx: mpsc::Receiver, sequencer_cometbft_client: sequencer_client::HttpClient, sequencer_grpc_client: SequencerServiceClient, - signer: Box, + signer: Arc, metrics: &'static Metrics, } @@ -238,6 +238,39 @@ fn report_exit(reason: eyre::Result<&str>) { } } +#[instrument(name = "sign_tx", skip_all, err)] +async fn sign_tx(signer: Arc, body: TransactionBody) -> eyre::Result { + let span = Span::current(); + let retry_config = tryhard::RetryFutureConfig::new(1024) + .exponential_backoff(Duration::from_millis(200)) + .max_delay(Duration::from_secs(60)) + .on_retry( + |attempt, next_delay: Option, err: &eyre::Report| { + let wait_duration = next_delay + .map(humantime::format_duration) + .map(tracing::field::display); + warn!( + parent: span.clone(), + attempt, + wait_duration, + error = ?err, + "failed signingstart transaction body; retrying after backoff", + ); + async move {} + }, + ); + let signed = tryhard::retry_fn(|| { + let body = body.clone(); + let signer = signer.clone(); + let span = info_span!(parent: span.clone(), "attempt sign"); + async move { signer.sign(body).await }.instrument(span) + }) + .with_config(retry_config) + .await + .wrap_err("failed to sign transaction after 1024 attempts")?; + Ok(signed) +} + /// Submits a transaction to the sequencer with exponential backoff. #[instrument( name = "submit_tx", diff --git a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/signer.rs b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/signer.rs index 0d14e55092..4d5674f373 100644 --- a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/signer.rs +++ b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/signer.rs @@ -28,6 +28,7 @@ pub(crate) trait Signer: Send + Sync { async fn sign(&self, tx: TransactionBody) -> eyre::Result; } +#[derive(Debug, Clone)] pub(crate) struct SequencerKey { address: Address, signing_key: SigningKey, From b7e08bdea523ae94eca3fb0e7e36b2120ce3eeeb Mon Sep 17 00:00:00 2001 From: elizabeth Date: Mon, 10 Feb 2025 15:20:42 -0500 Subject: [PATCH 16/19] fix test --- crates/astria-bridge-withdrawer/local.env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/astria-bridge-withdrawer/local.env.example b/crates/astria-bridge-withdrawer/local.env.example index 104751fa8c..214025a241 100644 --- a/crates/astria-bridge-withdrawer/local.env.example +++ b/crates/astria-bridge-withdrawer/local.env.example @@ -49,7 +49,7 @@ ASTRIA_BRIDGE_WITHDRAWER_FROST_PUBLIC_KEY_PACKAGE_PATH="" # The frost participant gRPC endpoints. # Only used if `frost_threshold_signing_enabled` is true. -ASTRIA_BRIDGE_WITHDRAWER_FROST_PARTICIPANT_ENDPOINTS="[]" +ASTRIA_BRIDGE_WITHDRAWER_FROST_PARTICIPANT_ENDPOINTS=[] # The prefix that will be used to construct bech32m sequencer addresses. ASTRIA_BRIDGE_WITHDRAWER_SEQUENCER_ADDRESS_PREFIX=astria From ec970b87431e6f70906cca499660988fafcd8b4c Mon Sep 17 00:00:00 2001 From: elizabeth Date: Mon, 10 Feb 2025 15:29:21 -0500 Subject: [PATCH 17/19] lint --- .../src/bridge_withdrawer/submitter/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/mod.rs b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/mod.rs index 5d072b0f05..c028bb3e54 100644 --- a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/mod.rs +++ b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/mod.rs @@ -253,7 +253,7 @@ async fn sign_tx(signer: Arc, body: TransactionBody) -> eyre::Result parent: span.clone(), attempt, wait_duration, - error = ?err, + error = err.as_ref() as &dyn std::error::Error, "failed signingstart transaction body; retrying after backoff", ); async move {} From 1b3a95889ee24d0e62b55fcdebd90de092af3336 Mon Sep 17 00:00:00 2001 From: elizabeth Date: Wed, 12 Feb 2025 18:05:13 -0500 Subject: [PATCH 18/19] add blackbox threshold signer test --- Cargo.lock | 1 + crates/astria-bridge-withdrawer/Cargo.toml | 1 + .../blackbox/helpers/mock_bridge_signer.rs | 176 ++++++++++++++++++ .../tests/blackbox/helpers/mod.rs | 1 + .../helpers/test_bridge_withdrawer.rs | 119 +++++++++++- .../tests/blackbox/main.rs | 39 ++++ crates/astria-core/src/generated/mod.rs | 13 +- 7 files changed, 343 insertions(+), 7 deletions(-) create mode 100644 crates/astria-bridge-withdrawer/tests/blackbox/helpers/mock_bridge_signer.rs diff --git a/Cargo.lock b/Cargo.lock index 38114a9866..7f81bb8d03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -538,6 +538,7 @@ dependencies = [ "pbjson-types", "pin-project-lite", "prost", + "rand 0.8.5", "reqwest", "serde", "serde_json", diff --git a/crates/astria-bridge-withdrawer/Cargo.toml b/crates/astria-bridge-withdrawer/Cargo.toml index eae0ab04f0..9b0e13e3f8 100644 --- a/crates/astria-bridge-withdrawer/Cargo.toml +++ b/crates/astria-bridge-withdrawer/Cargo.toml @@ -56,6 +56,7 @@ astria-grpc-mock = { path = "../astria-grpc-mock" } config = { package = "astria-config", path = "../astria-config", features = [ "tests", ] } +rand = { workspace = true } reqwest = { workspace = true, features = ["json"] } tempfile = { workspace = true } tendermint-rpc = { workspace = true } diff --git a/crates/astria-bridge-withdrawer/tests/blackbox/helpers/mock_bridge_signer.rs b/crates/astria-bridge-withdrawer/tests/blackbox/helpers/mock_bridge_signer.rs new file mode 100644 index 0000000000..e57bc9b0a3 --- /dev/null +++ b/crates/astria-bridge-withdrawer/tests/blackbox/helpers/mock_bridge_signer.rs @@ -0,0 +1,176 @@ +use std::{ + net::SocketAddr, + sync::Arc, +}; + +use astria_core::{ + self, + generated::astria::signer::v1::{ + frost_participant_service_server::{ + FrostParticipantService, + FrostParticipantServiceServer, + }, + GetVerifyingShareRequest, + GetVerifyingShareResponse, + Part1Request, + Part1Response, + Part2Request, + Part2Response, + }, +}; +use astria_eyre::eyre::{ + self, + WrapErr as _, +}; +use astria_grpc_mock::{ + matcher::message_type, + response::constant_response, + Mock, + MockServer, +}; +use frost_ed25519::round1; +use rand::rngs::OsRng; +use tokio::task::JoinHandle; +use tonic::{ + transport::Server, + Request, + Response, + Status, +}; + +#[expect( + clippy::module_name_repetitions, + reason = "naming is helpful for clarity here" +)] +pub struct MockBridgeSignerServer { + _server: JoinHandle>, + pub(crate) mock_server: MockServer, + pub(crate) local_addr: SocketAddr, + secret_package: frost_ed25519::keys::KeyPackage, +} + +impl MockBridgeSignerServer { + pub(crate) async fn spawn(secret_package: frost_ed25519::keys::KeyPackage) -> Self { + use tokio_stream::wrappers::TcpListenerStream; + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let local_addr = listener.local_addr().unwrap(); + + let mock_server = MockServer::new(); + + let server = { + let sequencer_service = FrostParticipantServiceImpl { + server: mock_server.clone(), + secret_package: secret_package.clone(), + nonce: tokio::sync::Mutex::new(None), + }; + tokio::spawn(async move { + Server::builder() + .add_service(FrostParticipantServiceServer::new(sequencer_service)) + .serve_with_incoming(TcpListenerStream::new(listener)) + .await + .wrap_err("gRPC sequencer server failed") + }) + }; + Self { + _server: server, + mock_server, + local_addr, + secret_package, + } + } + + pub(crate) async fn mount_get_verifying_share_response(&self, debug_name: impl Into) { + let resp = GetVerifyingShareResponse { + verifying_share: self + .secret_package + .verifying_share() + .to_owned() + .serialize() + .expect("can serialize verifying share") + .into(), + }; + Mock::for_rpc_given( + "get_verifying_share", + message_type::(), + ) + .respond_with(constant_response(resp)) + .up_to_n_times(1) + .expect(1) + .with_name(debug_name) + .mount(&self.mock_server) + .await; + } +} + +struct FrostParticipantServiceImpl { + server: MockServer, + secret_package: frost_ed25519::keys::KeyPackage, + nonce: tokio::sync::Mutex>, +} + +#[tonic::async_trait] +impl FrostParticipantService for FrostParticipantServiceImpl { + async fn get_verifying_share( + self: Arc, + request: Request, + ) -> Result, Status> { + self.server + .handle_request("get_verifying_share", request) + .await + } + + async fn part1( + self: Arc, + _request: Request, + ) -> Result, Status> { + let mut rng = OsRng; + let (nonces, commitments) = + frost_ed25519::round1::commit(self.secret_package.signing_share(), &mut rng); + let commitment = commitments + .serialize() + .map_err(|e| Status::internal(format!("failed to serialize commitments: {e}")))? + .into(); + let mut nonce = self.nonce.lock().await; + *nonce = Some(nonces.clone()); + Ok(Response::new(Part1Response { + request_identifier: 0, + commitment, + })) + } + + async fn part2( + self: Arc, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + let signing_commitments = request + .commitments + .into_iter() + .filter_map(|c| { + Some(( + frost_ed25519::Identifier::deserialize(&c.participant_identifier).ok()?, + round1::SigningCommitments::deserialize(&c.commitment).ok()?, + )) + }) + .collect(); + let signing_package = + frost_ed25519::SigningPackage::new(signing_commitments, &request.message); + + let nonce = { + let mut nonce = self.nonce.lock().await; + nonce + .take() + .ok_or_else(|| Status::internal("nonce not set"))? + }; + let signature_share = + frost_ed25519::round2::sign(&signing_package, &nonce, &self.secret_package) + .map_err(|e| Status::internal(format!("failed to sign: {e}")))? + .serialize() + .into(); + + Ok(Response::new(Part2Response { + signature_share, + })) + } +} diff --git a/crates/astria-bridge-withdrawer/tests/blackbox/helpers/mod.rs b/crates/astria-bridge-withdrawer/tests/blackbox/helpers/mod.rs index 78ffafd458..f12dccec09 100644 --- a/crates/astria-bridge-withdrawer/tests/blackbox/helpers/mod.rs +++ b/crates/astria-bridge-withdrawer/tests/blackbox/helpers/mod.rs @@ -1,4 +1,5 @@ mod ethereum; +mod mock_bridge_signer; mod mock_cometbft; mod mock_sequencer; mod test_bridge_withdrawer; diff --git a/crates/astria-bridge-withdrawer/tests/blackbox/helpers/test_bridge_withdrawer.rs b/crates/astria-bridge-withdrawer/tests/blackbox/helpers/test_bridge_withdrawer.rs index 17c8d7e3a5..f16660ea8e 100644 --- a/crates/astria-bridge-withdrawer/tests/blackbox/helpers/test_bridge_withdrawer.rs +++ b/crates/astria-bridge-withdrawer/tests/blackbox/helpers/test_bridge_withdrawer.rs @@ -1,4 +1,5 @@ use std::{ + collections::BTreeMap, io::Write as _, mem, net::SocketAddr, @@ -33,6 +34,15 @@ use ethers::{ abi::AbiEncode, types::TransactionReceipt, }; +use frost_ed25519::{ + keys::{ + IdentifierList, + KeyPackage, + PublicKeyPackage, + SecretShare, + }, + Identifier, +}; use futures::Future; use ibc_types::core::{ channel::ChannelId, @@ -54,6 +64,7 @@ use tracing::{ use super::{ ethereum::AstriaBridgeableERC20DeployerConfig, make_tx_sync_success_response, + mock_bridge_signer::MockBridgeSignerServer, mock_cometbft::{ mount_default_chain_id, mount_get_nonce_response, @@ -103,6 +114,9 @@ pub struct TestBridgeWithdrawer { /// The mock sequencer server. pub sequencer_mock: MockSequencerServer, + /// The mock bridge signer servers. + pub bridge_signer_mocks: Vec, + /// The rollup-side ethereum smart contract pub ethereum: TestEthereum, @@ -235,6 +249,57 @@ impl TestBridgeWithdrawer { ) .await } + + pub async fn mount_get_verifying_share_responses(&self) { + for server in &self.bridge_signer_mocks { + server + .mount_get_verifying_share_response("get_verifying_share") + .await; + } + } + + // pub async fn mount_part1_responses(&self, message: Vec) -> Vec { + // let mut part1_outputs = Vec::with_capacity(self.bridge_signer_mocks.len()); + // for server in &self.bridge_signer_mocks { + // let output = server.mount_part1_response("part1").await; + // part1_outputs.push(output); + // } + + // let commitments = part1_outputs + // .iter() + // .map(|output| (output.0.clone(), output.1.clone())) + // // .map(|output| CommitmentWithIdentifier { + // // commitment: output + // // .1 + // // .serialize() + // // .expect("can serialize commitment") + // // .into(), + // // participant_identifier: output.0.serialize().into(), + // // }) + // .collect::>(); + + // let mut part2_responses = Vec::with_capacity(self.bridge_signer_mocks.len()); + // for (server, output) in self.bridge_signer_mocks.iter().zip(part1_outputs) { + // let response = + // server.get_frost_part2_response(commitments.clone(), message.clone(), output.2); + // part2_responses.push(response); + // } + // // requests_and_nonces + + // // let mut part2_responses = Vec::with_capacity(self.bridge_signer_mocks.len()); + // // for (server, (request, nonce)) in + // // self.bridge_signer_mocks.iter().zip(requests_and_nonces) { let response = + // // server.get_frost_part2_response(commitments.clone(), message.clone(), nonce); + // // part2_responses.push(response); + // // } + // part2_responses + // } + + // pub async fn mount_part2_responses(&self, resps: Vec) { + // for (server, resp) in self.bridge_signer_mocks.iter().zip(resps) { + // server.mount_part2_response("part2", resp).await; + // } + // } } #[expect(clippy::module_name_repetitions, reason = "naming is for clarity here")] @@ -243,6 +308,8 @@ pub struct TestBridgeWithdrawerConfig { pub ethereum_config: TestEthereumConfig, /// The denomination of the asset pub asset_denom: Denom, + /// Threshold signer count, if threshold signing is to be enabled + pub threshold_signer_count: u16, } impl TestBridgeWithdrawerConfig { @@ -250,6 +317,7 @@ impl TestBridgeWithdrawerConfig { let Self { ethereum_config, asset_denom, + threshold_signer_count, } = self; LazyLock::force(&TELEMETRY); @@ -267,15 +335,40 @@ impl TestBridgeWithdrawerConfig { let cometbft_mock = wiremock::MockServer::start().await; let sequencer_mock = MockSequencerServer::spawn().await; + let (secret_shares, public_key_package) = get_frost_secret_shares(threshold_signer_count); + let mut frost_participant_endpoints = Vec::with_capacity(threshold_signer_count as usize); + let mut bridge_signer_mocks = Vec::with_capacity(threshold_signer_count as usize); + for secret_share in secret_shares.into_values() { + let secret_package = KeyPackage::try_from(secret_share) + .expect("can convert secret share to secret package"); + let server = MockBridgeSignerServer::spawn(secret_package).await; + server + .mount_get_verifying_share_response("get_verifying_share") + .await; + frost_participant_endpoints.push(format!("http://{}", server.local_addr)); + bridge_signer_mocks.push(server); + } + + let public_key_string = + serde_json::to_string(&public_key_package).expect("can serialize public key package"); + let mut public_key_package_file = NamedTempFile::new().unwrap(); + public_key_package_file + .write_all(public_key_string.as_bytes()) + .expect("can write public key package to file"); + let config = Config { sequencer_cometbft_endpoint: cometbft_mock.uri(), sequencer_grpc_endpoint: format!("http://{}", sequencer_mock.local_addr), sequencer_chain_id: SEQUENCER_CHAIN_ID.into(), sequencer_key_path, - frost_threshold_signing_enabled: false, - frost_min_signers: 0, - frost_public_key_package_path: String::new(), - frost_participant_endpoints: Vec::new(), + frost_threshold_signing_enabled: threshold_signer_count != 0, + frost_min_signers: threshold_signer_count as usize, + frost_public_key_package_path: public_key_package_file + .path() + .to_str() + .expect("can get public key package path") + .to_string(), + frost_participant_endpoints, fee_asset_denomination: asset_denom.clone(), rollup_asset_denomination: asset_denom.as_trace_prefixed().unwrap().clone(), sequencer_bridge_address: default_bridge_address().to_string(), @@ -312,6 +405,7 @@ impl TestBridgeWithdrawerConfig { ethereum, cometbft_mock, sequencer_mock, + bridge_signer_mocks, bridge_withdrawer_shutdown_handle: Some(bridge_withdrawer_shutdown_handle), bridge_withdrawer, config, @@ -331,6 +425,7 @@ impl TestBridgeWithdrawerConfig { ..Default::default() }), asset_denom: DEFAULT_IBC_DENOM.parse().unwrap(), + threshold_signer_count: 0, } } @@ -344,6 +439,7 @@ impl TestBridgeWithdrawerConfig { }, ), asset_denom: default_native_asset(), + threshold_signer_count: 0, } } @@ -357,6 +453,7 @@ impl TestBridgeWithdrawerConfig { }, ), asset_denom: DEFAULT_IBC_DENOM.parse().unwrap(), + threshold_signer_count: 0, } } } @@ -368,6 +465,7 @@ impl Default for TestBridgeWithdrawerConfig { AstriaWithdrawerDeployerConfig::default(), ), asset_denom: default_native_asset(), + threshold_signer_count: 0, } } } @@ -580,3 +678,16 @@ pub(crate) fn astria_address( .try_build() .unwrap() } + +fn get_frost_secret_shares( + num_signers: u16, +) -> (BTreeMap, PublicKeyPackage) { + use rand::rngs::OsRng; + frost_ed25519::keys::generate_with_dealer( + num_signers, + num_signers, + IdentifierList::Default, + OsRng, + ) + .expect("can generate keys") +} diff --git a/crates/astria-bridge-withdrawer/tests/blackbox/main.rs b/crates/astria-bridge-withdrawer/tests/blackbox/main.rs index 2a248cace9..3b0e32de98 100644 --- a/crates/astria-bridge-withdrawer/tests/blackbox/main.rs +++ b/crates/astria-bridge-withdrawer/tests/blackbox/main.rs @@ -175,6 +175,45 @@ async fn erc20_ics20_withdraw_success() { ); } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[ignore = "needs anvil to be present in $PATH; see github.com/foundry-rs/foundry for how to \ + install"] +async fn native_sequencer_withdraw_threshold_signing_success() { + let config = TestBridgeWithdrawerConfig { + threshold_signer_count: 3, + ..Default::default() + }; + let test_env = config.spawn().await; + + test_env + .mount_pending_nonce_response(1, "process batch 1") + .await; + let broadcast_guard = test_env + .mount_broadcast_tx_sync_success_response_as_scoped() + .await; + + // send a native sequencer withdrawal tx to the rollup + let value = 1_000_000.into(); + let recipient = default_sequencer_address(); + let receipt = test_env + .ethereum + .send_sequencer_withdraw_transaction(value, recipient) + .await; + + test_env + .timeout_ms( + 2_000, + "batch 1 execution", + broadcast_guard.wait_until_satisfied(), + ) + .await; + + assert_contract_receipt_action_matches_broadcast_action::( + &broadcast_guard.received_requests().await, + &receipt, + ); +} + trait ActionFromReceipt { fn action_from_receipt(receipt: ðers::types::TransactionReceipt) -> Action; } diff --git a/crates/astria-core/src/generated/mod.rs b/crates/astria-core/src/generated/mod.rs index a3981dd43e..b8c820c507 100644 --- a/crates/astria-core/src/generated/mod.rs +++ b/crates/astria-core/src/generated/mod.rs @@ -191,9 +191,16 @@ pub mod astria { #[path = ""] pub mod signer { - #[path = "astria.signer.v1.rs"] - pub mod v1; - } + pub mod v1 { + include!("astria.signer.v1.rs"); + + #[cfg(feature = "serde")] + mod _serde_impl { + use super::*; + include!("astria.signer.v1.serde.rs"); + } + } + } } #[path = ""] From c3606d474377c75b0bd3d9c857f300daa31e6b1d Mon Sep 17 00:00:00 2001 From: elizabeth Date: Thu, 13 Feb 2025 11:06:18 -0500 Subject: [PATCH 19/19] fix tests --- .../helpers/test_bridge_withdrawer.rs | 110 +++++++----------- 1 file changed, 43 insertions(+), 67 deletions(-) diff --git a/crates/astria-bridge-withdrawer/tests/blackbox/helpers/test_bridge_withdrawer.rs b/crates/astria-bridge-withdrawer/tests/blackbox/helpers/test_bridge_withdrawer.rs index f16660ea8e..04a7f54e6e 100644 --- a/crates/astria-bridge-withdrawer/tests/blackbox/helpers/test_bridge_withdrawer.rs +++ b/crates/astria-bridge-withdrawer/tests/blackbox/helpers/test_bridge_withdrawer.rs @@ -257,49 +257,6 @@ impl TestBridgeWithdrawer { .await; } } - - // pub async fn mount_part1_responses(&self, message: Vec) -> Vec { - // let mut part1_outputs = Vec::with_capacity(self.bridge_signer_mocks.len()); - // for server in &self.bridge_signer_mocks { - // let output = server.mount_part1_response("part1").await; - // part1_outputs.push(output); - // } - - // let commitments = part1_outputs - // .iter() - // .map(|output| (output.0.clone(), output.1.clone())) - // // .map(|output| CommitmentWithIdentifier { - // // commitment: output - // // .1 - // // .serialize() - // // .expect("can serialize commitment") - // // .into(), - // // participant_identifier: output.0.serialize().into(), - // // }) - // .collect::>(); - - // let mut part2_responses = Vec::with_capacity(self.bridge_signer_mocks.len()); - // for (server, output) in self.bridge_signer_mocks.iter().zip(part1_outputs) { - // let response = - // server.get_frost_part2_response(commitments.clone(), message.clone(), output.2); - // part2_responses.push(response); - // } - // // requests_and_nonces - - // // let mut part2_responses = Vec::with_capacity(self.bridge_signer_mocks.len()); - // // for (server, (request, nonce)) in - // // self.bridge_signer_mocks.iter().zip(requests_and_nonces) { let response = - // // server.get_frost_part2_response(commitments.clone(), message.clone(), nonce); - // // part2_responses.push(response); - // // } - // part2_responses - // } - - // pub async fn mount_part2_responses(&self, resps: Vec) { - // for (server, resp) in self.bridge_signer_mocks.iter().zip(resps) { - // server.mount_part2_response("part2", resp).await; - // } - // } } #[expect(clippy::module_name_repetitions, reason = "naming is for clarity here")] @@ -313,6 +270,7 @@ pub struct TestBridgeWithdrawerConfig { } impl TestBridgeWithdrawerConfig { + #[expect(clippy::too_many_lines, reason = "this is a test setup function")] pub async fn spawn(self) -> TestBridgeWithdrawer { let Self { ethereum_config, @@ -335,26 +293,48 @@ impl TestBridgeWithdrawerConfig { let cometbft_mock = wiremock::MockServer::start().await; let sequencer_mock = MockSequencerServer::spawn().await; - let (secret_shares, public_key_package) = get_frost_secret_shares(threshold_signer_count); - let mut frost_participant_endpoints = Vec::with_capacity(threshold_signer_count as usize); - let mut bridge_signer_mocks = Vec::with_capacity(threshold_signer_count as usize); - for secret_share in secret_shares.into_values() { - let secret_package = KeyPackage::try_from(secret_share) - .expect("can convert secret share to secret package"); - let server = MockBridgeSignerServer::spawn(secret_package).await; - server - .mount_get_verifying_share_response("get_verifying_share") - .await; - frost_participant_endpoints.push(format!("http://{}", server.local_addr)); - bridge_signer_mocks.push(server); - } + let ( + frost_public_key_package_path, + frost_participant_endpoints, + bridge_signer_mocks, + _public_key_package_file, + ) = if threshold_signer_count != 0 { + let (secret_shares, public_key_package) = + get_frost_secret_shares(threshold_signer_count); + let mut frost_participant_endpoints = + Vec::with_capacity(threshold_signer_count as usize); + let mut bridge_signer_mocks = Vec::with_capacity(threshold_signer_count as usize); + for secret_share in secret_shares.into_values() { + let secret_package = KeyPackage::try_from(secret_share) + .expect("can convert secret share to secret package"); + let server = MockBridgeSignerServer::spawn(secret_package).await; + server + .mount_get_verifying_share_response("get_verifying_share") + .await; + frost_participant_endpoints.push(format!("http://{}", server.local_addr)); + bridge_signer_mocks.push(server); + } - let public_key_string = - serde_json::to_string(&public_key_package).expect("can serialize public key package"); - let mut public_key_package_file = NamedTempFile::new().unwrap(); - public_key_package_file - .write_all(public_key_string.as_bytes()) - .expect("can write public key package to file"); + let public_key_string = serde_json::to_string(&public_key_package) + .expect("can serialize public key package"); + let mut public_key_package_file: NamedTempFile = NamedTempFile::new().unwrap(); + public_key_package_file + .write_all(public_key_string.as_bytes()) + .expect("can write public key package to file"); + let public_key_package_path = public_key_package_file + .path() + .to_str() + .expect("can get public key package path") + .to_string(); + ( + public_key_package_path, + frost_participant_endpoints, + bridge_signer_mocks, + Some(public_key_package_file), + ) + } else { + (String::new(), Vec::new(), Vec::new(), None) + }; let config = Config { sequencer_cometbft_endpoint: cometbft_mock.uri(), @@ -363,11 +343,7 @@ impl TestBridgeWithdrawerConfig { sequencer_key_path, frost_threshold_signing_enabled: threshold_signer_count != 0, frost_min_signers: threshold_signer_count as usize, - frost_public_key_package_path: public_key_package_file - .path() - .to_str() - .expect("can get public key package path") - .to_string(), + frost_public_key_package_path, frost_participant_endpoints, fee_asset_denomination: asset_denom.clone(), rollup_asset_denomination: asset_denom.as_trace_prefixed().unwrap().clone(),