diff --git a/ark-lib/src/tree/signed.rs b/ark-lib/src/tree/signed.rs index e9024ba..18ed174 100644 --- a/ark-lib/src/tree/signed.rs +++ b/ark-lib/src/tree/signed.rs @@ -23,7 +23,7 @@ const NODE3_TX_VSIZE: u64 = 197; const NODE4_TX_VSIZE: u64 = 240; -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] pub struct VtxoTreeSpec { pub cosigners: Vec, pub vtxos: Vec, @@ -251,7 +251,7 @@ impl VtxoTreeSpec { } } -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] pub struct SignedVtxoTree { pub spec: VtxoTreeSpec, pub utxo: OutPoint, @@ -286,7 +286,7 @@ impl SignedVtxoTree { } /// Validate the signatures. - pub fn validate(&self) -> Result<(), String> { + pub fn validate_signatures(&self) -> Result<(), String> { let pk = self.spec.cosign_taproot().output_key().to_inner(); let sighashes = self.spec.sighashes(self.utxo); for (i, (sighash, sig)) in sighashes.into_iter().rev().zip(self.signatures.iter()).enumerate() { diff --git a/arkd-rpc-client/src/arkd.rs b/arkd-rpc-client/src/arkd.rs index 63c5aa3..8e0d78d 100644 --- a/arkd-rpc-client/src/arkd.rs +++ b/arkd-rpc-client/src/arkd.rs @@ -63,17 +63,30 @@ pub struct ForfeitNonces { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct RoundProposal { +pub struct VtxoProposal { #[prost(uint64, tag = "1")] pub round_id: u64, #[prost(bytes = "vec", tag = "2")] pub vtxos_spec: ::prost::alloc::vec::Vec, + /// / The unsigned round tx. #[prost(bytes = "vec", tag = "3")] pub round_tx: ::prost::alloc::vec::Vec, #[prost(bytes = "vec", repeated, tag = "4")] pub vtxos_signers: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, #[prost(bytes = "vec", repeated, tag = "5")] pub vtxos_agg_nonces: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RoundProposal { + #[prost(uint64, tag = "1")] + pub round_id: u64, + /// / Completely signed vtxo tree. + #[prost(bytes = "vec", tag = "2")] + pub signed_vtxos: ::prost::alloc::vec::Vec, + /// / The unsigned round tx. + #[prost(bytes = "vec", tag = "3")] + pub round_tx: ::prost::alloc::vec::Vec, #[prost(message, repeated, tag = "6")] pub forfeit_nonces: ::prost::alloc::vec::Vec, } @@ -82,15 +95,17 @@ pub struct RoundProposal { pub struct RoundFinished { #[prost(uint64, tag = "1")] pub round_id: u64, + /// / Completely signed vtxo tree. #[prost(bytes = "vec", tag = "2")] pub signed_vtxos: ::prost::alloc::vec::Vec, + /// / The signed round tx. #[prost(bytes = "vec", tag = "3")] pub round_tx: ::prost::alloc::vec::Vec, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct RoundEvent { - #[prost(oneof = "round_event::Event", tags = "1, 2, 3")] + #[prost(oneof = "round_event::Event", tags = "1, 2, 3, 4")] pub event: ::core::option::Option, } /// Nested message and enum types in `RoundEvent`. @@ -101,8 +116,10 @@ pub mod round_event { #[prost(message, tag = "1")] Start(super::RoundStart), #[prost(message, tag = "2")] - Proposal(super::RoundProposal), + VtxoProposal(super::VtxoProposal), #[prost(message, tag = "3")] + RoundProposal(super::RoundProposal), + #[prost(message, tag = "4")] Finished(super::RoundFinished), } } @@ -150,7 +167,14 @@ pub struct ForfeitSignatures { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct VtxoSignatures { +pub struct ForfeitSignaturesRequest { + #[prost(message, repeated, tag = "1")] + pub signatures: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct VtxoSignaturesRequest { + /// / The cosign pubkey these signatures are for. #[prost(bytes = "vec", tag = "1")] pub pubkey: ::prost::alloc::vec::Vec, #[prost(bytes = "vec", repeated, tag = "2")] @@ -158,14 +182,6 @@ pub struct VtxoSignatures { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct RoundSignatures { - #[prost(message, repeated, tag = "1")] - pub forfeit: ::prost::alloc::vec::Vec, - #[prost(message, optional, tag = "2")] - pub vtxo: ::core::option::Option, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] pub struct Empty {} /// Generated client implementations. pub mod ark_service_client { @@ -388,9 +404,9 @@ pub mod ark_service_client { .insert(GrpcMethod::new("arkd.ArkService", "SubmitPayment")); self.inner.unary(req, path, codec).await } - pub async fn provide_signatures( + pub async fn provide_vtxo_signatures( &mut self, - request: impl tonic::IntoRequest, + request: impl tonic::IntoRequest, ) -> std::result::Result, tonic::Status> { self.inner .ready() @@ -403,11 +419,33 @@ pub mod ark_service_client { })?; let codec = tonic::codec::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static( - "/arkd.ArkService/ProvideSignatures", + "/arkd.ArkService/ProvideVtxoSignatures", ); let mut req = request.into_request(); req.extensions_mut() - .insert(GrpcMethod::new("arkd.ArkService", "ProvideSignatures")); + .insert(GrpcMethod::new("arkd.ArkService", "ProvideVtxoSignatures")); + self.inner.unary(req, path, codec).await + } + pub async fn provide_forfeit_signatures( + &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( + "/arkd.ArkService/ProvideForfeitSignatures", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("arkd.ArkService", "ProvideForfeitSignatures")); self.inner.unary(req, path, codec).await } } diff --git a/arkd/rpc-protos/arkd.proto b/arkd/rpc-protos/arkd.proto index 7b199c4..d0ace73 100644 --- a/arkd/rpc-protos/arkd.proto +++ b/arkd/rpc-protos/arkd.proto @@ -14,7 +14,8 @@ service ArkService { rpc SubscribeRounds(Empty) returns (stream RoundEvent) {} rpc SubmitPayment(SubmitPaymentRequest) returns (Empty) {} - rpc ProvideSignatures(RoundSignatures) returns (Empty) {} + rpc ProvideVtxoSignatures(VtxoSignaturesRequest) returns (Empty) {} + rpc ProvideForfeitSignatures(ForfeitSignaturesRequest) returns (Empty) {} } message ArkInfo { @@ -57,26 +58,38 @@ message ForfeitNonces { repeated bytes pub_nonces = 2; } -message RoundProposal { +message VtxoProposal { uint64 round_id = 1; bytes vtxos_spec = 2; + /// The unsigned round tx. bytes round_tx = 3; repeated bytes vtxos_signers = 4; repeated bytes vtxos_agg_nonces = 5; +} + +message RoundProposal { + uint64 round_id = 1; + /// Completely signed vtxo tree. + bytes signed_vtxos = 2; + /// The unsigned round tx. + bytes round_tx = 3; repeated ForfeitNonces forfeit_nonces = 6; } message RoundFinished { uint64 round_id = 1; + /// Completely signed vtxo tree. bytes signed_vtxos = 2; + /// The signed round tx. bytes round_tx = 3; } message RoundEvent { oneof event { RoundStart start = 1; - RoundProposal proposal = 2; - RoundFinished finished = 3; + VtxoProposal vtxo_proposal = 2; + RoundProposal round_proposal = 3; + RoundFinished finished = 4; }; } @@ -102,14 +115,14 @@ message ForfeitSignatures { repeated bytes signatures = 3; } -message VtxoSignatures { - bytes pubkey = 1; - repeated bytes signatures = 2; +message ForfeitSignaturesRequest { + repeated ForfeitSignatures signatures = 1; } -message RoundSignatures { - repeated ForfeitSignatures forfeit = 1; - VtxoSignatures vtxo = 2; +message VtxoSignaturesRequest { + /// The cosign pubkey these signatures are for. + bytes pubkey = 1; + repeated bytes signatures = 2; } diff --git a/arkd/src/round/mod.rs b/arkd/src/round/mod.rs index ffb4d91..7784929 100644 --- a/arkd/src/round/mod.rs +++ b/arkd/src/round/mod.rs @@ -24,18 +24,23 @@ pub enum RoundEvent { id: u64, offboard_feerate: FeeRate, }, - Proposal { + VtxoProposal { id: u64, - vtxos_spec: VtxoTreeSpec, round_tx: Transaction, + vtxos_spec: VtxoTreeSpec, vtxos_signers: Vec, vtxos_agg_nonces: Vec, + }, + RoundProposal { + id: u64, + round_tx: Transaction, + vtxos: SignedVtxoTree, forfeit_nonces: HashMap>, }, Finished { id: u64, - vtxos: SignedVtxoTree, round_tx: Transaction, + vtxos: SignedVtxoTree, }, } @@ -48,10 +53,12 @@ pub enum RoundInput { cosign_pubkey: PublicKey, public_nonces: Vec, }, - Signatures { - vtxo_pubkey: PublicKey, - vtxo_signatures: Vec, - forfeit: HashMap, Vec)>, + VtxoSignatures { + pubkey: PublicKey, + signatures: Vec, + }, + ForfeitSignatures { + signatures: HashMap, Vec)>, }, } @@ -249,9 +256,10 @@ pub async fn run_round_scheduler( }); } - // Start vtxo tree and connector chain construction - // + // **************************************************************** + // * Vtxo tree construction and signing + // * // * - We will always store vtxo tx data from top to bottom, // * meaning from the root tx down to the leaves. // **************************************************************** @@ -317,6 +325,96 @@ pub async fn run_round_scheduler( let vtxo_sighashes = vtxos_spec.sighashes(vtxos_utxo); assert_eq!(vtxo_sighashes.len(), agg_vtxo_nonces.len()); + // Send out vtxo proposal to signers. + let _ = app.round_event_tx.send(RoundEvent::VtxoProposal { + id: round_id, + round_tx: round_tx.clone(), + vtxos_spec: vtxos_spec.clone(), + vtxos_signers: cosigners.iter().copied().collect(), + vtxos_agg_nonces: agg_vtxo_nonces.clone(), + }); + + // Wait for signatures from users. + //TODO(stevenroose) we need a check to see when we have all data we need so we can skip + // timeout + let mut vtxo_part_sigs = HashMap::with_capacity(cosigners.len()); + tokio::pin! { let timeout = tokio::time::sleep(cfg.round_sign_time); } + 'receive: loop { + tokio::select! { + _ = &mut timeout => break 'receive, + input = round_input_rx.recv() => match input.expect("broken channel") { + RoundInput::VtxoSignatures { pubkey, signatures } => { + if !cosigners.contains(&pubkey) { + debug!("Received signatures from non-signer: {}", pubkey); + continue 'receive; + } + trace!("Received signatures from cosigner {}", pubkey); + + if validate_partial_vtxo_sigs( + cosigners.iter().copied(), + &agg_vtxo_nonces, + &vtxo_sighashes, + vtxos_spec.cosign_taptweak().to_byte_array(), + pubkey, + vtxo_pub_nonces.get(&pubkey).expect("user is cosigner"), + &signatures, + ) { + vtxo_part_sigs.insert(pubkey, signatures); + } else { + debug!("Received invalid partial vtxo sigs from signer: {}", pubkey); + continue 'receive; + } + }, + v => debug!("Received unexpected input: {:?}", v), + } + } + } + + //TODO(stevenroose) kick out signers that didn't sign and retry + if cosigners.len() - 1 != vtxo_part_sigs.len() { + error!("Not enough vtxo partial signatures! ({} != {})", + cosigners.len() - 1, vtxo_part_sigs.len()); + continue 'round; + } + + // Combine the vtxo signatures. + #[cfg(debug_assertions)] + let mut partial_sigs = Vec::with_capacity(nb_nodes); + let mut final_vtxo_sigs = Vec::with_capacity(nb_nodes); + for (i, sec_nonce) in sec_vtxo_nonces.into_iter().enumerate() { + let others = vtxo_part_sigs.values().map(|s| s[i].clone()).collect::>(); + let (_partial, final_sig) = musig::partial_sign( + cosigners.iter().copied(), + agg_vtxo_nonces[i], + &cosign_key, + sec_nonce, + vtxo_sighashes[i].to_byte_array(), + Some(vtxos_spec.cosign_taptweak().to_byte_array()), + Some(&others), + ); + final_vtxo_sigs.push(final_sig.expect("we provided others")); + #[cfg(debug_assertions)] + partial_sigs.push(_partial); + } + debug_assert!(validate_partial_vtxo_sigs( + cosigners.iter().copied(), + &agg_vtxo_nonces, + &vtxo_sighashes, + vtxos_spec.cosign_taptweak().to_byte_array(), + cosign_key.public_key(), + &pub_vtxo_nonces, + &partial_sigs, + ), "our own partial signatures were wrong"); + + // Then construct the final signed vtxo tree. + let signed_vtxos = SignedVtxoTree::new(vtxos_spec, vtxos_utxo, final_vtxo_sigs); + debug_assert!(signed_vtxos.validate_signatures().is_ok(), "invalid signed vtxo tree"); + + + // **************************************************************** + // * Broadcast signed vtxo tree and gather forfeit signatures + // **************************************************************** + // Prepare nonces for forfeit txs. // We need to prepare N nonces for each of N inputs. let mut forfeit_pub_nonces = HashMap::with_capacity(all_inputs.len()); @@ -333,51 +431,27 @@ pub async fn run_round_scheduler( forfeit_sec_nonces.insert(input.id(), secs); } - // Send out proposal to signers. - let _ = app.round_event_tx.send(RoundEvent::Proposal { + // Send out round proposal to signers. + let _ = app.round_event_tx.send(RoundEvent::RoundProposal { id: round_id, - vtxos_spec: vtxos_spec.clone(), round_tx: round_tx.clone(), - vtxos_signers: cosigners.iter().copied().collect(), - vtxos_agg_nonces: agg_vtxo_nonces.clone(), + vtxos: signed_vtxos.clone(), forfeit_nonces: forfeit_pub_nonces.clone(), }); // Wait for signatures from users. //TODO(stevenroose) we need a check to see when we have all data we need so we can skip // timeout - let mut vtxo_part_sigs = HashMap::with_capacity(cosigners.len()); let mut forfeit_part_sigs = HashMap::with_capacity(all_inputs.len()); tokio::pin! { let timeout = tokio::time::sleep(cfg.round_sign_time); } 'receive: loop { tokio::select! { _ = &mut timeout => break 'receive, input = round_input_rx.recv() => match input.expect("broken channel") { - RoundInput::Signatures { vtxo_pubkey, vtxo_signatures, forfeit } => { - if !cosigners.contains(&vtxo_pubkey) { - debug!("Received signatures from non-signer: {}", vtxo_pubkey); - continue 'receive; - } - trace!("Received signatures from cosigner {}", vtxo_pubkey); - - if validate_partial_vtxo_sigs( - cosigners.iter().copied(), - &agg_vtxo_nonces, - &vtxo_sighashes, - vtxos_spec.cosign_taptweak().to_byte_array(), - vtxo_pubkey, - vtxo_pub_nonces.get(&vtxo_pubkey).expect("user is cosigner"), - &vtxo_signatures, - ) { - vtxo_part_sigs.insert(vtxo_pubkey, vtxo_signatures); - } else { - debug!("Received invalid partial vtxo sigs from signer: {}", vtxo_pubkey); - continue 'receive; - } - + RoundInput::ForfeitSignatures { signatures } => { //TODO(stevenroose) validate forfeit txs let mut ok = true; - for (id, (nonces, sigs)) in &forfeit { + for (id, (nonces, sigs)) in &signatures { if nonces.len() != all_inputs.len() || sigs.len() != all_inputs.len() { warn!("User didn't provide enough forfeit sigs for {}", id); ok = false; @@ -386,7 +460,7 @@ pub async fn run_round_scheduler( if ok { //TODO(stevenroose) actually check if the forfeit sigs are //for actual inputs in the round - forfeit_part_sigs.extend(forfeit.into_iter()); + forfeit_part_sigs.extend(signatures.into_iter()); } // Check whether we have all and can skip the loop. @@ -402,11 +476,6 @@ pub async fn run_round_scheduler( } //TODO(stevenroose) kick out signers that didn't sign and retry - if cosigners.len() - 1 != vtxo_part_sigs.len() { - error!("Not enough vtxo partial signatures! ({} != {})", - cosigners.len() - 1, vtxo_part_sigs.len()); - continue 'round; - } if forfeit_part_sigs.len() != all_inputs.len() { error!("Not enough forfeit partial signatures! ({} != {})", forfeit_part_sigs.len(), all_inputs.len()); @@ -447,42 +516,12 @@ pub async fn run_round_scheduler( } //TODO(stevenroose) if missing forfeits, ban inputs and restart round - // Combine the vtxo signatures. - #[cfg(debug_assertions)] - let mut partial_sigs = Vec::with_capacity(nb_nodes); - let mut final_vtxo_sigs = Vec::with_capacity(nb_nodes); - for (i, sec_nonce) in sec_vtxo_nonces.into_iter().enumerate() { - let others = vtxo_part_sigs.values().map(|s| s[i].clone()).collect::>(); - let (_partial, final_sig) = musig::partial_sign( - cosigners.iter().copied(), - agg_vtxo_nonces[i], - &cosign_key, - sec_nonce, - vtxo_sighashes[i].to_byte_array(), - Some(vtxos_spec.cosign_taptweak().to_byte_array()), - Some(&others), - ); - final_vtxo_sigs.push(final_sig.expect("we provided others")); - #[cfg(debug_assertions)] - partial_sigs.push(_partial); - } - debug_assert!(validate_partial_vtxo_sigs( - cosigners.iter().copied(), - &agg_vtxo_nonces, - &vtxo_sighashes, - vtxos_spec.cosign_taptweak().to_byte_array(), - cosign_key.public_key(), - &pub_vtxo_nonces, - &partial_sigs, - ), "our own partial signatures were wrong"); - - // Then construct the final signed vtxo tree. - let signed_vtxos = SignedVtxoTree::new(vtxos_spec, vtxos_utxo, final_vtxo_sigs); - if let Err(e) = signed_vtxos.validate() { - bail!("We created an incorrect vtxo tree: {}", e); - } - // And sign the on-chain tx. + // **************************************************************** + // * Finish the round + // **************************************************************** + + // Sign the on-chain tx. let finalized = wallet.sign(&mut round_tx_psbt, bdk::SignOptions::default())?; assert!(finalized); let round_tx = round_tx_psbt.extract_tx(); @@ -504,7 +543,6 @@ pub async fn run_round_scheduler( round_tx: round_tx.clone(), }); - // Store forfeit txs and round info in database. let round_id = round_tx.txid(); for vtxo in all_inputs { diff --git a/arkd/src/rpc/arkd.rs b/arkd/src/rpc/arkd.rs index fe0dbf6..f5e338b 100644 --- a/arkd/src/rpc/arkd.rs +++ b/arkd/src/rpc/arkd.rs @@ -63,17 +63,30 @@ pub struct ForfeitNonces { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct RoundProposal { +pub struct VtxoProposal { #[prost(uint64, tag = "1")] pub round_id: u64, #[prost(bytes = "vec", tag = "2")] pub vtxos_spec: ::prost::alloc::vec::Vec, + /// / The unsigned round tx. #[prost(bytes = "vec", tag = "3")] pub round_tx: ::prost::alloc::vec::Vec, #[prost(bytes = "vec", repeated, tag = "4")] pub vtxos_signers: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, #[prost(bytes = "vec", repeated, tag = "5")] pub vtxos_agg_nonces: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RoundProposal { + #[prost(uint64, tag = "1")] + pub round_id: u64, + /// / Completely signed vtxo tree. + #[prost(bytes = "vec", tag = "2")] + pub signed_vtxos: ::prost::alloc::vec::Vec, + /// / The unsigned round tx. + #[prost(bytes = "vec", tag = "3")] + pub round_tx: ::prost::alloc::vec::Vec, #[prost(message, repeated, tag = "6")] pub forfeit_nonces: ::prost::alloc::vec::Vec, } @@ -82,15 +95,17 @@ pub struct RoundProposal { pub struct RoundFinished { #[prost(uint64, tag = "1")] pub round_id: u64, + /// / Completely signed vtxo tree. #[prost(bytes = "vec", tag = "2")] pub signed_vtxos: ::prost::alloc::vec::Vec, + /// / The signed round tx. #[prost(bytes = "vec", tag = "3")] pub round_tx: ::prost::alloc::vec::Vec, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct RoundEvent { - #[prost(oneof = "round_event::Event", tags = "1, 2, 3")] + #[prost(oneof = "round_event::Event", tags = "1, 2, 3, 4")] pub event: ::core::option::Option, } /// Nested message and enum types in `RoundEvent`. @@ -101,8 +116,10 @@ pub mod round_event { #[prost(message, tag = "1")] Start(super::RoundStart), #[prost(message, tag = "2")] - Proposal(super::RoundProposal), + VtxoProposal(super::VtxoProposal), #[prost(message, tag = "3")] + RoundProposal(super::RoundProposal), + #[prost(message, tag = "4")] Finished(super::RoundFinished), } } @@ -150,7 +167,14 @@ pub struct ForfeitSignatures { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct VtxoSignatures { +pub struct ForfeitSignaturesRequest { + #[prost(message, repeated, tag = "1")] + pub signatures: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct VtxoSignaturesRequest { + /// / The cosign pubkey these signatures are for. #[prost(bytes = "vec", tag = "1")] pub pubkey: ::prost::alloc::vec::Vec, #[prost(bytes = "vec", repeated, tag = "2")] @@ -158,14 +182,6 @@ pub struct VtxoSignatures { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct RoundSignatures { - #[prost(message, repeated, tag = "1")] - pub forfeit: ::prost::alloc::vec::Vec, - #[prost(message, optional, tag = "2")] - pub vtxo: ::core::option::Option, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] pub struct Empty {} /// Generated server implementations. pub mod ark_service_server { @@ -210,9 +226,13 @@ pub mod ark_service_server { &self, request: tonic::Request, ) -> std::result::Result, tonic::Status>; - async fn provide_signatures( + async fn provide_vtxo_signatures( &self, - request: tonic::Request, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + async fn provide_forfeit_signatures( + &self, + request: tonic::Request, ) -> std::result::Result, tonic::Status>; } /// / Public ark service for arkd. @@ -567,13 +587,13 @@ pub mod ark_service_server { }; Box::pin(fut) } - "/arkd.ArkService/ProvideSignatures" => { + "/arkd.ArkService/ProvideVtxoSignatures" => { #[allow(non_camel_case_types)] - struct ProvideSignaturesSvc(pub Arc); + struct ProvideVtxoSignaturesSvc(pub Arc); impl< T: ArkService, - > tonic::server::UnaryService - for ProvideSignaturesSvc { + > tonic::server::UnaryService + for ProvideVtxoSignaturesSvc { type Response = super::Empty; type Future = BoxFuture< tonic::Response, @@ -581,11 +601,12 @@ pub mod ark_service_server { >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::provide_signatures(&inner, request).await + ::provide_vtxo_signatures(&inner, request) + .await }; Box::pin(fut) } @@ -597,7 +618,57 @@ pub mod ark_service_server { let inner = self.inner.clone(); let fut = async move { let inner = inner.0; - let method = ProvideSignaturesSvc(inner); + let method = ProvideVtxoSignaturesSvc(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) + } + "/arkd.ArkService/ProvideForfeitSignatures" => { + #[allow(non_camel_case_types)] + struct ProvideForfeitSignaturesSvc(pub Arc); + impl< + T: ArkService, + > tonic::server::UnaryService + for ProvideForfeitSignaturesSvc { + type Response = super::Empty; + 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 { + ::provide_forfeit_signatures( + &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 = ProvideForfeitSignaturesSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( diff --git a/arkd/src/rpcserver.rs b/arkd/src/rpcserver.rs index a2f3cc8..3ed99dd 100644 --- a/arkd/src/rpcserver.rs +++ b/arkd/src/rpcserver.rs @@ -124,15 +124,22 @@ impl rpc::ArkService for Arc { offboard_feerate_sat_vkb: offboard_feerate.to_sat_per_kwu() * 4, }) }, - RoundEvent::Proposal { - id, vtxos_spec, round_tx, vtxos_signers, vtxos_agg_nonces, forfeit_nonces, + RoundEvent::VtxoProposal { + id, vtxos_spec, round_tx, vtxos_signers, vtxos_agg_nonces, } => { - rpc::round_event::Event::Proposal(rpc::RoundProposal { + rpc::round_event::Event::VtxoProposal(rpc::VtxoProposal { round_id: id, vtxos_spec: vtxos_spec.encode(), round_tx: bitcoin::consensus::serialize(&round_tx), vtxos_signers: vtxos_signers.into_iter().map(|k| k.serialize().to_vec()).collect(), vtxos_agg_nonces: vtxos_agg_nonces.into_iter().map(|n| n.serialize().to_vec()).collect(), + }) + }, + RoundEvent::RoundProposal { id, vtxos, round_tx, forfeit_nonces } => { + rpc::round_event::Event::RoundProposal(rpc::RoundProposal { + round_id: id, + signed_vtxos: vtxos.encode(), + round_tx: bitcoin::consensus::serialize(&round_tx), forfeit_nonces: forfeit_nonces.into_iter().map(|(id, nonces)| { rpc::ForfeitNonces { input_vtxo_id: id.bytes().to_vec(), @@ -206,35 +213,42 @@ impl rpc::ArkService for Arc { Ok(tonic::Response::new(rpc::Empty {})) } - async fn provide_signatures( + async fn provide_vtxo_signatures( &self, - req: tonic::Request, + req: tonic::Request, ) -> Result, tonic::Status> { let req = req.into_inner(); - let forfeit = req.forfeit.into_iter().map(|forfeit| { - let id = VtxoId::from_slice(&forfeit.input_vtxo_id) - .map_err(|e| badarg!("invalid vtxo id: {}", e))?; - let nonces = forfeit.pub_nonces.into_iter().map(|n| { - musig::MusigPubNonce::from_slice(&n) - .map_err(|e| badarg!("invalid forfeit nonce: {}", e)) - }).collect::>()?; - let signatures = forfeit.signatures.into_iter().map(|s| { - musig::MusigPartialSignature::from_slice(&s) - .map_err(|e| badarg!("invalid forfeit sig: {}", e)) - }).collect::>()?; - Ok((id, (nonces, signatures))) - }).collect::>()?; - - let vtxo = req.vtxo.ok_or_else(|| badarg!("vtxo signatures missing"))?; - let inp = RoundInput::Signatures { - vtxo_pubkey: PublicKey::from_slice(&vtxo.pubkey) + let inp = RoundInput::VtxoSignatures { + pubkey: PublicKey::from_slice(&req.pubkey) .map_err(|e| badarg!("invalid pubkey: {}", e))?, - vtxo_signatures: vtxo.signatures.into_iter().map(|s| { + signatures: req.signatures.into_iter().map(|s| { musig::MusigPartialSignature::from_slice(&s) .map_err(|e| badarg!("invalid signature: {}", e)) }).collect::>()?, - forfeit: forfeit, + }; + self.round_input_tx.send(inp).expect("input channel closed"); + Ok(tonic::Response::new(rpc::Empty {})) + } + + async fn provide_forfeit_signatures( + &self, + req: tonic::Request, + ) -> Result, tonic::Status> { + let inp = RoundInput::ForfeitSignatures { + signatures: req.into_inner().signatures.into_iter().map(|forfeit| { + let id = VtxoId::from_slice(&forfeit.input_vtxo_id) + .map_err(|e| badarg!("invalid vtxo id: {}", e))?; + let nonces = forfeit.pub_nonces.into_iter().map(|n| { + musig::MusigPubNonce::from_slice(&n) + .map_err(|e| badarg!("invalid forfeit nonce: {}", e)) + }).collect::>()?; + let signatures = forfeit.signatures.into_iter().map(|s| { + musig::MusigPartialSignature::from_slice(&s) + .map_err(|e| badarg!("invalid forfeit sig: {}", e)) + }).collect::>()?; + Ok((id, (nonces, signatures))) + }).collect::>()? }; self.round_input_tx.send(inp).expect("input channel closed"); Ok(tonic::Response::new(rpc::Empty {})) diff --git a/noah/src/lib.rs b/noah/src/lib.rs index ecac329..830e5e7 100644 --- a/noah/src/lib.rs +++ b/noah/src/lib.rs @@ -473,13 +473,17 @@ impl Wallet { public_nonces: pub_nonces.iter().map(|n| n.serialize().to_vec()).collect(), }).await.context("submitting payment to asp")?; - // Wait for proposal from asp. - let (vtxo_tree, round_tx, vtxo_signers, vtxo_agg_nonces, forfeit_nonces) = loop { + + // **************************************************************** + // * Wait for vtxo proposal from asp. + // **************************************************************** + + let (vtxo_tree, round_tx, vtxo_signers, vtxo_agg_nonces) = loop { //TODO(stevenroose) should we really gracefully handle ASP malformed data? // panicking seems kinda ok since if we can't understand the ASP, // what are we even doing? match events.next().await.context("events stream broke")??.event.unwrap() { - rpc::round_event::Event::Proposal(p) => { + rpc::round_event::Event::VtxoProposal(p) => { assert_eq!(p.round_id, round_id, "missing messages"); let vtxos = VtxoTreeSpec::decode(&p.vtxos_spec) .context("decoding vtxo spec")?; @@ -492,22 +496,7 @@ impl Wallet { musig::MusigAggNonce::from_slice(&k).context("invalid agg nonce") }).collect::>>()?; - // Directly filter the forfeit nonces only for out inputs. - let forfeit_nonces = p.forfeit_nonces.into_iter().filter_map(|f| { - let id = VtxoId::from_slice(&f.input_vtxo_id) - .expect("invalid vtxoid from asp"); //TODO(stevenroose) maybe handle? - if vtxo_ids.contains(&id) { - let nonces = f.pub_nonces.into_iter().map(|s| { - musig::MusigPubNonce::from_slice(&s) - .expect("invalid forfeit nonce from asp") - }).collect::>(); - Some((id, nonces)) - } else { - None - } - }).collect::>(); - - break (vtxos, tx, cosigners, vtxo_nonces, forfeit_nonces); + break (vtxos, tx, cosigners, vtxo_nonces); }, // If a new round started meanwhile, pick up on that one. rpc::round_event::Event::Start(rpc::RoundStart { round_id: id, .. }) => { @@ -548,6 +537,81 @@ impl Wallet { bail!("asp didn't include our cosign key in the vtxo tree"); } + // Make vtxo signatures from top to bottom, just like sighashes are returned. + let sighashes = vtxo_tree.sighashes(vtxos_utxo); + assert_eq!(sighashes.len(), vtxo_agg_nonces.len()); + let signatures = iter::zip(sec_nonces.into_iter(), iter::zip(sighashes, vtxo_agg_nonces)) + .map(|(sec_nonce, (sighash, agg_nonce))| { + musig::partial_sign( + vtxo_signers.iter().copied(), + agg_nonce, + &cosign_key, + sec_nonce, + sighash.to_byte_array(), + Some(vtxo_tree.cosign_taptweak().to_byte_array()), + None, + ).0 + }).collect::>(); + self.asp.provide_vtxo_signatures(rpc::VtxoSignaturesRequest { + pubkey: cosign_key.public_key().serialize().to_vec(), + signatures: signatures.iter().map(|s| s.serialize().to_vec()).collect(), + }).await.context("providing signatures to asp")?; + + + // **************************************************************** + // * Then proceed to get a round proposal and sign forfeits + // **************************************************************** + + // Wait for vtxo proposal from asp. + let (vtxos, new_round_tx, forfeit_nonces) = loop { + //TODO(stevenroose) should we really gracefully handle ASP malformed data? + // panicking seems kinda ok since if we can't understand the ASP, + // what are we even doing? + match events.next().await.context("events stream broke")??.event.unwrap() { + rpc::round_event::Event::RoundProposal(p) => { + assert_eq!(p.round_id, round_id, "missing messages"); + let tx = bitcoin::consensus::deserialize::(&p.round_tx) + .context("decoding round tx")?; + let vtxos = SignedVtxoTree::decode(&p.signed_vtxos) + .context("decoding vtxo spec")?; + + // Directly filter the forfeit nonces only for out inputs. + let forfeit_nonces = p.forfeit_nonces.into_iter().filter_map(|f| { + let id = VtxoId::from_slice(&f.input_vtxo_id) + .expect("invalid vtxoid from asp"); //TODO(stevenroose) maybe handle? + if vtxo_ids.contains(&id) { + let nonces = f.pub_nonces.into_iter().map(|s| { + musig::MusigPubNonce::from_slice(&s) + .expect("invalid forfeit nonce from asp") + }).collect::>(); + Some((id, nonces)) + } else { + None + } + }).collect::>(); + + break (vtxos, tx, forfeit_nonces); + }, + // If a new round started meanwhile, pick up on that one. + rpc::round_event::Event::Start(rpc::RoundStart { round_id: id, .. }) => { + error!("Unexpected new round start..."); + round_id = id; + continue 'round; + }, + //TODO(stevenroose) make this robust + other => panic!("Unexpected message: {:?}", other), + } + }; + + if round_tx != new_round_tx { + bail!("ASP changed the round tx halfway the round."); + } + + // Validate the vtxo tree. + if let Err(e) = vtxos.validate_signatures() { + bail!("Received incorrect signed vtxo tree from asp: {}", e); + } + // Make forfeit signatures. let connectors = ConnectorChain::new( forfeit_nonces.values().next().unwrap().len(), @@ -573,39 +637,23 @@ impl Wallet { }).collect::>>()?; Ok((v.id(), sigs)) }).collect::>>()?; - - // Make vtxo signatures from top to bottom, just like sighashes are returned. - let sighashes = vtxo_tree.sighashes(vtxos_utxo); - assert_eq!(sighashes.len(), vtxo_agg_nonces.len()); - let signatures = iter::zip(sec_nonces.into_iter(), iter::zip(sighashes, vtxo_agg_nonces)) - .map(|(sec_nonce, (sighash, agg_nonce))| { - musig::partial_sign( - vtxo_signers.iter().copied(), - agg_nonce, - &cosign_key, - sec_nonce, - sighash.to_byte_array(), - Some(vtxo_tree.cosign_taptweak().to_byte_array()), - None, - ).0 - }).collect::>(); - self.asp.provide_signatures(rpc::RoundSignatures { - forfeit: forfeit_signatures.into_iter().map(|(id, sigs)| { + self.asp.provide_forfeit_signatures(rpc::ForfeitSignaturesRequest { + signatures: forfeit_signatures.into_iter().map(|(id, sigs)| { rpc::ForfeitSignatures { input_vtxo_id: id.bytes().to_vec(), pub_nonces: sigs.iter().map(|s| s.0.serialize().to_vec()).collect(), signatures: sigs.iter().map(|s| s.1.serialize().to_vec()).collect(), } }).collect(), - vtxo: Some(rpc::VtxoSignatures { - pubkey: cosign_key.public_key().serialize().to_vec(), - signatures: signatures.iter().map(|s| s.serialize().to_vec()).collect(), - }), }).await.context("providing signatures to asp")?; - // Wait for the finishing of the round. + + // **************************************************************** + // * Wait for the finishing of the round. + // **************************************************************** + trace!("Waiting for round finish..."); - let (vtxos, round_tx) = match events.next().await.context("events stream broke")??.event.unwrap() { + let (new_vtxos, round_tx) = match events.next().await.context("events stream broke")??.event.unwrap() { rpc::round_event::Event::Finished(f) => { if f.round_id != round_id { bail!("Unexpected round ID from round finished event: {} != {}", @@ -627,9 +675,8 @@ impl Wallet { other => panic!("Unexpected message: {:?}", other), }; - // Validate the vtxo tree. - if let Err(e) = vtxos.validate() { - bail!("Received incorrect signed vtxo tree from asp: {}", e); + if vtxos != new_vtxos { + bail!("ASP changed the vtxo tree halfway the round"); } // We also broadcast the tx, just to have it go around faster.