From 2d044ec2c2bb7cf638bf4e2326b4546f514cd6fa Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Thu, 7 Mar 2024 09:36:59 +0100 Subject: [PATCH] Analyze rawtx msg and add sp_tweak --- Cargo.lock | 92 ++++++++++++++- Cargo.toml | 4 + src/daemon.rs | 304 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 98 ++++++++++++---- src/sp.rs | 155 +++++++++++++++++++++++++ 5 files changed, 629 insertions(+), 24 deletions(-) create mode 100644 src/daemon.rs create mode 100644 src/sp.rs diff --git a/Cargo.lock b/Cargo.lock index 22432d7..91c17d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anyhow" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" + [[package]] name = "atty" version = "0.2.14" @@ -58,24 +64,43 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + [[package]] name = "bech32" version = "0.10.0-beta" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea" +[[package]] +name = "bimap" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" + [[package]] name = "bitcoin" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd00f3c09b5f21fb357abe32d29946eb8bb7a0862bae62c0b5e4a692acbbe73c" dependencies = [ - "bech32", + "bech32 0.10.0-beta", "bitcoin-internals", "bitcoin_hashes", "hex-conservative", "hex_lit", "secp256k1", + "serde", ] [[package]] @@ -83,6 +108,9 @@ name = "bitcoin-internals" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" +dependencies = [ + "serde", +] [[package]] name = "bitcoin_hashes" @@ -92,6 +120,31 @@ checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" dependencies = [ "bitcoin-internals", "hex-conservative", + "serde", +] + +[[package]] +name = "bitcoincore-rpc" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eb70725a621848c83b3809913d5314c0d20ca84877d99dd909504b564edab00" +dependencies = [ + "bitcoincore-rpc-json", + "jsonrpc", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "bitcoincore-rpc-json" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "856ffbee2e492c23bca715d72ea34aae80d58400f2bda26a82015d6bc2ec3662" +dependencies = [ + "bitcoin", + "serde", + "serde_json", ] [[package]] @@ -386,6 +439,12 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd5256b483761cd23699d0da46cc6fd2ee3be420bbe6d020ae4a091e70b7e9fd" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hex-conservative" version = "0.1.1" @@ -456,6 +515,17 @@ dependencies = [ "libc", ] +[[package]] +name = "jsonrpc" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8128f36b47411cd3f044be8c1f5cc0c9e24d1d1bfdc45f0a57897b32513053f2" +dependencies = [ + "base64", + "serde", + "serde_json", +] + [[package]] name = "jwalk" version = "0.8.1" @@ -675,12 +745,16 @@ dependencies = [ name = "sdk_relay" version = "0.1.0" dependencies = [ + "anyhow", + "bitcoin", + "bitcoincore-rpc", "bitcoincore-zmq", "env_logger", "futures-util", "log", "serde", "serde_json", + "silentpayments", "tokio", "tokio-stream", "tokio-tungstenite", @@ -693,7 +767,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" dependencies = [ "bitcoin_hashes", + "rand", "secp256k1-sys", + "serde", ] [[package]] @@ -756,6 +832,20 @@ dependencies = [ "digest", ] +[[package]] +name = "silentpayments" +version = "0.1.0" +source = "git+https://github.com/cygnet3/rust-silentpayments?branch=master#e915f5f8daef4b39ea32902963c143a5c0ed1746" +dependencies = [ + "bech32 0.9.1", + "bimap", + "bitcoin_hashes", + "hex", + "secp256k1", + "serde", + "serde_json", +] + [[package]] name = "slab" version = "0.4.9" diff --git a/Cargo.toml b/Cargo.toml index 3b564a7..bdf1d79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,12 +4,16 @@ version = "0.1.0" edition = "2021" [dependencies] +anyhow = "1.0" +bitcoin = { version = "0.31.1", features = ["serde"] } +bitcoincore-rpc = { version = "0.18" } bitcoincore-zmq = "1.4.0" env_logger = "0.9" futures-util = { version = "0.3.28", default-features = false, features = ["sink", "std"] } log = "0.4.20" serde = { version = "1.0.193", features = ["derive"]} serde_json = "1.0" +silentpayments = { git = "https://github.com/cygnet3/rust-silentpayments", branch = "master", features = ['utils'] } tokio = { version = "1.0.0", features = ["io-util", "rt-multi-thread", "macros", "sync"] } tokio-stream = "0.1" tokio-tungstenite = "0.21.0" diff --git a/src/daemon.rs b/src/daemon.rs new file mode 100644 index 0000000..fde0250 --- /dev/null +++ b/src/daemon.rs @@ -0,0 +1,304 @@ +use anyhow::{Context, Result}; + +use bitcoin::{consensus::deserialize, hashes::hex::FromHex}; +use bitcoin::{Amount, BlockHash, Transaction, Txid}; +use bitcoincore_rpc::{json, jsonrpc, Auth, Client, RpcApi}; +// use crossbeam_channel::Receiver; +// use parking_lot::Mutex; +use serde_json::{json, Value}; + +use std::fs::File; +use std::io::Read; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::time::Duration; + +pub struct SensitiveAuth(pub Auth); + +impl SensitiveAuth { + pub(crate) fn get_auth(&self) -> Auth { + self.0.clone() + } +} + +enum PollResult { + Done(Result<()>), + Retry, +} + +fn rpc_poll(client: &mut Client, skip_block_download_wait: bool) -> PollResult { + match client.get_blockchain_info() { + Ok(info) => { + if skip_block_download_wait { + // bitcoind RPC is available, don't wait for block download to finish + return PollResult::Done(Ok(())); + } + let left_blocks = info.headers - info.blocks; + if info.initial_block_download || left_blocks > 0 { + log::info!( + "waiting for {} blocks to download{}", + left_blocks, + if info.initial_block_download { + " (IBD)" + } else { + "" + } + ); + return PollResult::Retry; + } + PollResult::Done(Ok(())) + } + Err(err) => { + if let Some(e) = extract_bitcoind_error(&err) { + if e.code == -28 { + log::debug!("waiting for RPC warmup: {}", e.message); + return PollResult::Retry; + } + } + PollResult::Done(Err(err).context("daemon not available")) + } + } +} + +fn read_cookie(path: &Path) -> Result<(String, String)> { + // Load username and password from bitcoind cookie file: + // * https://github.com/bitcoin/bitcoin/pull/6388/commits/71cbeaad9a929ba6a7b62d9b37a09b214ae00c1a + // * https://bitcoin.stackexchange.com/questions/46782/rpc-cookie-authentication + let mut file = File::open(path) + .with_context(|| format!("failed to open bitcoind cookie file: {}", path.display()))?; + let mut contents = String::new(); + file.read_to_string(&mut contents) + .with_context(|| format!("failed to read bitcoind cookie from {}", path.display()))?; + + let parts: Vec<&str> = contents.splitn(2, ':').collect(); + anyhow::ensure!( + parts.len() == 2, + "failed to parse bitcoind cookie - missing ':' separator" + ); + Ok((parts[0].to_owned(), parts[1].to_owned())) +} + +fn rpc_connect() -> Result { + let rpc_url = "http://127.0.0.1:39332"; + // Allow `wait_for_new_block` to take a bit longer before timing out. + // See https://github.com/romanz/electrs/issues/495 for more details. + let builder = jsonrpc::simple_http::SimpleHttpTransport::builder() + .url(&rpc_url)? + .timeout(Duration::from_secs(30)); + let daemon_auth = SensitiveAuth(Auth::CookieFile(PathBuf::from_str("/home/sosthene/.bitcoin/signet/.cookie").unwrap())); + let builder = match daemon_auth.get_auth() { + Auth::None => builder, + Auth::UserPass(user, pass) => builder.auth(user, Some(pass)), + Auth::CookieFile(path) => { + let (user, pass) = read_cookie(&path)?; + builder.auth(user, Some(pass)) + } + }; + Ok(Client::from_jsonrpc(jsonrpc::Client::with_transport( + builder.build(), + ))) +} + +pub struct Daemon { + // p2p: Mutex, + rpc: Client, +} + +impl Daemon { + pub(crate) fn connect( + // config: &Config, + // exit_flag: &ExitFlag, + // metrics: &Metrics, + ) -> Result { + let mut rpc = rpc_connect()?; + + loop { + match rpc_poll(&mut rpc, true) { + PollResult::Done(result) => { + result.context("bitcoind RPC polling failed")?; + break; // on success, finish polling + } + PollResult::Retry => { + std::thread::sleep(std::time::Duration::from_secs(1)); // wait a bit before polling + } + } + } + + let network_info = rpc.get_network_info()?; + // if network_info.version < 21_00_00 { + // bail!("electrs requires bitcoind 0.21+"); + // } + if !network_info.network_active { + anyhow::bail!("electrs requires active bitcoind p2p network"); + } + let info = rpc.get_blockchain_info()?; + if info.pruned { + anyhow::bail!("electrs requires non-pruned bitcoind node"); + } + + // let p2p = tokio::sync::Mutex::new(Connection::connect( + // config.network, + // config.daemon_p2p_addr, + // metrics, + // config.signet_magic, + // )?); + Ok(Self { rpc }) + } + + pub(crate) fn estimate_fee(&self, nblocks: u16) -> Result> { + Ok(self + .rpc + .estimate_smart_fee(nblocks, None) + .context("failed to estimate fee")? + .fee_rate) + } + + pub(crate) fn get_relay_fee(&self) -> Result { + Ok(self + .rpc + .get_network_info() + .context("failed to get relay fee")? + .relay_fee) + } + + pub(crate) fn broadcast(&self, tx: &Transaction) -> Result { + self.rpc + .send_raw_transaction(tx) + .context("failed to broadcast transaction") + } + + pub(crate) fn get_transaction_info( + &self, + txid: &Txid, + blockhash: Option, + ) -> Result { + // No need to parse the resulting JSON, just return it as-is to the client. + self.rpc + .call( + "getrawtransaction", + &[json!(txid), json!(true), json!(blockhash)], + ) + .context("failed to get transaction info") + } + + pub(crate) fn get_transaction_hex( + &self, + txid: &Txid, + blockhash: Option, + ) -> Result { + use bitcoin::consensus::serde::{hex::Lower, Hex, With}; + + let tx = self.get_transaction(txid, blockhash)?; + #[derive(serde::Serialize)] + #[serde(transparent)] + struct TxAsHex(#[serde(with = "With::>")] Transaction); + serde_json::to_value(TxAsHex(tx)).map_err(Into::into) + } + + pub(crate) fn get_transaction( + &self, + txid: &Txid, + blockhash: Option, + ) -> Result { + self.rpc + .get_raw_transaction(txid, blockhash.as_ref()) + .context("failed to get transaction") + } + + pub(crate) fn get_block_txids(&self, blockhash: BlockHash) -> Result> { + Ok(self + .rpc + .get_block_info(&blockhash) + .context("failed to get block txids")? + .tx) + } + + pub(crate) fn get_mempool_txids(&self) -> Result> { + self.rpc + .get_raw_mempool() + .context("failed to get mempool txids") + } + + pub(crate) fn get_mempool_entries( + &self, + txids: &[Txid], + ) -> Result>> { + let client = self.rpc.get_jsonrpc_client(); + log::debug!("getting {} mempool entries", txids.len()); + let args: Vec<_> = txids + .iter() + .map(|txid| vec![serde_json::value::to_raw_value(txid).unwrap()]) + .collect(); + let reqs: Vec<_> = args + .iter() + .map(|a| client.build_request("getmempoolentry", a)) + .collect(); + let res = client.send_batch(&reqs).context("batch request failed")?; + log::debug!("got {} mempool entries", res.len()); + Ok(res + .into_iter() + .map(|r| { + r.context("missing response")? + .result::() + .context("invalid response") + }) + .collect()) + } + + pub(crate) fn get_mempool_transactions( + &self, + txids: &[Txid], + ) -> Result>> { + let client = self.rpc.get_jsonrpc_client(); + log::debug!("getting {} transactions", txids.len()); + let args: Vec<_> = txids + .iter() + .map(|txid| vec![serde_json::value::to_raw_value(txid).unwrap()]) + .collect(); + let reqs: Vec<_> = args + .iter() + .map(|a| client.build_request("getrawtransaction", a)) + .collect(); + let res = client.send_batch(&reqs).context("batch request failed")?; + log::debug!("got {} mempool transactions", res.len()); + Ok(res + .into_iter() + .map(|r| -> Result { + let tx_hex = r + .context("missing response")? + .result::() + .context("invalid response")?; + let tx_bytes = Vec::from_hex(&tx_hex).context("non-hex transaction")?; + deserialize(&tx_bytes).context("invalid transaction") + }) + .collect()) + } + + // pub(crate) fn get_new_headers(&self, chain: &Chain) -> Result> { + // self.p2p.lock().get_new_headers(chain) + // } + + // pub(crate) fn for_blocks(&self, blockhashes: B, func: F) -> Result<()> + // where + // B: IntoIterator, + // F: FnMut(BlockHash, SerBlock), + // { + // self.p2p.lock().for_blocks(blockhashes, func) + // } + + // pub(crate) fn new_block_notification(&self) -> Receiver<()> { + // self.p2p.lock().new_block_notification() + // } +} + +pub(crate) type RpcError = bitcoincore_rpc::jsonrpc::error::RpcError; + +pub(crate) fn extract_bitcoind_error(err: &bitcoincore_rpc::Error) -> Option<&RpcError> { + use bitcoincore_rpc::{ + jsonrpc::error::Error::Rpc as ServerError, Error::JsonRpc as JsonRpcError, + }; + match err { + JsonRpcError(ServerError(e)) => Some(e), + _ => None, + } +} diff --git a/src/main.rs b/src/main.rs index 0303cb1..7c34968 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use std::{ sync::{Arc, Mutex}, }; +use bitcoin::{consensus::deserialize, secp256k1::PublicKey}; use futures_util::{future, pin_mut, stream::TryStreamExt, FutureExt, StreamExt}; use log::{debug, error}; @@ -14,27 +15,16 @@ use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_tungstenite::tungstenite::Message; +mod sp; +mod daemon; + +use crate::daemon::Daemon; +use crate::sp::VinData; + type Tx = UnboundedSender; type PeerMap = Arc>>; -// #[derive(Deserialize, Serialize, Debug)] -// struct AnkMessage { -// dest: SilentPaymentId, -// payload: String -// } - -// impl TryFrom for AnkMessage { -// type Error = serde_json::Error; - -// fn try_from(value: Message) -> Result { -// match value { -// Message::Text(s) => serde_json::from_str(&s), -// _ => Err(serde_json::Error::custom("Unsupported message type")), -// } -// } -// } - async fn handle_connection(peer_map: PeerMap, raw_stream: TcpStream, addr: SocketAddr) { debug!("Incoming TCP connection from: {}", addr); @@ -82,7 +72,17 @@ async fn handle_connection(peer_map: PeerMap, raw_stream: TcpStream, addr: Socke peer_map.lock().unwrap().remove(&addr); } -async fn handle_zmq(peer_map: PeerMap) { +fn flatten_msg(parts: &[Vec]) -> Vec { + let total_len = parts.iter().fold(0, |acc, v| acc + v.len()); + let mut final_vec = Vec::with_capacity(total_len); + for p in parts { + final_vec.extend(p); + } + + final_vec +} + +async fn handle_zmq(peer_map: PeerMap, daemon: Daemon) { tokio::task::spawn_blocking(move || { debug!("Starting listening on Core"); for msg in bitcoincore_zmq::subscribe_receiver(&["tcp://127.0.0.1:29000"]).unwrap() { @@ -91,11 +91,60 @@ async fn handle_zmq(peer_map: PeerMap) { debug!("Received a {} message", core_msg.topic_str()); let peers = peer_map.lock().unwrap(); - let vectors = core_msg.serialize_to_vecs(); + let payload: Vec; + match core_msg.topic_str() { + "rawtx" => { + let tx: bitcoin::Transaction = deserialize(&core_msg.serialize_data_to_vec()).unwrap(); - let mut payload: Vec = Vec::with_capacity(vectors[0].len() + vectors[1].len() + vectors[2].len()); - for v in vectors { - payload.extend(v); + if tx.is_coinbase() { + continue; + } + + let mut outpoints: Vec<(String, u32)> = Vec::with_capacity(tx.input.len()); + let mut pubkeys: Vec = Vec::with_capacity(tx.input.len()); + for input in tx.input { + outpoints.push((input.previous_output.txid.to_string(), input.previous_output.vout)); + let prev_tx = daemon.get_transaction(&input.previous_output.txid, None).unwrap(); + if let Some(output) = prev_tx.output.get(input.previous_output.vout as usize) { + let vin_data = VinData { + script_sig: input.script_sig.to_bytes().to_vec(), + txinwitness: input.witness.to_vec(), + script_pub_key: output.script_pubkey.to_bytes() + }; + match sp::get_pubkey_from_input(&vin_data) { + Ok(res) => { + if let Some(pubkey) = res { + pubkeys.push(pubkey); + } else { + continue; + } + }, + Err(e) => { + log::error!("Can't extract pubkey from input: {}", e.to_string()); + continue; + } + } + } else { + log::error!("Transaction with a non existing input"); + continue; + } + } + let input_pub_keys: Vec<&PublicKey> = pubkeys.iter().collect(); + match silentpayments::utils::receiving::recipient_calculate_tweak_data(&input_pub_keys, &outpoints) { + Ok(partial_tweak) => { + let mut vecs = core_msg.serialize_to_vecs().to_vec(); + vecs.push(partial_tweak.serialize().to_vec()); + payload = flatten_msg(&vecs); + }, + Err(e) => { + log::error!("Failed to compute tweak data: {}", e.to_string()); + continue; + } + } + }, + _ => { + payload = flatten_msg(&core_msg.serialize_to_vecs()); + } } for tx in peers.values() { @@ -121,8 +170,11 @@ async fn main() -> Result<(), IoError> { let state = PeerMap::new(Mutex::new(HashMap::new())); + // Connect the rpc daemon + let daemon = Daemon::connect().unwrap(); + // Subscribe to Bitcoin Core - tokio::spawn(handle_zmq(state.clone())); + tokio::spawn(handle_zmq(state.clone(), daemon)); // Create the event loop and TCP listener we'll accept connections on. let try_socket = TcpListener::bind(&addr).await; diff --git a/src/sp.rs b/src/sp.rs new file mode 100644 index 0000000..01eb718 --- /dev/null +++ b/src/sp.rs @@ -0,0 +1,155 @@ +use bitcoin::secp256k1::{Parity::Even, PublicKey, XOnlyPublicKey}; +use bitcoin::hashes::{hash160, Hash}; + +use anyhow::Error; + +// ** Putting all the pubkey extraction logic in the test utils for now. ** +// NUMS_H (defined in BIP340) +const NUMS_H: [u8; 32] = [ + 0x50, 0x92, 0x9b, 0x74, 0xc1, 0xa0, 0x49, 0x54, 0xb7, 0x8b, 0x4b, 0x60, 0x35, 0xe9, 0x7a, 0x5e, + 0x07, 0x8a, 0x5a, 0x0f, 0x28, 0xec, 0x96, 0xd5, 0x47, 0xbf, 0xee, 0x9a, 0xce, 0x80, 0x3a, 0xc0, +]; + +// Define OP_CODES used in script template matching for readability +const OP_1: u8 = 0x51; +const OP_0: u8 = 0x00; +const OP_PUSHBYTES_20: u8 = 0x14; +const OP_PUSHBYTES_32: u8 = 0x20; +const OP_HASH160: u8 = 0xA9; +const OP_EQUAL: u8 = 0x87; +const OP_DUP: u8 = 0x76; +const OP_EQUALVERIFY: u8 = 0x88; +const OP_CHECKSIG: u8 = 0xAC; + +// Only compressed pubkeys are supported for silent payments +const COMPRESSED_PUBKEY_SIZE: usize = 33; + +pub struct VinData { + pub script_sig: Vec, + pub txinwitness: Vec>, + pub script_pub_key: Vec, +} + +// script templates for inputs allowed in BIP352 shared secret derivation +pub fn is_p2tr(spk: &[u8]) -> bool { + matches!(spk, [OP_1, OP_PUSHBYTES_32, ..] if spk.len() == 34) +} + +fn is_p2wpkh(spk: &[u8]) -> bool { + matches!(spk, [OP_0, OP_PUSHBYTES_20, ..] if spk.len() == 22) +} + +fn is_p2sh(spk: &[u8]) -> bool { + matches!(spk, [OP_HASH160, OP_PUSHBYTES_20, .., OP_EQUAL] if spk.len() == 23) +} + +fn is_p2pkh(spk: &[u8]) -> bool { + matches!(spk, [OP_DUP, OP_HASH160, OP_PUSHBYTES_20, .., OP_EQUALVERIFY, OP_CHECKSIG] if spk.len() == 25) +} + +pub fn get_pubkey_from_input(vin: &VinData) -> Result, Error> { + if is_p2pkh(&vin.script_pub_key) { + match (&vin.txinwitness.is_empty(), &vin.script_sig.is_empty()) { + (true, false) => { + let spk_hash = &vin.script_pub_key[3..23]; + for i in (COMPRESSED_PUBKEY_SIZE..=vin.script_sig.len()).rev() { + if let Some(pubkey_bytes) = &vin.script_sig.get(i - COMPRESSED_PUBKEY_SIZE..i) { + let pubkey_hash = hash160::Hash::hash(pubkey_bytes); + if pubkey_hash.to_byte_array() == spk_hash { + return Ok(Some(PublicKey::from_slice(pubkey_bytes)?)); + } + } else { + return Ok(None); + } + } + } + (_, true) => return Err(Error::msg("Empty script_sig for spending a p2pkh")), + (false, _) => return Err(Error::msg("non empty witness for spending a p2pkh")), + } + } else if is_p2sh(&vin.script_pub_key) { + match (&vin.txinwitness.is_empty(), &vin.script_sig.is_empty()) { + (false, false) => { + let redeem_script = &vin.script_sig[1..]; + if is_p2wpkh(redeem_script) { + if let Some(value) = vin.txinwitness.last() { + if let Ok(pubkey) = PublicKey::from_slice(value) { + return Ok(Some(pubkey)); + } else { + return Ok(None); + } + } + } + } + (_, true) => { + return Err(Error::msg( + "Empty script_sig for spending a p2sh".to_owned(), + )) + } + (true, false) => { + return Ok(None); + } + } + } else if is_p2wpkh(&vin.script_pub_key) { + match (&vin.txinwitness.is_empty(), &vin.script_sig.is_empty()) { + (false, true) => { + if let Some(value) = vin.txinwitness.last() { + if let Ok(pubkey) = PublicKey::from_slice(value) { + return Ok(Some(pubkey)); + } else { + return Ok(None); + } + } else { + return Err(Error::msg("Empty witness".to_owned())); + } + } + (_, false) => { + return Err(Error::msg( + "Non empty script sig for spending a segwit output".to_owned(), + )) + } + (true, _) => { + return Err(Error::msg( + "Empty witness for spending a segwit output".to_owned(), + )) + } + } + } else if is_p2tr(&vin.script_pub_key) { + match (&vin.txinwitness.is_empty(), &vin.script_sig.is_empty()) { + (false, true) => { + // check for the optional annex + let annex = match vin.txinwitness.last().and_then(|value| value.get(0)) { + Some(&0x50) => 1, + Some(_) => 0, + None => return Err(Error::msg("Empty or invalid witness".to_owned())), + }; + + // Check for script path + let stack_size = vin.txinwitness.len(); + if stack_size > annex && vin.txinwitness[stack_size - annex - 1][1..33] == NUMS_H { + return Ok(None); + } + + // Return the pubkey from the script pubkey + return XOnlyPublicKey::from_slice(&vin.script_pub_key[2..34]) + .map_err(|e| Error::new(e)) + .map(|x_only_public_key| { + Some(PublicKey::from_x_only_public_key(x_only_public_key, Even)) + }); + } + (_, false) => { + return Err(Error::msg( + "Non empty script sig for spending a segwit output".to_owned(), + )) + } + (true, _) => { + return Err(Error::msg( + "Empty witness for spending a segwit output".to_owned(), + )) + } + } + } else { + // We don't support this kind of output + return Err(Error::msg("Unsupported script pubkey type")); + } + Ok(None) +}