use std_shims::{ vec::Vec, collections::HashMap, io::{self, Write}, }; #[cfg(feature = "std")] use std::io::{Read, BufReader}; use k256::{ elliptic_curve::sec1::{Tag, ToEncodedPoint}, Scalar, ProjectivePoint, }; #[cfg(feature = "std")] use frost::{ curve::{Ciphersuite, Secp256k1}, ThresholdKeys, }; use bitcoin::{ consensus::encode::serialize, key::TweakedPublicKey, OutPoint, ScriptBuf, TxOut, Transaction, Block, }; #[cfg(feature = "std")] use bitcoin::{hashes::Hash, consensus::encode::Decodable, TapTweakHash}; use crate::crypto::x_only; #[cfg(feature = "std")] use crate::crypto::make_even; #[cfg(feature = "std")] mod send; #[cfg(feature = "std")] pub use send::*; /// Tweak keys to ensure they're usable with Bitcoin's Taproot upgrade. /// /// This adds an unspendable script path to the key, preventing any outputs received to this key /// from being spent via a script. To have keys which have spendable script paths, further offsets /// from this position must be used. /// /// After adding an unspendable script path, the key is incremented until its even. This means the /// existence of the unspendable script path may not provable, without an understanding of the /// algorithm used here. #[cfg(feature = "std")] pub fn tweak_keys(keys: &ThresholdKeys) -> ThresholdKeys { // Adds the unspendable script path per // https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_note-23 let keys = { use k256::elliptic_curve::{ bigint::{Encoding, U256}, ops::Reduce, group::GroupEncoding, }; let tweak_hash = TapTweakHash::hash(&keys.group_key().to_bytes().as_slice()[1 ..]); /* https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#cite_ref-13-0 states how the bias is negligible. This reduction shouldn't ever occur, yet if it did, the script path would be unusable due to a check the script path hash is less than the order. That doesn't impact us as we don't want the script path to be usable. */ keys.offset(::F::reduce(U256::from_be_bytes( *tweak_hash.to_raw_hash().as_ref(), ))) }; // This doesn't risk re-introducing a script path as you'd have to find a preimage for the tweak // hash with whatever increment, or manipulate the key so that the tweak hash and increment // equals the desired offset, yet manipulating the key would change the tweak hash let (_, offset) = make_even(keys.group_key()); keys.offset(Scalar::from(offset)) } /// Return the Taproot address payload for a public key. /// /// If the key is odd, this will return None. pub fn p2tr_script_buf(key: ProjectivePoint) -> Option { if key.to_encoded_point(true).tag() != Tag::CompressedEvenY { return None; } Some(ScriptBuf::new_p2tr_tweaked(TweakedPublicKey::dangerous_assume_tweaked(x_only(&key)))) } /// A spendable output. #[derive(Clone, PartialEq, Eq, Debug)] pub struct ReceivedOutput { // The scalar offset to obtain the key usable to spend this output. offset: Scalar, // The output to spend. output: TxOut, // The TX ID and vout of the output to spend. outpoint: OutPoint, } impl ReceivedOutput { /// The offset for this output. pub fn offset(&self) -> Scalar { self.offset } /// The Bitcoin output for this output. pub fn output(&self) -> &TxOut { &self.output } /// The outpoint for this output. pub fn outpoint(&self) -> &OutPoint { &self.outpoint } /// The value of this output. pub fn value(&self) -> u64 { self.output.value.to_sat() } /// Read a ReceivedOutput from a generic satisfying Read. #[cfg(feature = "std")] pub fn read(r: &mut R) -> io::Result { let offset = Secp256k1::read_F(r)?; let output; let outpoint; { let mut buf_r = BufReader::with_capacity(0, r); output = TxOut::consensus_decode(&mut buf_r).map_err(|_| io::Error::other("invalid TxOut"))?; outpoint = OutPoint::consensus_decode(&mut buf_r).map_err(|_| io::Error::other("invalid OutPoint"))?; } Ok(ReceivedOutput { offset, output, outpoint }) } /// Write a ReceivedOutput to a generic satisfying Write. pub fn write(&self, w: &mut W) -> io::Result<()> { w.write_all(&self.offset.to_bytes())?; w.write_all(&serialize(&self.output))?; w.write_all(&serialize(&self.outpoint)) } /// Serialize a ReceivedOutput to a `Vec`. pub fn serialize(&self) -> Vec { let mut res = Vec::new(); self.write(&mut res).unwrap(); res } } /// A transaction scanner capable of being used with HDKD schemes. #[derive(Clone, Debug)] pub struct Scanner { key: ProjectivePoint, scripts: HashMap, } impl Scanner { /// Construct a Scanner for a key. /// /// Returns None if this key can't be scanned for. pub fn new(key: ProjectivePoint) -> Option { let mut scripts = HashMap::new(); scripts.insert(p2tr_script_buf(key)?, Scalar::ZERO); Some(Scanner { key, scripts }) } /// Register an offset to scan for. /// /// Due to Bitcoin's requirement that points are even, not every offset may be used. /// If an offset isn't usable, it will be incremented until it is. If this offset is already /// present, None is returned. Else, Some(offset) will be, with the used offset. /// /// This means offsets are surjective, not bijective, and the order offsets are registered in /// may determine the validity of future offsets. /// /// The offsets registered must be securely generated. Arbitrary offsets may introduce a script /// path into the output, allowing the output to be spent by satisfaction of an arbitrary script /// (not by the signature of the key). pub fn register_offset(&mut self, mut offset: Scalar) -> Option { // This loop will terminate as soon as an even point is found, with any point having a ~50% // chance of being even // That means this should terminate within a very small amount of iterations loop { match p2tr_script_buf(self.key + (ProjectivePoint::GENERATOR * offset)) { Some(script) => { if self.scripts.contains_key(&script) { None?; } self.scripts.insert(script, offset); return Some(offset); } None => offset += Scalar::ONE, } } } /// Scan a transaction. pub fn scan_transaction(&self, tx: &Transaction) -> Vec { let mut res = Vec::new(); for (vout, output) in tx.output.iter().enumerate() { // If the vout index exceeds 2**32, stop scanning outputs let Ok(vout) = u32::try_from(vout) else { break }; if let Some(offset) = self.scripts.get(&output.script_pubkey) { res.push(ReceivedOutput { offset: *offset, output: output.clone(), outpoint: OutPoint::new(tx.compute_txid(), vout), }); } } res } /// Scan a block. /// /// This will also scan the coinbase transaction which is bound by maturity. If received outputs /// must be immediately spendable, a post-processing pass is needed to remove those outputs. /// Alternatively, scan_transaction can be called on `block.txdata[1 ..]`. pub fn scan_block(&self, block: &Block) -> Vec { let mut res = Vec::new(); for tx in &block.txdata { res.extend(self.scan_transaction(tx)); } res } }