Separate vtxo tree signing and forfeit tx signing

This commit is contained in:
Steven Roose
2024-02-09 14:39:12 +00:00
parent f8e483c401
commit 4958af8839
7 changed files with 421 additions and 200 deletions

View File

@@ -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<PublicKey>,
pub vtxos: Vec<VtxoRequest>,
@@ -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() {

View File

@@ -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<u8>,
/// / The unsigned round tx.
#[prost(bytes = "vec", tag = "3")]
pub round_tx: ::prost::alloc::vec::Vec<u8>,
#[prost(bytes = "vec", repeated, tag = "4")]
pub vtxos_signers: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec<u8>>,
#[prost(bytes = "vec", repeated, tag = "5")]
pub vtxos_agg_nonces: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec<u8>>,
}
#[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<u8>,
/// / The unsigned round tx.
#[prost(bytes = "vec", tag = "3")]
pub round_tx: ::prost::alloc::vec::Vec<u8>,
#[prost(message, repeated, tag = "6")]
pub forfeit_nonces: ::prost::alloc::vec::Vec<ForfeitNonces>,
}
@@ -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<u8>,
/// / The signed round tx.
#[prost(bytes = "vec", tag = "3")]
pub round_tx: ::prost::alloc::vec::Vec<u8>,
}
#[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<round_event::Event>,
}
/// 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<ForfeitSignatures>,
}
#[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<u8>,
#[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<ForfeitSignatures>,
#[prost(message, optional, tag = "2")]
pub vtxo: ::core::option::Option<VtxoSignatures>,
}
#[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<super::RoundSignatures>,
request: impl tonic::IntoRequest<super::VtxoSignaturesRequest>,
) -> std::result::Result<tonic::Response<super::Empty>, 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<super::ForfeitSignaturesRequest>,
) -> std::result::Result<tonic::Response<super::Empty>, 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
}
}

View File

@@ -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;
}

View File

@@ -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<PublicKey>,
vtxos_agg_nonces: Vec<musig::MusigAggNonce>,
},
RoundProposal {
id: u64,
round_tx: Transaction,
vtxos: SignedVtxoTree,
forfeit_nonces: HashMap<VtxoId, Vec<musig::MusigPubNonce>>,
},
Finished {
id: u64,
vtxos: SignedVtxoTree,
round_tx: Transaction,
vtxos: SignedVtxoTree,
},
}
@@ -48,10 +53,12 @@ pub enum RoundInput {
cosign_pubkey: PublicKey,
public_nonces: Vec<musig::MusigPubNonce>,
},
Signatures {
vtxo_pubkey: PublicKey,
vtxo_signatures: Vec<musig::MusigPartialSignature>,
forfeit: HashMap<VtxoId, (Vec<musig::MusigPubNonce>, Vec<musig::MusigPartialSignature>)>,
VtxoSignatures {
pubkey: PublicKey,
signatures: Vec<musig::MusigPartialSignature>,
},
ForfeitSignatures {
signatures: HashMap<VtxoId, (Vec<musig::MusigPubNonce>, Vec<musig::MusigPartialSignature>)>,
},
}
@@ -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::<Vec<_>>();
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::<Vec<_>>();
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 {

View File

@@ -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<u8>,
/// / The unsigned round tx.
#[prost(bytes = "vec", tag = "3")]
pub round_tx: ::prost::alloc::vec::Vec<u8>,
#[prost(bytes = "vec", repeated, tag = "4")]
pub vtxos_signers: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec<u8>>,
#[prost(bytes = "vec", repeated, tag = "5")]
pub vtxos_agg_nonces: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec<u8>>,
}
#[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<u8>,
/// / The unsigned round tx.
#[prost(bytes = "vec", tag = "3")]
pub round_tx: ::prost::alloc::vec::Vec<u8>,
#[prost(message, repeated, tag = "6")]
pub forfeit_nonces: ::prost::alloc::vec::Vec<ForfeitNonces>,
}
@@ -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<u8>,
/// / The signed round tx.
#[prost(bytes = "vec", tag = "3")]
pub round_tx: ::prost::alloc::vec::Vec<u8>,
}
#[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<round_event::Event>,
}
/// 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<ForfeitSignatures>,
}
#[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<u8>,
#[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<ForfeitSignatures>,
#[prost(message, optional, tag = "2")]
pub vtxo: ::core::option::Option<VtxoSignatures>,
}
#[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<super::SubmitPaymentRequest>,
) -> std::result::Result<tonic::Response<super::Empty>, tonic::Status>;
async fn provide_signatures(
async fn provide_vtxo_signatures(
&self,
request: tonic::Request<super::RoundSignatures>,
request: tonic::Request<super::VtxoSignaturesRequest>,
) -> std::result::Result<tonic::Response<super::Empty>, tonic::Status>;
async fn provide_forfeit_signatures(
&self,
request: tonic::Request<super::ForfeitSignaturesRequest>,
) -> std::result::Result<tonic::Response<super::Empty>, 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<T: ArkService>(pub Arc<T>);
struct ProvideVtxoSignaturesSvc<T: ArkService>(pub Arc<T>);
impl<
T: ArkService,
> tonic::server::UnaryService<super::RoundSignatures>
for ProvideSignaturesSvc<T> {
> tonic::server::UnaryService<super::VtxoSignaturesRequest>
for ProvideVtxoSignaturesSvc<T> {
type Response = super::Empty;
type Future = BoxFuture<
tonic::Response<Self::Response>,
@@ -581,11 +601,12 @@ pub mod ark_service_server {
>;
fn call(
&mut self,
request: tonic::Request<super::RoundSignatures>,
request: tonic::Request<super::VtxoSignaturesRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as ArkService>::provide_signatures(&inner, request).await
<T as ArkService>::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<T: ArkService>(pub Arc<T>);
impl<
T: ArkService,
> tonic::server::UnaryService<super::ForfeitSignaturesRequest>
for ProvideForfeitSignaturesSvc<T> {
type Response = super::Empty;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::ForfeitSignaturesRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as ArkService>::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(

View File

@@ -124,15 +124,22 @@ impl rpc::ArkService for Arc<App> {
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<App> {
Ok(tonic::Response::new(rpc::Empty {}))
}
async fn provide_signatures(
async fn provide_vtxo_signatures(
&self,
req: tonic::Request<rpc::RoundSignatures>,
req: tonic::Request<rpc::VtxoSignaturesRequest>,
) -> Result<tonic::Response<rpc::Empty>, 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::<Result<_, tonic::Status>>()?;
let signatures = forfeit.signatures.into_iter().map(|s| {
musig::MusigPartialSignature::from_slice(&s)
.map_err(|e| badarg!("invalid forfeit sig: {}", e))
}).collect::<Result<_, tonic::Status>>()?;
Ok((id, (nonces, signatures)))
}).collect::<Result<_, tonic::Status>>()?;
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::<Result<_, tonic::Status>>()?,
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<rpc::ForfeitSignaturesRequest>,
) -> Result<tonic::Response<rpc::Empty>, 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::<Result<_, tonic::Status>>()?;
let signatures = forfeit.signatures.into_iter().map(|s| {
musig::MusigPartialSignature::from_slice(&s)
.map_err(|e| badarg!("invalid forfeit sig: {}", e))
}).collect::<Result<_, tonic::Status>>()?;
Ok((id, (nonces, signatures)))
}).collect::<Result<_, tonic::Status>>()?
};
self.round_input_tx.send(inp).expect("input channel closed");
Ok(tonic::Response::new(rpc::Empty {}))

View File

@@ -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::<anyhow::Result<Vec<_>>>()?;
// 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::<Vec<_>>();
Some((id, nonces))
} else {
None
}
}).collect::<HashMap<_, _>>();
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::<Vec<_>>();
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::<Transaction>(&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::<Vec<_>>();
Some((id, nonces))
} else {
None
}
}).collect::<HashMap<_, _>>();
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::<anyhow::Result<Vec<_>>>()?;
Ok((v.id(), sigs))
}).collect::<anyhow::Result<HashMap<_, _>>>()?;
// 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::<Vec<_>>();
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.