diff --git a/Cargo.lock b/Cargo.lock index 4cdc5215..65a651cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8197,6 +8197,7 @@ dependencies = [ "serde", "sha2", "sled", + "strata-bridge-primitives", "strata-key-derivation", "terrors", "tokio", @@ -8215,6 +8216,7 @@ dependencies = [ "quinn", "rkyv", "secret-service-proto", + "strata-bridge-primitives", "terrors", "tokio", "tracing", @@ -8228,6 +8230,7 @@ dependencies = [ "musig2 0.1.0", "quinn", "rkyv", + "strata-bridge-primitives", ] [[package]] @@ -8242,6 +8245,7 @@ dependencies = [ "quinn", "rkyv", "secret-service-proto", + "strata-bridge-primitives", "terrors", "tokio", "tracing", diff --git a/crates/secret-service-client/Cargo.toml b/crates/secret-service-client/Cargo.toml index e01ceeb2..87b27a38 100644 --- a/crates/secret-service-client/Cargo.toml +++ b/crates/secret-service-client/Cargo.toml @@ -10,6 +10,7 @@ musig2 = { path = "../musig2" } quinn.workspace = true rkyv.workspace = true secret-service-proto = { version = "0.1.0", path = "../secret-service-proto" } +strata-bridge-primitives.workspace = true terrors.workspace = true tokio.workspace = true tracing.workspace = true diff --git a/crates/secret-service-client/src/lib.rs b/crates/secret-service-client/src/lib.rs index 33d0f3d7..8df7802c 100644 --- a/crates/secret-service-client/src/lib.rs +++ b/crates/secret-service-client/src/lib.rs @@ -31,6 +31,7 @@ use secret_service_proto::{ WireMessage, }, }; +use strata_bridge_primitives::scripts::taproot::TaprootWitness; use terrors::OneOf; use tokio::time::timeout; @@ -428,12 +429,15 @@ struct Musig2Client { impl Musig2Signer for Musig2Client { fn new_session( &self, - ctx: KeyAggContext, - signer_idx: usize, + pubkeys: Vec, + witness: TaprootWitness, ) -> impl Future, ClientError>> + Send { async move { - let msg = ClientMessage::Musig2NewSession { ctx, signer_idx }; + let msg = ClientMessage::Musig2NewSession { + pubkeys: pubkeys.into_iter().map(|pk| pk.serialize()).collect(), + witness: witness.into(), + }; let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; let ServerMessage::Musig2NewSession(maybe_session_id) = res else { return Err(ClientError::ProtocolError(res)); @@ -449,6 +453,18 @@ impl Musig2Signer for Musig2Client { }) } } + + fn pubkey(&self) -> impl Future::Container> + Send { + async move { + let msg = ClientMessage::Musig2Pubkey; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; + let ServerMessage::Musig2Pubkey { pubkey } = res else { + return Err(ClientError::ProtocolError(res)); + }; + + PublicKey::from_slice(&pubkey).map_err(|_| ClientError::ProtocolError(res)) + } + } } struct WotsClient { diff --git a/crates/secret-service-proto/Cargo.toml b/crates/secret-service-proto/Cargo.toml index 54ec29ba..a5168bca 100644 --- a/crates/secret-service-proto/Cargo.toml +++ b/crates/secret-service-proto/Cargo.toml @@ -8,3 +8,4 @@ bitcoin.workspace = true musig2 = { path = "../musig2" } quinn.workspace = true rkyv.workspace = true +strata-bridge-primitives.workspace = true diff --git a/crates/secret-service-proto/src/v1/traits.rs b/crates/secret-service-proto/src/v1/traits.rs index 9844dd03..9be9655f 100644 --- a/crates/secret-service-proto/src/v1/traits.rs +++ b/crates/secret-service-proto/src/v1/traits.rs @@ -8,6 +8,7 @@ use musig2::{ }; use quinn::{ConnectionError, ReadExactError, WriteError}; use rkyv::{rancor, Archive, Deserialize, Serialize}; +use strata_bridge_primitives::scripts::taproot::TaprootWitness; use super::wire::ServerMessage; @@ -63,9 +64,10 @@ pub struct SignerIdxOutOfBounds { pub trait Musig2Signer: Send + Sync { fn new_session( &self, - ctx: KeyAggContext, - signer_idx: usize, + pubkeys: Vec, + witness: TaprootWitness, ) -> impl Future>> + Send; + fn pubkey(&self) -> impl Future> + Send; } pub trait Musig2SignerFirstRound: Send + Sync { diff --git a/crates/secret-service-proto/src/v1/wire.rs b/crates/secret-service-proto/src/v1/wire.rs index f5cec60c..3f09261a 100644 --- a/crates/secret-service-proto/src/v1/wire.rs +++ b/crates/secret-service-proto/src/v1/wire.rs @@ -1,8 +1,14 @@ +use bitcoin::{ + hashes::Hash, + taproot::{ControlBlock, TaprootError}, + ScriptBuf, TapNodeHash, +}; use musig2::{ errors::{RoundContributionError, RoundFinalizeError}, KeyAggContext, }; use rkyv::{with::Map, Archive, Deserialize, Serialize}; +use strata_bridge_primitives::scripts::taproot::TaprootWitness; use super::traits::{Musig2SessionId, SignerIdxOutOfBounds}; @@ -26,6 +32,9 @@ pub enum ServerMessage { }, Musig2NewSession(Result), + Musig2Pubkey { + pubkey: [u8; 33], + }, Musig2FirstRoundOurNonce { our_nonce: [u8; 66], @@ -110,9 +119,10 @@ pub enum ClientMessage { P2PPubkey, Musig2NewSession { - ctx: KeyAggContext, - signer_idx: usize, + pubkeys: Vec<[u8; 33]>, + witness: SerializableTaprootWitness, }, + Musig2Pubkey, Musig2FirstRoundOurNonce { session_id: usize, @@ -163,3 +173,62 @@ pub enum ClientMessage { deposit_idx: u64, }, } + +#[derive(Debug, Clone, Archive, Serialize, Deserialize)] +pub enum SerializableTaprootWitness { + Key, + Script { + script_buf: Vec, + control_block: Vec, + }, + Tweaked { + tweak: [u8; 32], + }, +} + +impl From for SerializableTaprootWitness { + fn from(witness: TaprootWitness) -> Self { + match witness { + TaprootWitness::Key => SerializableTaprootWitness::Key, + TaprootWitness::Script { + script_buf, + control_block, + } => SerializableTaprootWitness::Script { + script_buf: script_buf.into_bytes(), + control_block: control_block.serialize(), + }, + TaprootWitness::Tweaked { tweak } => SerializableTaprootWitness::Tweaked { + tweak: tweak.to_raw_hash().to_byte_array(), + }, + } + } +} + +pub enum TaprootWitnessError { + InvalidWitnessType, + InvalidScriptControlBlock(TaprootError), +} + +impl TryFrom for TaprootWitness { + type Error = TaprootWitnessError; + fn try_from(value: SerializableTaprootWitness) -> Result { + match value { + SerializableTaprootWitness::Key => Ok(TaprootWitness::Key), + SerializableTaprootWitness::Script { + script_buf, + control_block, + } => { + let script_buf = ScriptBuf::from_bytes(script_buf); + let control_block = ControlBlock::decode(&control_block) + .map_err(TaprootWitnessError::InvalidScriptControlBlock)?; + Ok(TaprootWitness::Script { + script_buf, + control_block, + }) + } + SerializableTaprootWitness::Tweaked { tweak } => Ok(TaprootWitness::Tweaked { + tweak: TapNodeHash::from_byte_array(tweak), + }), + } + } +} diff --git a/crates/secret-service-server/Cargo.toml b/crates/secret-service-server/Cargo.toml index dece1dc5..4dde12d0 100644 --- a/crates/secret-service-server/Cargo.toml +++ b/crates/secret-service-server/Cargo.toml @@ -12,6 +12,7 @@ parking_lot.workspace = true quinn.workspace = true rkyv.workspace = true secret-service-proto = { version = "0.1.0", path = "../secret-service-proto" } +strata-bridge-primitives.workspace = true terrors.workspace = true tokio.workspace = true tracing.workspace = true diff --git a/crates/secret-service-server/src/lib.rs b/crates/secret-service-server/src/lib.rs index 0c45f7cb..cfc2d1db 100644 --- a/crates/secret-service-server/src/lib.rs +++ b/crates/secret-service-server/src/lib.rs @@ -34,6 +34,7 @@ use secret_service_proto::{ }, wire::{ArchivedVersionedClientMessage, LengthUint, VersionedServerMessage, WireMessage}, }; +use strata_bridge_primitives::scripts::taproot::TaprootWitness; use terrors::OneOf; use tokio::{ sync::Mutex, @@ -238,15 +239,25 @@ where } } - ArchivedClientMessage::Musig2NewSession { ctx, signer_idx } => 'block: { + ArchivedClientMessage::Musig2NewSession { pubkeys, witness } => 'block: { let signer = service.musig2_signer(); - let Ok(ctx) = deserialize::(ctx) else { + let Ok(ser_witness) = deserialize::<_, rancor::Error>(witness) else { break 'block ServerMessage::InvalidClientMessage; }; - let first_round = match signer - .new_session(ctx, signer_idx.to_native() as usize) - .await - { + let Ok(witness) = TaprootWitness::try_from(ser_witness) + .map_err(|_| ServerMessage::InvalidClientMessage) + else { + break 'block ServerMessage::InvalidClientMessage; + }; + let Ok(pubkeys) = pubkeys + .into_iter() + .map(|data| PublicKey::from_slice(data)) + .collect::, _>>() + else { + break 'block ServerMessage::InvalidClientMessage; + }; + + let first_round = match signer.new_session(pubkeys, witness).await { Ok(fr) => fr, Err(e) => break 'block ServerMessage::Musig2NewSession(Err(e)), }; @@ -266,6 +277,10 @@ where ServerMessage::Musig2NewSession(Ok(write_perm.session_id())) } + ArchivedClientMessage::Musig2Pubkey => ServerMessage::Musig2Pubkey { + pubkey: service.musig2_signer().pubkey().await.serialize(), + }, + ArchivedClientMessage::Musig2FirstRoundOurNonce { session_id } => { let r = musig2_sm .lock() diff --git a/crates/secret-service/Cargo.toml b/crates/secret-service/Cargo.toml index abb34aaf..3f0a06bd 100644 --- a/crates/secret-service/Cargo.toml +++ b/crates/secret-service/Cargo.toml @@ -18,6 +18,7 @@ secret-service-server = { version = "0.1.0", path = "../secret-service-server" } serde.workspace = true sha2.workspace = true sled = "0.34.7" +strata-bridge-primitives.workspace = true strata-key-derivation = { git = "https://github.com/alpenlabs/strata", version = "0.1.0" } terrors.workspace = true diff --git a/crates/secret-service/src/disk/musig2.rs b/crates/secret-service/src/disk/musig2.rs index 18eccb8a..6d866acd 100644 --- a/crates/secret-service/src/disk/musig2.rs +++ b/crates/secret-service/src/disk/musig2.rs @@ -1,8 +1,9 @@ use std::future::Future; +use bitcoin::key::Keypair; use musig2::{ errors::{RoundContributionError, RoundFinalizeError}, - secp256k1::{PublicKey, SecretKey}, + secp256k1::{PublicKey, SecretKey, SECP256K1}, FirstRound, KeyAggContext, LiftedSignature, SecNonceSpices, SecondRound, }; use rand::{thread_rng, Rng}; @@ -13,32 +14,58 @@ use secret_service_proto::v1::traits::{ }; use secret_service_server::RoundPersister; use sled::Tree; +use strata_bridge_primitives::scripts::taproot::TaprootWitness; use terrors::OneOf; pub struct Ms2Signer { - key: SecretKey, + kp: Keypair, } impl Ms2Signer { pub fn new(key: SecretKey) -> Self { - Self { key } + Self { + kp: Keypair::from_secret_key(SECP256K1, &key), + } } } impl Musig2Signer for Ms2Signer { fn new_session( &self, - ctx: KeyAggContext, - signer_idx: usize, + mut pubkeys: Vec, + witness: TaprootWitness, ) -> impl Future> + Send { async move { let nonce_seed = thread_rng().gen::<[u8; 32]>(); - let ordered_public_keys = ctx.pubkeys().iter().cloned().map(|p| p.into()).collect(); + if !pubkeys.contains(&self.kp.public_key()) { + pubkeys.push(self.kp.public_key()); + } + pubkeys.sort(); + let signer_index = pubkeys + .iter() + .position(|pk| pk == &self.kp.public_key()) + .unwrap(); + let mut ctx = KeyAggContext::new(pubkeys.clone()).unwrap(); + + match witness { + TaprootWitness::Key => { + ctx = ctx + .with_unspendable_taproot_tweak() + .expect("must be able to tweak the key agg context") + } + TaprootWitness::Tweaked { tweak } => { + ctx = ctx + .with_taproot_tweak(tweak.as_ref()) + .expect("must be able to tweak the key agg context") + } + _ => {} + } + let first_round = FirstRound::new( ctx, nonce_seed, - signer_idx, - SecNonceSpices::new().with_seckey(self.key.clone()), + signer_index, + SecNonceSpices::new().with_seckey(self.kp.secret_key()), ) .map_err(|e| SignerIdxOutOfBounds { index: e.index, @@ -46,11 +73,15 @@ impl Musig2Signer for Ms2Signer { })?; Ok(ServerFirstRound { first_round, - ordered_public_keys, - seckey: self.key.clone(), + ordered_public_keys: pubkeys, + seckey: self.kp.secret_key(), }) } } + + fn pubkey(&self) -> impl Future::Container> + Send { + async move { self.kp.public_key() } + } } pub struct SledRoundPersist {