diff --git a/core/src/builder/sighash.rs b/core/src/builder/sighash.rs index 2cdebdc0..10f6dc73 100644 --- a/core/src/builder/sighash.rs +++ b/core/src/builder/sighash.rs @@ -118,6 +118,8 @@ pub fn create_nofn_sighash_stream( panic!("Not enough operators"); } + let watchtower_pks = db.get_all_watchtowers_xonly_pks(None).await?; + for (operator_idx, (operator_xonly_pk, _operator_reimburse_address, collateral_funding_txid)) in operators.iter().enumerate() { @@ -188,8 +190,8 @@ pub fn create_nofn_sighash_stream( let mut watchtower_challenge_page_tx_handler = builder::transaction::create_watchtower_challenge_page_txhandler( &kickoff_txhandler, - nofn_xonly_pk, config.num_watchtowers as u32, + &watchtower_pks, watchtower_wots.clone(), network, ); @@ -201,7 +203,7 @@ pub fn create_nofn_sighash_stream( )?; for i in 0..config.num_watchtowers { - let mut watchtower_challenge_txhandler = + let watchtower_challenge_txhandler = builder::transaction::create_watchtower_challenge_txhandler( &watchtower_challenge_page_tx_handler, i, @@ -210,12 +212,6 @@ pub fn create_nofn_sighash_stream( *operator_xonly_pk, network, ); - yield convert_tx_to_script_spend( - &mut watchtower_challenge_txhandler, - 0, - 0, - None, - )?; let mut operator_challenge_nack_txhandler = builder::transaction::create_operator_challenge_nack_txhandler( diff --git a/core/src/builder/transaction.rs b/core/src/builder/transaction.rs index f9abac6c..615174a3 100644 --- a/core/src/builder/transaction.rs +++ b/core/src/builder/transaction.rs @@ -454,8 +454,8 @@ pub fn create_kickoff_txhandler( /// Creates a [`TxHandler`] for the watchtower challenge page transaction. pub fn create_watchtower_challenge_page_txhandler( kickoff_tx_handler: &TxHandler, - nofn_xonly_pk: XOnlyPublicKey, num_watchtowers: u32, + watchtower_xonly_pks: &[XOnlyPublicKey], watchtower_wots: Vec>, network: bitcoin::Network, ) -> TxHandler { @@ -475,7 +475,7 @@ pub fn create_watchtower_challenge_page_txhandler( .map(|i| { let mut x = verifier.checksig_verify(&wots_params, watchtower_wots[i as usize].as_ref()); - x = x.push_x_only_key(&nofn_xonly_pk); + x = x.push_x_only_key(&watchtower_xonly_pks[i as usize]); x = x.push_opcode(OP_CHECKSIG); // TODO: Add checksig in the beginning let x = x.compile(); let (watchtower_challenge_addr, watchtower_challenge_spend) = diff --git a/core/src/database/common.rs b/core/src/database/common.rs index 5f46b806..2d183b7b 100644 --- a/core/src/database/common.rs +++ b/core/src/database/common.rs @@ -25,7 +25,7 @@ use bitcoin::{Address, OutPoint, Txid}; use bitvm::bridge::transactions::signing_winternitz::WinternitzPublicKey; use bitvm::signatures::winternitz; use risc0_zkvm::Receipt; -use secp256k1::schnorr; +use secp256k1::{schnorr, XOnlyPublicKey}; use sqlx::{Postgres, QueryBuilder}; impl Database { @@ -1078,6 +1078,70 @@ impl Database { Ok(watchtower_winternitz_public_keys) } + + /// Sets xonly public key of a watchtoer. + #[tracing::instrument(skip(self, tx), err(level = tracing::Level::ERROR), ret(level = tracing::Level::TRACE))] + pub async fn save_watchtower_xonly_pk( + &self, + tx: Option<&mut sqlx::Transaction<'_, Postgres>>, + watchtower_id: u32, + xonly_pk: &XOnlyPublicKey, + ) -> Result<(), BridgeError> { + let query = sqlx::query( + "INSERT INTO watchtower_xonly_public_keys (watchtower_id, xonly_pk) VALUES ($1, $2);", + ) + .bind(watchtower_id as i64) + .bind(xonly_pk.serialize()); + + match tx { + Some(tx) => query.execute(&mut **tx).await, + None => query.execute(&self.connection).await, + }?; + + Ok(()) + } + + /// Gets xonly public keys of all watchtowers. + #[tracing::instrument(skip(self, tx), err(level = tracing::Level::ERROR), ret(level = tracing::Level::TRACE))] + pub async fn get_all_watchtowers_xonly_pks( + &self, + tx: Option<&mut sqlx::Transaction<'_, Postgres>>, + ) -> Result, BridgeError> { + let query = sqlx::query_as( + "SELECT xonly_pk FROM watchtower_xonly_public_keys ORDER BY watchtower_id;", + ); + + let rows: Vec<(Vec,)> = match tx { + Some(tx) => query.fetch_all(&mut **tx).await, + None => query.fetch_all(&self.connection).await, + }?; + + rows.into_iter() + .map(|xonly_pk| { + XOnlyPublicKey::from_slice(&xonly_pk.0).map_err(BridgeError::Secp256k1Error) + }) + .collect() + } + + /// Gets xonly public key of a single watchtower + #[tracing::instrument(skip(self, tx), err(level = tracing::Level::ERROR), ret(level = tracing::Level::TRACE))] + pub async fn get_watchtower_xonly_pk( + &self, + tx: Option<&mut sqlx::Transaction<'_, Postgres>>, + watchtower_id: u32, + ) -> Result { + let query = sqlx::query_as( + "SELECT xonly_pk FROM watchtower_xonly_public_keys WHERE watchtower_id = $1;", + ) + .bind(watchtower_id as i64); + + let xonly_key: (Vec,) = match tx { + Some(tx) => query.fetch_one(&mut **tx).await, + None => query.fetch_one(&self.connection).await, + }?; + + Ok(XOnlyPublicKey::from_slice(&xonly_key.0)?) + } } #[cfg(test)] @@ -1894,4 +1958,39 @@ mod tests { assert_eq!(wpk0, read_wpks[0]); assert_eq!(wpk1, read_wpks[1]); } + #[tokio::test] + async fn save_get_watchtower_xonly_pk() { + let config = create_test_config_with_thread_name!(None); + let database = Database::new(&config).await.unwrap(); + + use secp256k1::{rand, Keypair, Secp256k1, XOnlyPublicKey}; + + let secp = Secp256k1::new(); + let keypair1 = Keypair::new(&secp, &mut rand::thread_rng()); + let xonly1 = XOnlyPublicKey::from_keypair(&keypair1).0; + + let keypair2 = Keypair::new(&secp, &mut rand::thread_rng()); + let xonly2 = XOnlyPublicKey::from_keypair(&keypair2).0; + + let w_data = vec![xonly1, xonly2]; + + for (id, data) in w_data.iter().enumerate() { + database + .save_watchtower_xonly_pk(None, id as u32, data) + .await + .unwrap(); + } + + let read_pks = database.get_all_watchtowers_xonly_pks(None).await.unwrap(); + + assert_eq!(read_pks, w_data); + + for (id, key) in w_data.iter().enumerate() { + let read_pk = database + .get_watchtower_xonly_pk(None, id as u32) + .await + .unwrap(); + assert_eq!(read_pk, *key); + } + } } diff --git a/core/src/rpc/clementine.proto b/core/src/rpc/clementine.proto index 851fed49..46498fef 100644 --- a/core/src/rpc/clementine.proto +++ b/core/src/rpc/clementine.proto @@ -216,6 +216,8 @@ message WatchtowerParams { uint32 watchtower_id = 1; // Flattened list of Winternitz pubkeys for each operator's timetxs. repeated WinternitzPubkey winternitz_pubkeys = 2; + // xonly public key serialized to bytes + bytes xonly_pk = 3; } // Watchtowers are responsible for challenging the operator's kickoff txs. diff --git a/core/src/rpc/clementine.rs b/core/src/rpc/clementine.rs index 3a7626bc..6bfac76b 100644 --- a/core/src/rpc/clementine.rs +++ b/core/src/rpc/clementine.rs @@ -205,6 +205,9 @@ pub struct WatchtowerParams { /// Flattened list of Winternitz pubkeys for each operator's timetxs. #[prost(message, repeated, tag = "2")] pub winternitz_pubkeys: ::prost::alloc::vec::Vec, + /// xonly public key serialized to bytes + #[prost(bytes = "vec", tag = "3")] + pub xonly_pk: ::prost::alloc::vec::Vec, } #[derive(Clone, PartialEq, ::prost::Message)] pub struct RawSignedMoveTx { diff --git a/core/src/rpc/verifier.rs b/core/src/rpc/verifier.rs index 148c66b0..d6eb067b 100644 --- a/core/src/rpc/verifier.rs +++ b/core/src/rpc/verifier.rs @@ -19,7 +19,7 @@ use bitvm::{ bridge::transactions::signing_winternitz::WinternitzPublicKey, signatures::winternitz, }; use futures::StreamExt; -use secp256k1::{schnorr, Message}; +use secp256k1::{schnorr, Message, XOnlyPublicKey}; use std::{pin::pin, str::FromStr}; use tokio::sync::mpsc; use tokio_stream::wrappers::ReceiverStream; @@ -211,6 +211,13 @@ impl ClementineVerifier for Verifier { .await?; } + let xonly_pk = XOnlyPublicKey::from_slice(&watchtower_params.xonly_pk).map_err(|_| { + BridgeError::RPCParamMalformed("watchtower.xonly_pk", "Invalid xonly key".to_string()) + })?; + self.db + .save_watchtower_xonly_pk(None, watchtower_params.watchtower_id, &xonly_pk) + .await?; + Ok(Response::new(Empty {})) } diff --git a/core/src/rpc/watchtower.rs b/core/src/rpc/watchtower.rs index 13677a8f..10da7979 100644 --- a/core/src/rpc/watchtower.rs +++ b/core/src/rpc/watchtower.rs @@ -19,9 +19,12 @@ impl ClementineWatchtower for Watchtower { .map(WinternitzPubkey::from_bitvm) .collect::>(); + let xonly_pk = self.actor.xonly_public_key.serialize().to_vec(); + Ok(Response::new(WatchtowerParams { watchtower_id: self.config.index, winternitz_pubkeys, + xonly_pk, })) } } @@ -76,6 +79,10 @@ mod tests { .into_inner(); assert_eq!(params.watchtower_id, watchtower.config.index); + assert_eq!( + params.xonly_pk, + watchtower.actor.xonly_public_key.serialize().to_vec() + ); assert!(params.winternitz_pubkeys.len() == config.num_operators * config.num_time_txs); } } diff --git a/scripts/schema.sql b/scripts/schema.sql index 96f1aacc..c28defdc 100644 --- a/scripts/schema.sql +++ b/scripts/schema.sql @@ -131,6 +131,12 @@ create table if not exists header_chain_proofs ( proof bytea ); +create table if not exists watchtower_xonly_public_keys ( + watchtower_id int not null, + xonly_pk bytea not null, + primary key (watchtower_id) +); + -- Verifier table of watchtower Winternitz public keys for every operator and time_tx pair create table if not exists watchtower_winternitz_public_keys ( watchtower_id int not null,