diff --git a/Cargo.lock b/Cargo.lock index cf483afff4..7f81bb8d03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -528,14 +528,17 @@ dependencies = [ "astria-telemetry", "axum", "ethers", + "frost-ed25519", "futures", "hex", "http 0.2.12", "humantime", "hyper 0.14.30", "ibc-types", + "pbjson-types", "pin-project-lite", "prost", + "rand 0.8.5", "reqwest", "serde", "serde_json", @@ -2538,9 +2541,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 +3405,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 +3415,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 +3428,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 +3442,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 +4783,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/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/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..9631a175c0 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: "{{ .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 4f4bbcf8da..da33a5910f 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: @@ -28,6 +28,10 @@ config: rollupAssetDenom: "" evmContractAddress: "0x" evmRpcEndpoint: "" + frostThresholdSigningEnabled: "false" + frostMinSigners: "0" + frostPublicKeyPackagePath: "" + frostParticipantEndpoints: "[]" sequencerPrivateKey: devContent: "" secret: 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" diff --git a/charts/evm-stack/Chart.yaml b/charts/evm-stack/Chart.yaml index 17bceddce5..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 @@ -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 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 diff --git a/crates/astria-bridge-withdrawer/Cargo.toml b/crates/astria-bridge-withdrawer/Cargo.toml index 103f23ae8e..9b0e13e3f8 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 } @@ -18,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"] } @@ -54,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/local.env.example b/crates/astria-bridge-withdrawer/local.env.example index 3cd1747f4c..214025a241 100644 --- a/crates/astria-bridge-withdrawer/local.env.example +++ b/crates/astria-bridge-withdrawer/local.env.example @@ -31,10 +31,26 @@ 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. 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 aeb0f1a721..5d77000e13 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::{ @@ -45,6 +46,10 @@ use self::{ }; use crate::{ api, + bridge_withdrawer::submitter::frost_signer::{ + initialize_frost_participant_clients, + FrostSignerBuilder, + }, config::Config, metrics::Metrics, }; @@ -71,13 +76,20 @@ 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 +131,27 @@ impl BridgeWithdrawer { let startup_handle = startup::InfoHandle::new(state.subscribe()); // make submitter object + 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(), startup_handle: startup_handle.clone(), sequencer_cometbft_client, sequencer_grpc_client, - sequencer_key_path, - sequencer_address_prefix: sequencer_address_prefix.clone(), + signer, state: state.clone(), metrics, } - .build() - .wrap_err("failed to initialize submitter")?; + .build(); let ethereum_watcher = watcher::Builder { ethereum_contract_address, @@ -260,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: 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 = + 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")?; + Arc::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 { + Arc::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 62308d1572..f92c3cc0c7 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: Arc, pub(crate) sequencer_cometbft_client: sequencer_client::HttpClient, pub(crate) sequencer_grpc_client: SequencerServiceClient, pub(crate) state: Arc, @@ -56,29 +58,23 @@ 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, - 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); let handle = Handle::new(batches_tx); - Ok(( + ( super::Submitter { shutdown_token, startup_handle, @@ -90,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 new file mode 100644 index 0000000000..3d4422a5ac --- /dev/null +++ b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/frost_signer.rs @@ -0,0 +1,320 @@ +use std::collections::{ + BTreeMap, + HashMap, +}; + +use astria_core::{ + crypto::VerificationKey, + generated::astria::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::{ + PublicKeyPackage, + VerifyingShare, + }, + round1, + Identifier, +}; +use futures::StreamExt as _; +use prost::{ + Message as _, + Name as _, +}; + +use super::Signer; + +pub(crate) async fn initialize_frost_participant_clients( + endpoints: Vec, + public_key_package: &PublicKeyPackage, +) -> eyre::Result>> { + 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 { + min_signers: Option, + public_key_package: Option, + address_prefix: Option, + participant_clients: + HashMap>, +} + +impl FrostSignerBuilder { + 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 { + min_signers: Some(min_signers), + ..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 address_prefix(self, address_prefix: String) -> Self { + Self { + address_prefix: Some(address_prefix), + ..self + } + } + + pub(crate) fn participant_clients( + self, + participant_clients: HashMap< + Identifier, + FrostParticipantServiceClient, + >, + ) -> Self { + Self { + participant_clients, + ..self + } + } + + pub(crate) fn try_build(self) -> eyre::Result { + 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"))?; + 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.address_prefix + .ok_or_else(|| eyre!("astria address prefix is required"))?, + ) + .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, + address, + participant_clients: self.participant_clients, + }) + } +} + +#[derive(Debug, Clone)] +pub(crate) struct FrostSigner { + min_signers: usize, + public_key_package: PublicKeyPackage, + address: Address, + participant_clients: + HashMap>, +} + +#[tonic::async_trait] +impl Signer for FrostSigner { + async fn sign(&self, tx: TransactionBody) -> eyre::Result { + // 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: 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!( + 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 response for participant with id {id:?}" + ))?; + Ok((id, resp.into_inner())) + }); + } + let results: Vec> = stream.collect::>().await; + let mut signing_package_commitments: BTreeMap = + BTreeMap::new(); + + 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) + } + + 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().into(), + }) + .collect(); + for (id, _, request_identifier) in commitments { + let mut client = self + .participant_clients + .get(&id) + .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(); + 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(); + + sig_shares + } +} 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..c028bb3e54 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: Arc, metrics: &'static Metrics, } @@ -178,7 +180,11 @@ impl Submitter { .wrap_err("failed to build unsigned transaction")?; // sign transaction - let signed = unsigned.sign(signer.signing_key()); + // 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 @@ -232,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.as_ref() as &dyn std::error::Error, + "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 d5a0c8be23..4d5674f373 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, @@ -16,7 +20,15 @@ use astria_eyre::eyre::{ eyre, Context, }; +use tonic::async_trait; + +#[async_trait] +pub(crate) trait Signer: Send + Sync { + fn address(&self) -> &Address; + async fn sign(&self, tx: TransactionBody) -> eyre::Result; +} +#[derive(Debug, Clone)] pub(crate) struct SequencerKey { address: Address, signing_key: SigningKey, @@ -96,12 +108,15 @@ impl SequencerKey { prefix: None, } } +} - pub(crate) fn address(&self) -> &Address { +#[async_trait] +impl Signer for SequencerKey { + fn address(&self) -> &Address { &self.address } - pub(crate) fn signing_key(&self) -> &SigningKey { - &self.signing_key + async fn sign(&self, tx: TransactionBody) -> eyre::Result { + Ok(tx.sign(&self.signing_key)) } } 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..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,12 +49,12 @@ 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()) .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/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 154c409e87..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 @@ -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,14 @@ 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; + } + } } #[expect(clippy::module_name_repetitions, reason = "naming is for clarity here")] @@ -243,13 +265,17 @@ 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 { + #[expect(clippy::too_many_lines, reason = "this is a test setup function")] pub async fn spawn(self) -> TestBridgeWithdrawer { let Self { ethereum_config, asset_denom, + threshold_signer_count, } = self; LazyLock::force(&TELEMETRY); @@ -267,11 +293,58 @@ impl TestBridgeWithdrawerConfig { let cometbft_mock = wiremock::MockServer::start().await; let sequencer_mock = MockSequencerServer::spawn().await; + 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 = 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(), sequencer_grpc_endpoint: format!("http://{}", sequencer_mock.local_addr), sequencer_chain_id: SEQUENCER_CHAIN_ID.into(), sequencer_key_path, + frost_threshold_signing_enabled: threshold_signer_count != 0, + frost_min_signers: threshold_signer_count as usize, + frost_public_key_package_path, + 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(), @@ -297,7 +370,9 @@ 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()); @@ -306,6 +381,7 @@ impl TestBridgeWithdrawerConfig { ethereum, cometbft_mock, sequencer_mock, + bridge_signer_mocks, bridge_withdrawer_shutdown_handle: Some(bridge_withdrawer_shutdown_handle), bridge_withdrawer, config, @@ -325,6 +401,7 @@ impl TestBridgeWithdrawerConfig { ..Default::default() }), asset_denom: DEFAULT_IBC_DENOM.parse().unwrap(), + threshold_signer_count: 0, } } @@ -338,6 +415,7 @@ impl TestBridgeWithdrawerConfig { }, ), asset_denom: default_native_asset(), + threshold_signer_count: 0, } } @@ -351,6 +429,7 @@ impl TestBridgeWithdrawerConfig { }, ), asset_denom: DEFAULT_IBC_DENOM.parse().unwrap(), + threshold_signer_count: 0, } } } @@ -362,6 +441,7 @@ impl Default for TestBridgeWithdrawerConfig { AstriaWithdrawerDeployerConfig::default(), ), asset_denom: default_native_asset(), + threshold_signer_count: 0, } } } @@ -574,3 +654,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/astria.signer.v1.rs b/crates/astria-core/src/generated/astria.signer.v1.rs new file mode 100644 index 0000000000..1231eb059b --- /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. + /// 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, +} +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(bytes = "bytes", tag = "2")] + pub message: ::prost::bytes::Bytes, + #[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..3fb4ce22e5 --- /dev/null +++ b/crates/astria-core/src/generated/astria.signer.v1.serde.rs @@ -0,0 +1,692 @@ +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.message.is_empty() { + 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 !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)?; + } + 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", + "message", + "request_identifier", + "requestIdentifier", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Commitments, + Message, + 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), + "message" => Ok(GeneratedField::Message), + "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 message__ = 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::Message => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("message")); + } + message__ = + 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(Part2Request { + commitments: commitments__.unwrap_or_default(), + message: message__.unwrap_or_default(), + 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..b8c820c507 100644 --- a/crates/astria-core/src/generated/mod.rs +++ b/crates/astria-core/src/generated/mod.rs @@ -188,6 +188,19 @@ pub mod astria { #[path = "astria.composer.v1.rs"] pub mod v1; } + + #[path = ""] + pub mod signer { + pub mod v1 { + include!("astria.signer.v1.rs"); + + #[cfg(feature = "serde")] + mod _serde_impl { + use super::*; + include!("astria.signer.v1.serde.rs"); + } + } + } } #[path = ""] diff --git a/proto/signerapis/astria/signer/v1/frost.proto b/proto/signerapis/astria/signer/v1/frost.proto new file mode 100644 index 0000000000..f8b60515c0 --- /dev/null +++ b/proto/signerapis/astria/signer/v1/frost.proto @@ -0,0 +1,44 @@ +syntax = 'proto3'; + +package astria.signer.v1; + +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; + bytes message = 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) {} +} +