diff --git a/Cargo.lock b/Cargo.lock index 38c8a8276..7fab47a02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8055,6 +8055,7 @@ dependencies = [ "serde_bytes", "serde_json", "sha3", + "thiserror 1.0.69", ] [[package]] diff --git a/chain-signatures/contract-sol/src/lib.rs b/chain-signatures/contract-sol/src/lib.rs index 26706e5bf..3426640be 100644 --- a/chain-signatures/contract-sol/src/lib.rs +++ b/chain-signatures/contract-sol/src/lib.rs @@ -218,19 +218,56 @@ pub struct SignatureRequestedEvent { pub fee_payer: Option, } +/// Event emitted when a bidirectional signing request is initiated +/// via the `sign_bidirectional` instruction on the Solana program. #[event] #[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub struct SignBidirectionalEvent { + /// The public key of the sender initiating the request. pub sender: Pubkey, + + /// The serialized transaction payload to be signed. pub serialized_transaction: Vec, + + /// CAIP-2 chain ID of the *target chain* where the signed transaction will be sent. + /// + /// Note: This is NOT the chain where `respond()` or `respond_bidirectional()` is executed. pub caip2_id: String, + + /// Version of the key to be used for signing. pub key_version: u32, + + /// Deposit associated with the request. pub deposit: u64, + + /// Derivation path used for signing. pub path: String, + + /// Signing algorithm identifier. + /// + /// If empty (`""`), ECDSA will be used by default. pub algo: String, + + /// Destination field (currently unused). + /// + /// Should be left empty (`""`). pub dest: String, + + /// Additional parameters encoded as a string (currently unused). + /// + /// Should be left empty (`""`). pub params: String, + + /// The program ID of the Solana program that emitted this event. + /// + /// Used by MPC service to filter and verify events from the correct program. + /// + /// MUST match the deployed program ID. pub program_id: Pubkey, + + /// Schema used to deserialize the output of the signed transaction. pub output_deserialization_schema: Vec, + + /// Schema used to serialize the `respond_bidirectional` payload. pub respond_serialization_schema: Vec, } diff --git a/chain-signatures/node/src/indexer_hydration.rs b/chain-signatures/node/src/indexer_hydration.rs index 95adabeea..c6b1edce0 100644 --- a/chain-signatures/node/src/indexer_hydration.rs +++ b/chain-signatures/node/src/indexer_hydration.rs @@ -184,18 +184,53 @@ impl SignatureEvent for HydrationSignatureRequestedEvent { } } +/// The deserialized representation of a bidirectional signing request +/// event emitted from the Hydration chain. #[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub struct HydrationSignBidirectionalRequestedEvent { + /// The 32-byte identifier of the sender. pub sender: [u8; 32], + + /// The serialized transaction payload to be signed. pub serialized_transaction: Vec, + + /// CAIP-2 chain ID of the *target chain* where the signed transaction will be sent. + /// + /// Note: This is NOT the chain where `respond()` or `respond_bidirectional()` is executed. pub caip2_id: String, + + /// Version of the key to be used for signing. pub key_version: u32, + + /// Deposit associated with the request. pub deposit: u64, + + /// Derivation path used for signing. pub path: String, + + /// Signing algorithm identifier. + /// + /// If empty (`""`), ECDSA will be used by default. pub algo: String, + + /// Destination field (currently unused). + /// + /// Should be left empty (`""`). pub dest: String, + + /// Additional parameters encoded as a string (currently unused). + /// + /// Should be left empty (`""`). pub params: String, + + /// Schema used to deserialize the output of the signed transaction. + /// + /// MUST be provided. pub output_deserialization_schema: Vec, + + /// Schema used to serialize the `respond_bidirectional` payload. + /// + /// MUST be provided. pub respond_serialization_schema: Vec, } @@ -664,6 +699,9 @@ fn decode_sign_bidirectional_requested( let output_deserialization_schema = get_named_vec_u8(&fields, "output_deserialization_schema")?; let respond_serialization_schema = get_named_vec_u8(&fields, "respond_serialization_schema")?; + Chain::from_caip2_chain_id(&caip2_id) + .map_err(|e| anyhow!("invalid caip2 chain id in sign bidirectional event: {e:?}"))?; + Ok(HydrationSignBidirectionalRequestedEvent { sender, serialized_transaction, diff --git a/chain-signatures/node/src/indexer_sol.rs b/chain-signatures/node/src/indexer_sol.rs index 9413c9d1d..45cd48e8b 100644 --- a/chain-signatures/node/src/indexer_sol.rs +++ b/chain-signatures/node/src/indexer_sol.rs @@ -568,7 +568,13 @@ fn parse_cpi_events( } } else if event_discriminator == SignBidirectionalEvent::DISCRIMINATOR { match ::deserialize(&mut &event_data[..]) { - Ok(ev) => acc.push(Box::new(ev) as SignatureEventBox), + Ok(ev) => { + if let Err(e) = Chain::from_caip2_chain_id(&ev.caip2_id) { + tracing::warn!("invalid caip2 chain id in sign bidirectional event: {e:?}") + } else { + acc.push(Box::new(ev) as SignatureEventBox) + } + } Err(e) => { tracing::warn!("Failed to deserialize SignBidirectionalEvent: {e}") } diff --git a/chain-signatures/node/src/protocol/signature.rs b/chain-signatures/node/src/protocol/signature.rs index 18bd58c59..df9b98f1d 100644 --- a/chain-signatures/node/src/protocol/signature.rs +++ b/chain-signatures/node/src/protocol/signature.rs @@ -1000,7 +1000,7 @@ impl SignGenerator { tracing::info!( ?sign_id, source_chain = ?self.indexed.chain, - target_chain = ?event.dest(), + target_chain = ?event.target_chain().ok(), "generated signature for bidirectional request, awaiting indexer to process" ); } diff --git a/chain-signatures/node/src/stream/ops.rs b/chain-signatures/node/src/stream/ops.rs index 9aee19925..47e886f5c 100644 --- a/chain-signatures/node/src/stream/ops.rs +++ b/chain-signatures/node/src/stream/ops.rs @@ -15,7 +15,6 @@ use crate::stream::ExecutionOutcome; use anchor_lang::prelude::Pubkey; use k256::Scalar; use mpc_primitives::{SignId, Signature}; -use std::str::FromStr; use std::time::Instant; use tokio::sync::{mpsc, watch}; @@ -128,6 +127,11 @@ impl SignBidirectionalEvent { )), } } + + pub fn target_chain(&self) -> Result { + // we can directly unwrap because we've checked that the chain id is valid during event deserialization in the indexer + Chain::from_caip2_chain_id(&self.caip2_id()) + } } pub enum RespondBidirectionalEvent { @@ -252,7 +256,7 @@ pub(crate) async fn process_sign_event( args: sign_request.args.clone(), unix_timestamp_indexed: sign_request.unix_timestamp_indexed, }), - SignRequestType::SignBidirectional(_event) => { + SignRequestType::SignBidirectional(_) => { // For bidirectional requests, start with a Sign transaction // The protocol will advance it to Bidirectional after generating the signature BacklogTransaction::Sign(SignTx { @@ -302,7 +306,7 @@ pub(crate) async fn process_sign_request( args: sign_request.args.clone(), unix_timestamp_indexed: sign_request.unix_timestamp_indexed, }), - SignRequestType::SignBidirectional(_event) => BacklogTransaction::Sign(SignTx { + SignRequestType::SignBidirectional(_) => BacklogTransaction::Sign(SignTx { request_id: sign_id.request_id, source_chain: sign_request.chain, status: PendingRequestStatus::AwaitingResponse, @@ -418,8 +422,11 @@ pub(crate) async fn process_respond_event( }; tracing::info!(?sign_id, "bidirectional processing initial respond event"); - let target_chain = Chain::from_str(&event.dest()) - .map_err(|err| anyhow::anyhow!("unable to parse target chain from dest: {err:?}"))?; + + let target_chain = event.target_chain().map_err(|err| { + anyhow::anyhow!("failed to process respond event: {err:?} for sign id: {sign_id:?}") + })?; + if !matches!(entry.tx, BacklogTransaction::Sign(_)) { tracing::info!( ?sign_id, diff --git a/chain-signatures/primitives/Cargo.toml b/chain-signatures/primitives/Cargo.toml index 918b5f1e0..56f462feb 100644 --- a/chain-signatures/primitives/Cargo.toml +++ b/chain-signatures/primitives/Cargo.toml @@ -12,6 +12,7 @@ near-sdk.workspace = true serde.workspace = true serde_bytes.workspace = true sha3.workspace = true +thiserror.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2.12", features = ["custom"] } diff --git a/chain-signatures/primitives/src/lib.rs b/chain-signatures/primitives/src/lib.rs index 5c8169a1e..83c5b6a6c 100644 --- a/chain-signatures/primitives/src/lib.rs +++ b/chain-signatures/primitives/src/lib.rs @@ -155,6 +155,14 @@ pub enum Chain { Hydration, } +#[derive(Debug, PartialEq, Eq, Clone, thiserror::Error)] +pub enum ChainFromError { + #[error("unknown CAIP-2 chain ID: {0}")] + UnknownCaip2Id(String), + #[error("unknown deprecated chain ID: {0}")] + UnknownDeprecatedId(String), +} + impl Chain { pub const fn as_str(&self) -> &'static str { match self { @@ -232,6 +240,20 @@ impl Chain { pub fn expected_response_time_secs(&self) -> u64 { self.expected_finality_time_secs() + 60 } + + pub fn from_caip2_chain_id(chain_id: &str) -> Result { + Self::iter() + .into_iter() + .find(|chain| chain.caip2_chain_id() == chain_id) + .ok_or_else(|| ChainFromError::UnknownCaip2Id(chain_id.to_string())) + } + + pub fn from_deprecated_chain_id(chain_id: &str) -> Result { + Self::iter() + .into_iter() + .find(|chain| chain.deprecated_chain_id() == chain_id) + .ok_or_else(|| ChainFromError::UnknownDeprecatedId(chain_id.to_string())) + } } impl fmt::Display for Chain {