diff --git a/Cargo.toml b/Cargo.toml index 38a66f9..4c32259 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ std = [ "light-bitcoin-primitives/std", "light-bitcoin-script/std", "light-bitcoin-serialization/std", + "light-bitcoin-mast/std", ] derive = ["light-bitcoin-serialization/derive"] @@ -27,6 +28,7 @@ light-bitcoin-merkle = { path = "merkle", default-features = false } light-bitcoin-primitives = { path = "primitives", default-features = false } light-bitcoin-script = { path = "script", default-features = false } light-bitcoin-serialization = { path = "serialization", default-features = false } +light-bitcoin-mast = { path = "mast", default-features = false } [workspace] members = [ @@ -38,4 +40,5 @@ members = [ "script", "serialization", "serialization-derive", + "mast", ] diff --git a/README.md b/README.md index 1e9c706..7518763 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,9 @@ The library is largely based on the following: * [parity-bitcoin](https://github.com/paritytech/parity-bitcoin/commit/e4cdea3b575574aac1ac20a4750ff263fa3e2e89) (GPL-v3) * [rust-bitcoin](https://github.com/rust-bitcoin/rust-bitcoin) (Creative Commons CC0 1.0 Universal license) +## Run tests +`cargo test --all --release` + ## License [GPL-v3](./LICENSE) diff --git a/chain/Cargo.toml b/chain/Cargo.toml index 366aa72..d705ecf 100644 --- a/chain/Cargo.toml +++ b/chain/Cargo.toml @@ -11,6 +11,7 @@ std = [ "codec/std", "hex/std", "serde", + "scale-info/std", "light-bitcoin-crypto/std", "light-bitcoin-primitives/std", @@ -18,9 +19,10 @@ std = [ ] [dependencies] -codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = ["derive"] } +codec = { package = "parity-scale-codec", version = "1.3", default-features = false, features = ["derive"] } hex = { version = "0.4", default-features = false } serde = { version = "1.0", features = ["derive"], optional = true } +scale-info = { version = "1.0", default-features = false, features = ["derive"] } light-bitcoin-crypto = { path = "../crypto", default-features = false } light-bitcoin-primitives = { path = "../primitives", default-features = false } diff --git a/chain/src/block.rs b/chain/src/block.rs index 1202445..c30958a 100644 --- a/chain/src/block.rs +++ b/chain/src/block.rs @@ -10,7 +10,7 @@ use crate::merkle_root::merkle_root; use crate::transaction::Transaction; /// A Bitcoin block, which is a collection of transactions with an attached proof of work. -#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Debug, Default)] +#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Debug, Default, scale_info::TypeInfo)] #[derive(Serializable, Deserializable)] pub struct Block { /// The block header diff --git a/chain/src/block_header.rs b/chain/src/block_header.rs index 1246c2e..bc825b3 100644 --- a/chain/src/block_header.rs +++ b/chain/src/block_header.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; /// A block header, which contains all the block's information except /// the actual transactions -#[derive(Ord, PartialOrd, Eq, PartialEq, Copy, Clone, Default)] +#[derive(Ord, PartialOrd, Eq, PartialEq, Copy, Clone, Default, scale_info::TypeInfo)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Serializable, Deserializable)] pub struct BlockHeader { @@ -69,7 +69,7 @@ impl BlockHeader { impl codec::Encode for BlockHeader { fn encode(&self) -> Vec { - let value = serialize::(&self); + let value = serialize::(self); value.encode() } } diff --git a/chain/src/indexed_block.rs b/chain/src/indexed_block.rs index 2af3f1a..94b38c9 100644 --- a/chain/src/indexed_block.rs +++ b/chain/src/indexed_block.rs @@ -14,7 +14,7 @@ use crate::indexed_transaction::IndexedTransaction; use crate::merkle_root::merkle_root; use crate::transaction::Transaction; -#[derive(Ord, PartialOrd, Eq, Clone, Debug, Default, Deserializable)] +#[derive(Ord, PartialOrd, Eq, Clone, Debug, Default, Deserializable, scale_info::TypeInfo)] pub struct IndexedBlock { pub header: IndexedBlockHeader, pub transactions: Vec, diff --git a/chain/src/indexed_header.rs b/chain/src/indexed_header.rs index 95280a0..a82e764 100644 --- a/chain/src/indexed_header.rs +++ b/chain/src/indexed_header.rs @@ -6,7 +6,7 @@ use light_bitcoin_serialization::{Deserializable, Reader}; use crate::block_header::BlockHeader; use crate::read_and_hash::ReadAndHash; -#[derive(Ord, PartialOrd, Eq, Copy, Clone, Default)] +#[derive(Ord, PartialOrd, Eq, Copy, Clone, Default, scale_info::TypeInfo)] pub struct IndexedBlockHeader { pub hash: H256, pub raw: BlockHeader, diff --git a/chain/src/indexed_transaction.rs b/chain/src/indexed_transaction.rs index 78a078c..3789516 100644 --- a/chain/src/indexed_transaction.rs +++ b/chain/src/indexed_transaction.rs @@ -6,7 +6,7 @@ use light_bitcoin_serialization::{Deserializable, Reader}; use crate::read_and_hash::ReadAndHash; use crate::transaction::Transaction; -#[derive(Ord, PartialOrd, Eq, Clone, Default)] +#[derive(Ord, PartialOrd, Eq, Clone, Default, scale_info::TypeInfo)] pub struct IndexedTransaction { pub hash: H256, pub raw: Transaction, diff --git a/chain/src/lib.rs b/chain/src/lib.rs index be8535e..6edf8ac 100644 --- a/chain/src/lib.rs +++ b/chain/src/lib.rs @@ -22,7 +22,9 @@ pub use light_bitcoin_primitives::*; pub use self::block::Block; pub use self::block_header::BlockHeader; pub use self::merkle_root::{merkle_node_hash, merkle_root}; -pub use self::transaction::{OutPoint, Transaction, TransactionInput, TransactionOutput}; +pub use self::transaction::{ + OutPoint, Transaction, TransactionInput, TransactionOutput, TransactionOutputArray, +}; pub use self::indexed_block::IndexedBlock; pub use self::indexed_header::IndexedBlockHeader; diff --git a/chain/src/transaction.rs b/chain/src/transaction.rs index 462699c..84668c1 100644 --- a/chain/src/transaction.rs +++ b/chain/src/transaction.rs @@ -23,7 +23,7 @@ const WITNESS_MARKER: u8 = 0; const WITNESS_FLAG: u8 = 1; /// A reference to a transaction output -#[derive(Ord, PartialOrd, PartialEq, Eq, Copy, Clone)] +#[derive(Ord, PartialOrd, PartialEq, Eq, Copy, Clone, scale_info::TypeInfo)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Serializable, Deserializable)] pub struct OutPoint { @@ -74,7 +74,7 @@ impl OutPoint { } /// A transaction input, which defines old coins to be consumed -#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Debug, Default)] +#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Debug, Default, scale_info::TypeInfo)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] pub struct TransactionInput { /// The reference to the previous output that is being used an an input @@ -139,7 +139,7 @@ impl Deserializable for TransactionInput { } /// A transaction output, which defines new coins to be created from old ones. -#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Debug)] +#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Debug, scale_info::TypeInfo)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Serializable, Deserializable)] pub struct TransactionOutput { @@ -149,6 +149,13 @@ pub struct TransactionOutput { pub script_pubkey: Bytes, } +#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Debug)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[derive(Serializable, Deserializable)] +pub struct TransactionOutputArray { + pub outputs: Vec, +} + impl Default for TransactionOutput { fn default() -> Self { TransactionOutput { @@ -159,7 +166,7 @@ impl Default for TransactionOutput { } /// A Bitcoin transaction, which describes an authenticated movement of coins. -#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Debug, Default)] +#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Debug, Default, scale_info::TypeInfo)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] pub struct Transaction { /// The protocol version, is currently expected to be 1 or 2 (BIP 68). @@ -329,7 +336,7 @@ impl Deserializable for Transaction { impl codec::Encode for Transaction { fn encode(&self) -> Vec { - let value = serialize::(&self); + let value = serialize::(self); value.encode() } } @@ -392,36 +399,36 @@ mod tests { fn test_transaction_reader_with_witness() { let actual: Transaction = "01000000000102fff7f7881a8099afa6940d42d1e7f6362bec38171ea3edf433541db4e4ad969f00000000494830450221008b9d1dc26ba6a9cb62127b02742fa9d754cd3bebf337f7a55d114c8e5cdd30be022040529b194ba3f9281a99f2b1c0a19c0489bc22ede944ccf4ecbab4cc618ef3ed01eeffffffef51e1b804cc89d182d279655c3aa89e815b1b309fe287d9b2b55d57b90ec68a0100000000ffffffff02202cb206000000001976a9148280b37df378db99f66f85c95a783a76ac7a6d5988ac9093510d000000001976a9143bde42dbee7e4dbe6a21b2d50ce2f0167faa815988ac000247304402203609e17b84f6a7d30c80bfa610b5b4542f32a8a0d5447a12fb1366d7f01cc44a0220573a954c4518331561406f90300e8f3358f51928d43c212a8caed02de67eebee0121025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee635711000000".parse().unwrap(); let expected = Transaction { - version: 1, - inputs: vec![TransactionInput { - previous_output: OutPoint { - txid: h256("fff7f7881a8099afa6940d42d1e7f6362bec38171ea3edf433541db4e4ad969f"), - index: 0, - }, - script_sig: "4830450221008b9d1dc26ba6a9cb62127b02742fa9d754cd3bebf337f7a55d114c8e5cdd30be022040529b194ba3f9281a99f2b1c0a19c0489bc22ede944ccf4ecbab4cc618ef3ed01".parse().unwrap(), - sequence: 0xffffffee, - script_witness: vec![], - }, TransactionInput { - previous_output: OutPoint { + version: 1, + inputs: vec![TransactionInput { + previous_output: OutPoint { + txid: h256("fff7f7881a8099afa6940d42d1e7f6362bec38171ea3edf433541db4e4ad969f"), + index: 0, + }, + script_sig: "4830450221008b9d1dc26ba6a9cb62127b02742fa9d754cd3bebf337f7a55d114c8e5cdd30be022040529b194ba3f9281a99f2b1c0a19c0489bc22ede944ccf4ecbab4cc618ef3ed01".parse().unwrap(), + sequence: 0xffffffee, + script_witness: vec![], + }, TransactionInput { + previous_output: OutPoint { txid: h256("ef51e1b804cc89d182d279655c3aa89e815b1b309fe287d9b2b55d57b90ec68a"), - index: 1, - }, - script_sig: "".parse().unwrap(), - sequence: 0xffffffff, - script_witness: vec![ - "304402203609e17b84f6a7d30c80bfa610b5b4542f32a8a0d5447a12fb1366d7f01cc44a0220573a954c4518331561406f90300e8f3358f51928d43c212a8caed02de67eebee01".parse().unwrap(), - "025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee6357".parse().unwrap(), - ], - }], - outputs: vec![TransactionOutput { - value: 0x0000000006b22c20, - script_pubkey: "76a9148280b37df378db99f66f85c95a783a76ac7a6d5988ac".parse().unwrap(), - }, TransactionOutput { - value: 0x000000000d519390, - script_pubkey: "76a9143bde42dbee7e4dbe6a21b2d50ce2f0167faa815988ac".parse().unwrap(), - }], - lock_time: 0x00000011, - }; + index: 1, + }, + script_sig: "".parse().unwrap(), + sequence: 0xffffffff, + script_witness: vec![ + "304402203609e17b84f6a7d30c80bfa610b5b4542f32a8a0d5447a12fb1366d7f01cc44a0220573a954c4518331561406f90300e8f3358f51928d43c212a8caed02de67eebee01".parse().unwrap(), + "025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee6357".parse().unwrap(), + ], + }], + outputs: vec![TransactionOutput { + value: 0x0000000006b22c20, + script_pubkey: "76a9148280b37df378db99f66f85c95a783a76ac7a6d5988ac".parse().unwrap(), + }, TransactionOutput { + value: 0x000000000d519390, + script_pubkey: "76a9143bde42dbee7e4dbe6a21b2d50ce2f0167faa815988ac".parse().unwrap(), + }], + lock_time: 0x00000011, + }; assert_eq!(actual, expected); } @@ -438,6 +445,11 @@ mod tests { serialize_with_flags(&transaction_with_witness, 0), serialize_with_flags(&transaction_with_witness, SERIALIZE_TRANSACTION_WITNESS) ); + let tx : Transaction = "020000000001015dce8efe6cbd845587aa230a0b3667d4b52a45d3965d1607ab187de1f9d9d82b00000000000000000002a086010000000000225120dc82a9c33d787242d80fb4535bcc8d90bb13843fea52c9e78bb43c541dd607b900350c0000000000225120c9929543dfa1e0bb84891acd47bfa6546b05e26b7a04af8eb6765fcc969d565f0140708f206174a9e2963dd87d3afbb9f390fb320e2e9d4fdfc7b8bd7bc71a29c252026aa505ae71d4155ee3c13ce189ccba1fc0a26cfbcaa5f8b91bab377c2124eb00000000".parse().unwrap(); + let transaction_output = TransactionOutputArray { + outputs: vec![tx.outputs[0].clone()], + }; + assert_eq!( hex::encode(&serialize(&transaction_output)), "01a086010000000000225120dc82a9c33d787242d80fb4535bcc8d90bb13843fea52c9e78bb43c541dd607b9") } #[test] diff --git a/keys/Cargo.toml b/keys/Cargo.toml index 12e8f95..9c5b019 100644 --- a/keys/Cargo.toml +++ b/keys/Cargo.toml @@ -10,9 +10,13 @@ default = ["std"] std = [ "bs58/std", "codec/std", + "digest/std", "hex/std", "libsecp256k1/std", + "musig2/std", "serde", + "scale-info/std", + "sha2/std", "light-bitcoin-crypto/std", "light-bitcoin-primitives/std", @@ -20,12 +24,18 @@ std = [ ] [dependencies] +arrayref = { version = "0.3.6" } bs58 = { version = "0.4", default-features = false, features = ["alloc"] } codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = ["derive"] } -hex = { version = "0.4", default-features = false } +digest = { version = "0.9.0", default-features = false } +hex = { version = "0.4", default-features = false, features = ["alloc"] } libsecp256k1 = { version = "0.3.5", default-features = false, features = ["hmac"] } +musig2 = { git = "https://github.com/chainx-org/Musig2", branch = "lib", default-features = false } serde = { version = "1.0", features = ["derive"], optional = true } - +scale-info = { version = "1.0", default-features = false, features = ["derive"] } +sha2 = { version = "0.9.5", default-features = false } +# for no-std +bitcoin-bech32 = { git = "https://github.com/chainx-org/rust-bech32-bitcoin", branch = "master", default-features = false } light-bitcoin-crypto = { path = "../crypto", default-features = false } light-bitcoin-primitives = { path = "../primitives", default-features = false } light-bitcoin-serialization = { path = "../serialization", default-features = false, features = ["derive"] } diff --git a/keys/src/address.rs b/keys/src/address.rs index 0a11722..c3da9ff 100644 --- a/keys/src/address.rs +++ b/keys/src/address.rs @@ -5,10 +5,14 @@ //! //! https://en.bitcoin.it/wiki/Address -use core::{fmt, ops, str}; +extern crate alloc; +use alloc::string::{String, ToString}; +use core::{convert::TryFrom, fmt, ops, str}; +use bitcoin_bech32::constants::Network as Bech32Network; +use bitcoin_bech32::{u5, WitnessProgram}; use light_bitcoin_crypto::checksum; -use light_bitcoin_primitives::io; +use light_bitcoin_primitives::{io, H160, H256}; use light_bitcoin_serialization::{Deserializable, Reader, Serializable, Stream}; use codec::{Decode, Encode}; @@ -17,11 +21,11 @@ use serde::{Deserialize, Serialize}; use crate::display::DisplayLayout; use crate::error::Error; -use crate::AddressHash; +use crate::{AddressHash, XOnly}; /// There are two address formats currently in use. /// https://bitcoin.org/en/developer-reference#address-conversion -#[derive(Ord, PartialOrd, Eq, PartialEq, Copy, Clone, Debug)] +#[derive(Ord, PartialOrd, Eq, PartialEq, Copy, Clone, Debug, scale_info::TypeInfo)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode)] pub enum Type { @@ -33,6 +37,12 @@ pub enum Type { /// Newer P2SH type starting with the number 3, eg: 3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy. /// https://bitcoin.org/en/glossary/p2sh-address P2SH, + /// Pay to Witness PubKey Hash + P2WPKH, + /// Pay to Witness Script Hash + P2WSH, + /// Pay to Witness Taproot + P2TR, } impl Default for Type { @@ -46,6 +56,9 @@ impl Type { match v { 0 => Some(Type::P2PKH), 1 => Some(Type::P2SH), + 2 => Some(Type::P2WPKH), + 3 => Some(Type::P2WSH), + 4 => Some(Type::P2TR), _ => None, } } @@ -56,6 +69,9 @@ impl Serializable for Type { let _stream = match *self { Type::P2PKH => s.append(&Type::P2PKH), Type::P2SH => s.append(&Type::P2SH), + Type::P2WPKH => s.append(&Type::P2WPKH), + Type::P2WSH => s.append(&Type::P2WSH), + Type::P2TR => s.append(&Type::P2TR), }; } } @@ -71,7 +87,7 @@ impl Deserializable for Type { } } -#[derive(Ord, PartialOrd, Eq, PartialEq, Copy, Clone, Debug)] +#[derive(Ord, PartialOrd, Eq, PartialEq, Copy, Clone, Debug, scale_info::TypeInfo)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode)] pub enum Network { @@ -79,6 +95,15 @@ pub enum Network { Testnet, } +impl ToString for Network { + fn to_string(&self) -> String { + match self { + Network::Mainnet => "Mainnet".to_string(), + Network::Testnet => "Testnet".to_string(), + } + } +} + impl Default for Network { fn default() -> Network { Network::Mainnet @@ -115,8 +140,66 @@ impl Deserializable for Network { } } +#[derive(Ord, PartialOrd, Eq, PartialEq, Copy, Clone, Debug, scale_info::TypeInfo)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[derive(Encode, Decode)] +pub enum AddressTypes { + Legacy(AddressHash), + WitnessV0ScriptHash(H256), + WitnessV0KeyHash(H160), + WitnessV1Taproot(XOnly), +} + +impl Default for AddressTypes { + fn default() -> Self { + AddressTypes::Legacy(AddressHash::default()) + } +} + +impl Serializable for AddressTypes { + fn serialize(&self, s: &mut Stream) { + let _stream = match *self { + AddressTypes::Legacy(h) => s.append(&0).append(&h), + AddressTypes::WitnessV0ScriptHash(h) => s.append(&1).append(&h), + AddressTypes::WitnessV0KeyHash(h) => s.append(&2).append(&h), + AddressTypes::WitnessV1Taproot(h) => s.append(&3).append_slice(&h.0), + }; + } +} + +impl Deserializable for AddressTypes { + fn deserialize(reader: &mut Reader) -> Result + where + Self: Sized, + T: io::Read, + { + let t: u32 = reader.read()?; + match t { + 0 => { + let h: H160 = reader.read()?; + Ok(AddressTypes::Legacy(h)) + } + 1 => { + let h: H256 = reader.read()?; + Ok(AddressTypes::WitnessV0ScriptHash(h)) + } + 2 => { + let h: H160 = reader.read()?; + Ok(AddressTypes::WitnessV0KeyHash(h)) + } + 3 => { + let mut keys = [0u8; 32]; + reader.read_slice(&mut keys)?; + + Ok(AddressTypes::WitnessV1Taproot(XOnly(keys))) + } + _ => Err(io::Error::ReadMalformedData), + } + } +} + /// `AddressHash` with network identifier and format type -#[derive(Ord, PartialOrd, Eq, PartialEq, Copy, Clone, Debug, Default)] +#[derive(Ord, PartialOrd, Eq, PartialEq, Copy, Clone, Debug, Default, scale_info::TypeInfo)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Serializable, Deserializable)] #[derive(Encode, Decode)] @@ -126,12 +209,45 @@ pub struct Address { /// The network of the address. pub network: Network, /// Public key hash. - pub hash: AddressHash, + pub hash: AddressTypes, } impl fmt::Display for Address { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - bs58::encode(self.layout().0).into_string().fmt(f) + let network = match self.network { + Network::Mainnet => Bech32Network::Bitcoin, + Network::Testnet => Bech32Network::Testnet, + }; + match self.hash { + AddressTypes::Legacy(_) => bs58::encode(self.layout().0).into_string().fmt(f), + AddressTypes::WitnessV0ScriptHash(h) => { + let witness = WitnessProgram::new( + u5::try_from_u8(0).map_err(|_| fmt::Error)?, + h.0.to_vec(), + network, + ) + .map_err(|_| fmt::Error)?; + witness.to_string().fmt(f) + } + AddressTypes::WitnessV0KeyHash(h) => { + let witness = WitnessProgram::new( + u5::try_from_u8(0).map_err(|_| fmt::Error)?, + h.0.to_vec(), + network, + ) + .map_err(|_| fmt::Error)?; + witness.to_string().fmt(f) + } + AddressTypes::WitnessV1Taproot(h) => { + let witness = WitnessProgram::new( + u5::try_from_u8(1).map_err(|_| fmt::Error)?, + h.0.to_vec(), + network, + ) + .map_err(|_| fmt::Error)?; + witness.to_string().fmt(f) + } + } } } @@ -139,14 +255,44 @@ impl str::FromStr for Address { type Err = Error; fn from_str(s: &str) -> Result { - let hex = bs58::decode(s) - .into_vec() - .map_err(|_| Error::InvalidAddress)?; - Address::from_layout(&hex) + if bs58::decode(s).into_vec().is_ok() { + let hex = bs58::decode(s) + .into_vec() + .map_err(|_| Error::InvalidAddress)?; + Address::from_layout(&hex) + } else { + let witness = WitnessProgram::from_str(s).map_err(|_| Error::InvalidAddress)?; + let version = witness.version().to_u8(); + let network = match witness.network() { + Bech32Network::Bitcoin => Network::Mainnet, + _ => Network::Testnet, + }; + let (kind, hash) = if version == 1 { + ( + Type::P2TR, + AddressTypes::WitnessV1Taproot(XOnly::try_from(witness.program())?), + ) + } else if witness.program().len() == 20 { + ( + Type::P2WPKH, + AddressTypes::WitnessV0KeyHash(H160::from_slice(witness.program())), + ) + } else { + ( + Type::P2WSH, + AddressTypes::WitnessV0ScriptHash(H256::from_slice(witness.program())), + ) + }; + Ok(Self { + kind, + network, + hash, + }) + } } } -#[derive(Ord, PartialOrd, Eq, PartialEq, Copy, Clone, Debug, Default)] +#[derive(Ord, PartialOrd, Eq, PartialEq, Copy, Clone, Debug, Default, scale_info::TypeInfo)] pub struct AddressDisplayLayout([u8; 25]); impl ops::Deref for AddressDisplayLayout { @@ -168,9 +314,13 @@ impl DisplayLayout for Address { (Network::Mainnet, Type::P2SH) => 5, (Network::Testnet, Type::P2PKH) => 111, (Network::Testnet, Type::P2SH) => 196, + _ => todo!(), }; - result[1..21].copy_from_slice(self.hash.as_bytes()); + match self.hash { + AddressTypes::Legacy(h) => result[1..21].copy_from_slice(h.as_bytes()), + _ => todo!(), + }; let cs = checksum(&result[0..21]); result[21..25].copy_from_slice(cs.as_bytes()); AddressDisplayLayout(result) @@ -201,23 +351,22 @@ impl DisplayLayout for Address { Ok(Address { kind, network, - hash, + hash: AddressTypes::Legacy(hash), }) } } #[cfg(test)] mod tests { - use light_bitcoin_primitives::h160; - use super::*; + use light_bitcoin_primitives::h160; #[test] fn test_address_to_string() { let address = Address { kind: Type::P2PKH, network: Network::Mainnet, - hash: h160("3f4aa1fedf1f54eeb03b759deadb36676b184911"), + hash: AddressTypes::Legacy(h160("3f4aa1fedf1f54eeb03b759deadb36676b184911")), }; assert_eq!( address.to_string(), @@ -227,7 +376,7 @@ mod tests { let address = Address { kind: Type::P2SH, network: Network::Mainnet, - hash: h160("d246f700f4969106291a75ba85ad863cae68d667"), + hash: AddressTypes::Legacy(h160("d246f700f4969106291a75ba85ad863cae68d667")), }; assert_eq!( address.to_string(), @@ -240,7 +389,7 @@ mod tests { let address = Address { kind: Type::P2PKH, network: Network::Mainnet, - hash: h160("3f4aa1fedf1f54eeb03b759deadb36676b184911"), + hash: AddressTypes::Legacy(h160("3f4aa1fedf1f54eeb03b759deadb36676b184911")), }; assert_eq!( address, @@ -250,7 +399,7 @@ mod tests { let address = Address { kind: Type::P2SH, network: Network::Mainnet, - hash: h160("d246f700f4969106291a75ba85ad863cae68d667"), + hash: AddressTypes::Legacy(h160("d246f700f4969106291a75ba85ad863cae68d667")), }; assert_eq!( address, diff --git a/keys/src/error.rs b/keys/src/error.rs index 5c8b6cc..ebd6ca7 100644 --- a/keys/src/error.rs +++ b/keys/src/error.rs @@ -1,14 +1,28 @@ -#[derive(Debug, PartialEq)] + +use hex::FromHexError; + +#[derive(Debug, PartialEq, scale_info::TypeInfo)] pub enum Error { + // public key InvalidPublic, + // xonly error + InvalidXOnly, + XCoordinateNotExist, + InvalidNoncePoint, InvalidSecret, InvalidMessage, + // sig error InvalidSignature, + SignatureOverflow, InvalidNetwork, InvalidChecksum, InvalidPrivate, InvalidAddress, FailedKeyGeneration, + // hex error + InvalidHexCharacter, + InvalidStringLength, + OddLength, } #[cfg(feature = "std")] @@ -18,14 +32,21 @@ impl core::fmt::Display for Error { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let msg = match *self { Error::InvalidPublic => "Invalid Public", + Error::InvalidXOnly => "Invalid XOnly", + Error::XCoordinateNotExist => "X Coordinate Not Exist", + Error::InvalidNoncePoint => "Invalid nonce point R", Error::InvalidSecret => "Invalid Secret", Error::InvalidMessage => "Invalid Message", Error::InvalidSignature => "Invalid Signature", + Error::SignatureOverflow => "Signature Overflow", Error::InvalidNetwork => "Invalid Network", Error::InvalidChecksum => "Invalid Checksum", Error::InvalidPrivate => "Invalid Private", Error::InvalidAddress => "Invalid Address", Error::FailedKeyGeneration => "Key generation failed", + Error::InvalidHexCharacter => "Invalid hex character", + Error::InvalidStringLength => "Invalid string length", + Error::OddLength => "Hex odd length", }; msg.fmt(f) @@ -43,3 +64,13 @@ impl From for Error { } } } + +impl From for Error { + fn from(e: FromHexError) -> Self { + match e { + FromHexError::InvalidHexCharacter { .. } => Error::InvalidHexCharacter, + FromHexError::InvalidStringLength => Error::InvalidStringLength, + FromHexError::OddLength => Error::OddLength, + } + } +} diff --git a/keys/src/keypair.rs b/keys/src/keypair.rs index 8b87f25..aac0010 100644 --- a/keys/src/keypair.rs +++ b/keys/src/keypair.rs @@ -4,13 +4,13 @@ use core::fmt; use light_bitcoin_primitives::{H264, H520}; -use crate::address::{Address, Network, Type}; +use crate::address::{Address, AddressTypes, Network, Type}; use crate::error::Error; use crate::private::Private; use crate::public::Public; use crate::Secret; -#[derive(Ord, PartialOrd, Eq, PartialEq, Debug, Copy, Clone, Default)] +#[derive(Ord, PartialOrd, Eq, PartialEq, Debug, Copy, Clone, Default, scale_info::TypeInfo)] pub struct KeyPair { private: Private, public: Public, @@ -70,7 +70,7 @@ impl KeyPair { Address { kind: Type::P2PKH, network: self.private.network, - hash: self.public.address_hash(), + hash: AddressTypes::Legacy(self.public.address_hash()), } } } diff --git a/keys/src/lib.rs b/keys/src/lib.rs index de115cc..750f6cf 100644 --- a/keys/src/lib.rs +++ b/keys/src/lib.rs @@ -11,17 +11,21 @@ mod error; mod keypair; mod private; mod public; +mod schnorr; mod signature; +mod tagged; use light_bitcoin_primitives::*; -pub use self::address::{Address, Network, Type}; +pub use self::address::{Address, AddressTypes, Network, Type}; pub use self::display::DisplayLayout; pub use self::error::Error; pub use self::keypair::KeyPair; pub use self::private::Private; -pub use self::public::Public; -pub use self::signature::{CompactSignature, Signature}; +pub use self::public::{Public, XOnly}; +pub use self::schnorr::*; +pub use self::signature::{CompactSignature, SchnorrSignature, Signature}; +pub use self::tagged::*; /// 20 bytes long hash derived from public `ripemd160(sha256(public))` pub type AddressHash = H160; diff --git a/keys/src/private.rs b/keys/src/private.rs index cd54fc8..a4229f9 100644 --- a/keys/src/private.rs +++ b/keys/src/private.rs @@ -14,7 +14,7 @@ use crate::signature::{CompactSignature, Signature}; use crate::{Message, Secret}; /// Secret with additional network identifier and format type -#[derive(Ord, PartialOrd, Eq, PartialEq, Debug, Copy, Clone, Default)] +#[derive(Ord, PartialOrd, Eq, PartialEq, Debug, Copy, Clone, Default, scale_info::TypeInfo)] pub struct Private { /// The network on which this key should be used. pub network: Network, diff --git a/keys/src/public.rs b/keys/src/public.rs index df97fdd..0875476 100644 --- a/keys/src/public.rs +++ b/keys/src/public.rs @@ -1,4 +1,8 @@ -use core::{fmt, ops}; +use arrayref::array_mut_ref; +use core::{ + convert::{TryFrom, TryInto}, + fmt, ops, +}; use light_bitcoin_crypto::dhash160; use light_bitcoin_primitives::{H264, H512, H520}; @@ -7,12 +11,17 @@ use codec::{Decode, Encode}; #[cfg(feature = "std")] use serde::{Deserialize, Serialize}; -use crate::error::Error; -use crate::signature::{CompactSignature, Signature}; -use crate::{AddressHash, Message}; +use crate::{ + error::Error, + schnorr::verify_schnorr, + signature::{CompactSignature, SchnorrSignature, Signature}, + tagged::HashInto, + AddressHash, Message, +}; +use secp256k1::curve::{Affine, Field}; /// Secret public key -#[derive(Ord, PartialOrd, Eq, PartialEq, Copy, Clone)] +#[derive(Ord, PartialOrd, Eq, PartialEq, Copy, Clone, scale_info::TypeInfo)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[cfg_attr(feature = "std", serde(untagged))] #[derive(Encode, Decode)] @@ -58,6 +67,13 @@ impl Default for Public { } } +impl TryFrom for musig2::PublicKey { + type Error = Error; + fn try_from(p: Public) -> Result { + musig2::PublicKey::parse_slice(&p.to_vec()).map_err(|_| Error::InvalidPublic) + } +} + impl Public { pub fn from_slice(data: &[u8]) -> Result { match data.len() { @@ -84,6 +100,18 @@ impl Public { Ok(secp256k1::verify(&message, &signature, &public)) } + pub fn verify_schnorr(&self, message: &Message, signature: [u8; 64]) -> Result { + let public = match self { + Public::Normal(pubkey) => secp256k1::PublicKey::parse(pubkey.as_fixed_bytes())?, + Public::Compressed(pubkey) => { + secp256k1::PublicKey::parse_compressed(pubkey.as_fixed_bytes())? + } + }; + let xonly = public.try_into()?; + let signature = SchnorrSignature::try_from(signature)?; + verify_schnorr(&signature, message, xonly) + } + pub fn verify_compact(&self, message: &Message, signature: &[u8; 64]) -> Result { let public = match self { Public::Normal(pubkey) => secp256k1::PublicKey::parse(pubkey.as_fixed_bytes())?, @@ -116,6 +144,150 @@ impl Public { } } +/// An [`XOnly`] is the compressed representation of a [`PublicKey`] which +/// only stores the x-coordinate of the point. +/// +/// X-only public keys become equivalent to a compressed public key +/// that is the X-only key prefixed by the byte 0x02 +#[derive(Ord, PartialOrd, Eq, PartialEq, Copy, Clone, Debug, scale_info::TypeInfo)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[derive(Encode, Decode)] +pub struct XOnly(pub [u8; 32]); + +impl XOnly { + pub fn on_curve(&self) -> Result { + let mut elem = secp256k1::curve::Field::default(); + let mut affine_coords = secp256k1::curve::Affine::default(); + if elem.set_b32(&self.0) && affine_coords.set_xquad(&elem) { + Ok(true) + } else { + Err(Error::XCoordinateNotExist) + } + } + + pub fn verify(&self, message: &Message, signature: [u8; 64]) -> Result { + let signature = SchnorrSignature::try_from(signature)?; + + verify_schnorr(&signature, message, *self) + } +} + +/// Convert [`Field`] to [`XOnly`] +impl From<&mut secp256k1::curve::Field> for XOnly { + fn from(field: &mut secp256k1::curve::Field) -> Self { + field.normalize(); + let slice = field.b32(); + Self(slice) + } +} + +impl TryFrom<&[u8]> for XOnly { + type Error = Error; + + fn try_from(slice: &[u8]) -> Result { + let mut bytes = [0u8; 32]; + bytes.copy_from_slice(slice); + Ok(Self(bytes)) + } +} + +/// Parse [`XOnly`] from hex +impl TryFrom<&str> for XOnly { + type Error = Error; + + fn try_from(value: &str) -> Result { + let x_bytes = hex::decode(value)?; + if x_bytes.len() != 32 { + return Err(Error::InvalidStringLength); + } + x_bytes[..].try_into() + } +} + +/// Parse [`XOnly`] from 32 bytes +impl TryFrom<[u8; 32]> for XOnly { + type Error = Error; + + fn try_from(value: [u8; 32]) -> Result { + let mut elem = secp256k1::curve::Field::default(); + let mut affine_coords = secp256k1::curve::Affine::default(); + if elem.set_b32(&value) && affine_coords.set_xquad(&elem) { + Ok(Self(value)) + } else { + Err(Error::XCoordinateNotExist) + } + } +} + +/// Convert [`PublicKey`] to [`XOnly`] +/// +/// The public keys constructed by such an algorithm (assuming they use the 33-byte compressed encoding) +/// need to be converted by dropping the first byte. +/// +/// [BIP340]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#public-key-conversion +impl TryFrom for XOnly { + type Error = Error; + + fn try_from(pubkey: secp256k1::PublicKey) -> Result { + let conmpressed = pubkey.serialize_compressed(); + let (_, right) = conmpressed.split_at(1); + right.try_into() + } +} + +/// Convert [`XOnly`] to [`PublicKey`] +impl TryInto for XOnly { + type Error = Error; + + fn try_into(self) -> Result { + let mut elem = Field::default(); + let mut affine = Affine::default(); + if elem.set_b32(&self.0) && affine.set_xo_var(&elem, false) { + let mut ret = [0u8; 65]; + ret[0] = 0x04; + affine.x.normalize_var(); + affine.y.normalize_var(); + affine.x.fill_b32(array_mut_ref!(ret, 1, 32)); + affine.y.fill_b32(array_mut_ref!(ret, 33, 32)); + Ok(secp256k1::PublicKey::parse(&ret)?) + } else { + Err(Error::XCoordinateNotExist) + } + + // let mut x = secp256k1::curve::Field::default(); + // let _ = x.set_b32(&self.0); + // x.normalize(); + // + // let mut elem = secp256k1::curve::Affine::default(); + // elem.set_xquad(&x); + // elem.y.normalize(); + // // determine the first byte of the compressed format public key + // let tag = if elem.y.is_odd() { + // // need to convert y to an even number + // secp256k1::util::TAG_PUBKEY_EVEN + // } else { + // secp256k1::util::TAG_PUBKEY_ODD + // }; + // // construct compressed public key + // let mut c = [0u8; 33]; + // for (i, byte) in c.iter_mut().enumerate() { + // if i == 0 { + // *byte = tag; + // continue; + // } + // *byte = x.b32()[i - 1]; + // } + // let p = secp256k1::PublicKey::parse_compressed(&c)?; + // Ok(p) + } +} + +impl HashInto for XOnly { + fn hash_into(&self, hash: &mut impl digest::Digest) { + hash.update(self.0) + } +} + #[test] fn test_serde_public() { #[derive(Ord, PartialOrd, Eq, PartialEq, Debug, Copy, Clone)] diff --git a/keys/src/schnorr.rs b/keys/src/schnorr.rs new file mode 100644 index 0000000..7dd284f --- /dev/null +++ b/keys/src/schnorr.rs @@ -0,0 +1,303 @@ +//! This is the basic implementation of the schnorr signature algorithm. +//! +//! Here are some implementation references: +//! [`bitcoin`]: https://github.com/bitcoin/bitcoin/blob/3820090bd6/src/secp256k1/src/modules/schnorrsig/main_impl.h +//! [`taproot-workshop`]: https://github.com/bitcoinops/taproot-workshop/blob/master/solutions/1.1-schnorr-signatures-solutions.ipynb +#![allow(non_snake_case)] + +use core::convert::TryInto; +use core::ops::Neg; + +use crate::{ + public::XOnly, + signature::SchnorrSignature, + tagged::{HashAdd, Tagged}, + Error, Message, +}; +use digest::Digest; +use secp256k1::{ + curve::{Affine, Jacobian, Scalar, ECMULT_CONTEXT}, + PublicKey, SecretKey, +}; + +/// Verify a schnorr signature +pub fn verify_schnorr( + sig: &SchnorrSignature, + msg: &Message, + xonlypubkey: XOnly, +) -> Result { + let (rx, s) = (&sig.rx, &sig.s); + + // Determine if the x coordinate is on the elliptic curve + // Also here it will be verified that there are two y's at point x + if rx.on_curve().is_err() { + return Err(Error::XCoordinateNotExist); + } + + // Detect signature overflow + let mut s_check = Scalar::default(); + let s_choice = s_check.set_b32(&s.b32()); + if s_choice.unwrap_u8() == 1 { + return Err(Error::SignatureOverflow); + } + + let pubkey: PublicKey = xonlypubkey.try_into()?; + let mut P: Affine = pubkey.into(); + + // Note that the correctness of verification relies on the fact that + // lift_x always returns a point with an even Y coordinate. + P.y.normalize(); + let mut P = if P.y.is_odd() { P.neg() } else { P }; + + let mut pj = secp256k1::curve::Jacobian::default(); + pj.set_ge(&P); + + let pkx = (&mut P.x).into(); + + let h = schnorrsig_challenge(rx, &pkx, msg); + + let mut rj = Jacobian::default(); + ECMULT_CONTEXT.ecmult(&mut rj, &pj, &h.neg(), s); + + let mut R = Affine::from_gej(&rj); + + if R.is_infinity() { + return Err(Error::InvalidNoncePoint); + } + R.y.normalize_var(); + + if R.y.is_odd() { + return Err(Error::InvalidNoncePoint); + } + + // S == R + h * P + let Rx: XOnly = (&mut R.x).into(); + if rx == &Rx { + Ok(true) + } else { + Err(Error::InvalidSignature) + } +} + +/// Construct schnorr sig challenge +/// hash(R_x|P_x|msg) +pub fn schnorrsig_challenge(rx: &XOnly, pkx: &XOnly, msg: &Message) -> Scalar { + let mut bytes = [0u8; 32]; + + let hash = sha2::Sha256::default().tagged(b"BIP0340/challenge"); + let tagged = hash.add(rx).add(pkx).add(&msg.0).finalize(); + + bytes.copy_from_slice(tagged.as_slice()); + let mut scalar = Scalar::default(); + let _ = scalar.set_b32(&bytes); + scalar +} + +/// Generate nonce k and nonce point R +pub fn nonce_function_bip340( + bip340_sk: &Scalar, + bip340_pkx: &XOnly, + msg: &Message, + aux: &Message, +) -> Result<(Scalar, Affine), Error> { + let aux_hash = sha2::Sha256::default().tagged(b"BIP0340/aux"); + let aux_tagged = aux_hash.add(&aux.0).finalize(); + let sec_bytes: [u8; 32] = bip340_sk.b32(); + let mut aux_bytes = [0u8; 32]; + aux_bytes.copy_from_slice(&aux_tagged); + + // bitwise xor the hashed randomness with secret + for (i, byte) in aux_bytes.iter_mut().enumerate() { + *byte ^= sec_bytes[i] + } + + let nonce_hash = sha2::Sha256::default().tagged(b"BIP0340/nonce"); + let nonce_tagged = nonce_hash + .add(&aux_bytes) + .add(bip340_pkx) + .add(&msg.0) + .finalize(); + + let mut nonce_bytes = [0u8; 32]; + nonce_bytes.copy_from_slice(nonce_tagged.as_slice()); + let mut scalar = Scalar::default(); + let _ = scalar.set_b32(&nonce_bytes); + let k = SecretKey::parse(&scalar.b32())?; + let R = PublicKey::from_secret_key(&k); + Ok((k.into(), R.into())) +} + +/// Sign a message using the secret key with aux +pub fn sign_with_aux( + msg: Message, + aux: Message, + seckey: SecretKey, +) -> Result { + let pubkey = PublicKey::from_secret_key(&seckey); + + let mut pk: Affine = pubkey.into(); + + pk.x.normalize(); + pk.y.normalize(); + + let pkx = XOnly::from(&mut pk.x); + + let sk: Scalar = seckey.clone().into(); + let sec = if pk.y.is_odd() { sk.neg() } else { sk }; + + // Get nonce k and nonce point R + let (k, mut R) = nonce_function_bip340(&sec, &pkx, &msg, &aux)?; + R.y.normalize(); + R.x.normalize(); + let k_even = if R.y.is_odd() { k.neg() } else { k }; + + // Generate s = k + tagged_hash("BIP0340/challenge", R_x|P_x|msg) * d + let rx = XOnly::from(&mut R.x); + let h = schnorrsig_challenge(&rx, &pkx, &msg); + let s = k_even + h * seckey.into(); + + // Generate sig = R_x|s + Ok(SchnorrSignature { rx, s }) +} + +#[cfg(test)] +mod tests { + use core::convert::{TryFrom, TryInto}; + use light_bitcoin_primitives::h256; + + use super::*; + + /// BIP340 test vectors + /// https://github.com/bitcoin/bips/blob/master/bip-0340/test-vectors.csv + const SECRET_0: &str = "0000000000000000000000000000000000000000000000000000000000000003"; + const SECRET_1: &str = "B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF"; + const SECRET_2: &str = "C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9"; + const SECRET_3: &str = "0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710"; + + const PUBKEY_4: &str = "D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9"; + const PUBKEY_5: &str = "EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34"; + const PUBKEY_6: &str = "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659"; + const PUBKEY_7: &str = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30"; + + const AUX_0: &str = "0000000000000000000000000000000000000000000000000000000000000000"; + const AUX_1: &str = "0000000000000000000000000000000000000000000000000000000000000001"; + const AUX_2: &str = "C87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906"; + const AUX_3: &str = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"; + + const MESSAGE_0: &str = "0000000000000000000000000000000000000000000000000000000000000000"; + const MESSAGE_1: &str = "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89"; + const MESSAGE_2: &str = "7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C"; + const MESSAGE_3: &str = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"; + const MESSAGE_4: &str = "4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703"; + const MESSAGE_5: &str = "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89"; + + const SIGNATURE_0: &str = "E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA821525F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0"; + const SIGNATURE_1: &str = "6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE33418906D11AC976ABCCB20B091292BFF4EA897EFCB639EA871CFA95F6DE339E4B0A"; + const SIGNATURE_2: &str = "5831AAEED7B44BB74E5EAB94BA9D4294C49BCF2A60728D8B4C200F50DD313C1BAB745879A5AD954A72C45A91C3A51D3C7ADEA98D82F8481E0E1E03674A6F3FB7"; + const SIGNATURE_3: &str = "7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC97582B9CB13DB3933705B32BA982AF5AF25FD78881EBB32771FC5922EFC66EA3"; + const SIGNATURE_4: &str = "00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C6376AFB1548AF603B3EB45C9F8207DEE1060CB71C04E80F593060B07D28308D7F4"; + const SIGNATURE_5: &str = "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B"; + const SIGNATURE_6: &str = "FFF97BD5755EEEA420453A14355235D382F6472F8568A18B2F057A14602975563CC27944640AC607CD107AE10923D9EF7A73C643E166BE5EBEAFA34B1AC553E2"; + const SIGNATURE_7: &str = "1FA62E331EDBC21C394792D2AB1100A7B432B013DF3F6FF4F99FCB33E0E1515F28890B3EDB6E7189B630448B515CE4F8622A954CFE545735AAEA5134FCCDB2BD"; + const SIGNATURE_8: &str = "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769961764B3AA9B2FFCB6EF947B6887A226E8D7C93E00C5ED0C1834FF0D0C2E6DA6"; + const SIGNATURE_9: &str = "0000000000000000000000000000000000000000000000000000000000000000123DDA8328AF9C23A94C1FEECFD123BA4FB73476F0D594DCB65C6425BD186051"; + const SIGNATURE_10: &str = "00000000000000000000000000000000000000000000000000000000000000017615FBAF5AE28864013C099742DEADB4DBA87F11AC6754F93780D5A1837CF197"; + const SIGNATURE_11: &str = "4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B"; + const SIGNATURE_12: &str = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B"; + const SIGNATURE_13: &str = "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"; + const SIGNATURE_14: &str = "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B"; + + fn check_sign(secret: &str, msg: &str, aux: &str, signature: &str) -> Result { + let m = h256(msg); + let a = h256(aux); + + let seckey = SecretKey::parse_slice(hex::decode(secret).unwrap().as_slice())?; + let sig = sign_with_aux(m, a, seckey)?; + + Ok(sig.eq(&signature.try_into()?)) + } + + fn check_verify(sig: &str, msg: &str, pubkey: &str) -> Result { + let s = sig.try_into()?; + + let px = XOnly::try_from(pubkey)?; + let m = Message::from_slice(&hex::decode(msg)?[..]); + + verify_schnorr(&s, &m, px) + } + + #[test] + fn test_sign() { + assert_eq!( + check_sign(SECRET_0, MESSAGE_0, AUX_0, SIGNATURE_0), + Ok(true) + ); + assert_eq!( + check_sign(SECRET_1, MESSAGE_1, AUX_1, SIGNATURE_1), + Ok(true) + ); + assert_eq!( + check_sign(SECRET_2, MESSAGE_2, AUX_2, SIGNATURE_2), + Ok(true) + ); + assert_eq!( + check_sign(SECRET_3, MESSAGE_3, AUX_3, SIGNATURE_3), + Ok(false) + ); + } + + #[test] + fn test_verify() { + assert_eq!(check_verify(SIGNATURE_4, MESSAGE_4, PUBKEY_4), Ok(true)); + // public key not on the curve + assert_eq!( + check_verify(SIGNATURE_5, MESSAGE_5, PUBKEY_5), + Err(Error::XCoordinateNotExist) + ); + // has_even_y(R) is false + assert_eq!( + check_verify(SIGNATURE_6, MESSAGE_5, PUBKEY_6), + Err(Error::InvalidNoncePoint) + ); + // negated message + assert_eq!( + check_verify(SIGNATURE_7, MESSAGE_5, PUBKEY_6), + Err(Error::InvalidNoncePoint) + ); + // negated s value + assert_eq!( + check_verify(SIGNATURE_8, MESSAGE_5, PUBKEY_6), + Err(Error::InvalidSignature) + ); + // sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 0 + assert_eq!( + check_verify(SIGNATURE_9, MESSAGE_5, PUBKEY_6), + Err(Error::XCoordinateNotExist) + ); + // sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 1 + assert_eq!( + check_verify(SIGNATURE_10, MESSAGE_5, PUBKEY_6), + Err(Error::InvalidNoncePoint) + ); + // sig[0:32] is not an X coordinate on the curve + assert_eq!( + check_verify(SIGNATURE_11, MESSAGE_5, PUBKEY_6), + Err(Error::XCoordinateNotExist) + ); + // sig[0:32] is equal to field size + assert_eq!( + check_verify(SIGNATURE_12, MESSAGE_5, PUBKEY_6), + Err(Error::XCoordinateNotExist) + ); + // sig[32:64] is equal to curve order + assert_eq!( + check_verify(SIGNATURE_13, MESSAGE_5, PUBKEY_6), + Err(Error::InvalidSignature) + ); + // public key is not a valid X coordinate because it exceeds the field size + assert_eq!( + check_verify(SIGNATURE_14, MESSAGE_5, PUBKEY_7), + Err(Error::XCoordinateNotExist) + ); + } +} diff --git a/keys/src/signature.rs b/keys/src/signature.rs index 6583b32..a4ebfa9 100644 --- a/keys/src/signature.rs +++ b/keys/src/signature.rs @@ -4,13 +4,17 @@ #[cfg(not(feature = "std"))] use alloc::vec::Vec; -use core::{fmt, ops, str}; +use core::{ + convert::{TryFrom, TryInto}, + fmt, ops, str, +}; +use secp256k1::curve::Scalar; use light_bitcoin_primitives::H520; -use crate::error::Error; +use crate::{error::Error, public::XOnly}; -#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Default)] +#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Default, scale_info::TypeInfo)] pub struct Signature(Vec); impl fmt::Debug for Signature { @@ -72,7 +76,7 @@ impl<'a> From<&'a [u8]> for Signature { } /// Recovery ID (1 byte) + Compact signature (64 bytes) -#[derive(Ord, PartialOrd, Eq, PartialEq, Copy, Clone, Default)] +#[derive(Ord, PartialOrd, Eq, PartialEq, Copy, Clone, Default, scale_info::TypeInfo)] pub struct CompactSignature(H520); impl fmt::Debug for CompactSignature { @@ -126,3 +130,79 @@ impl From for H520 { s.0 } } + +/// This is 64-byte schnorr signature. +/// +/// More details: +/// [`BIP340`]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#design +/// A standard for 64-byte Schnorr signatures over the elliptic curve secp256k1 +#[derive(Eq, PartialEq, Clone)] +pub struct SchnorrSignature { + pub rx: XOnly, + pub s: Scalar, +} + +impl TryFrom<&str> for SchnorrSignature { + type Error = Error; + + fn try_from(value: &str) -> Result { + let mut s_slice = [0u8; 64]; + s_slice.copy_from_slice(&hex::decode(value)?[..]); + s_slice.try_into() + } +} + +impl TryFrom<[u8; 64]> for SchnorrSignature { + type Error = Error; + + fn try_from(bytes: [u8; 64]) -> Result { + let mut rx_bytes = [0u8; 32]; + rx_bytes.copy_from_slice(&bytes[0..32]); + + let mut s_bytes = [0u8; 32]; + s_bytes.copy_from_slice(&bytes[32..64]); + let mut s = Scalar::default(); + let _ = s.set_b32(&s_bytes); + let rx = rx_bytes.try_into()?; + Ok(SchnorrSignature { rx, s }) + } +} +impl From for [u8; 64] { + fn from(sig: SchnorrSignature) -> Self { + let mut bytes = [0u8; 64]; + bytes[0..32].copy_from_slice(&sig.rx.0[..]); + bytes[32..64].copy_from_slice(&sig.s.b32()[..]); + bytes + } +} + +impl TryFrom<&[u8]> for SchnorrSignature { + type Error = Error; + + fn try_from(bytes: &[u8]) -> Result { + if bytes.len() != 64 { + return Err(Error::InvalidSignature); + } + let mut keys = [0u8; 64]; + keys.copy_from_slice(bytes); + + let mut rx_bytes = [0u8; 32]; + rx_bytes.copy_from_slice(&keys[0..32]); + + let mut s_bytes = [0u8; 32]; + s_bytes.copy_from_slice(&keys[32..64]); + let mut s = Scalar::default(); + let _ = s.set_b32(&s_bytes); + let rx = rx_bytes.try_into()?; + Ok(SchnorrSignature { rx, s }) + } +} + +impl fmt::Debug for SchnorrSignature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut bytes = [0u8; 64]; + bytes[0..32].copy_from_slice(&self.rx.0[..]); + bytes[32..64].copy_from_slice(&self.s.b32()); + hex::encode(bytes).fmt(f) + } +} diff --git a/keys/src/tagged.rs b/keys/src/tagged.rs new file mode 100644 index 0000000..00616a7 --- /dev/null +++ b/keys/src/tagged.rs @@ -0,0 +1,89 @@ +//! Generally useful utilities related to hashing. +//! +//! In general, things in here are defined against the [`Digest`] trait from the [`RustCrypto`] project. +//! +//! [`Digest`]: digest::Digest +//! [`RustCrypto`]: https://github.com/RustCrypto/hashes +//! +//! Code from: +//! [`secp256kfun`]: https://github.com/LLFourn/secp256kfun/blob/master/secp256kfun/src/hash.rs +use digest::{ + generic_array::typenum::{PartialDiv, Unsigned}, + BlockInput, Digest, +}; +use secp256k1::curve::Scalar; +/// Extension trait to "tag" a hash as described in [BIP-340]. +/// +/// [BIP-340]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki +pub trait Tagged: Default + Clone { + /// Returns the _tagged_ (domain separated) SHA256 instance. + /// This is meant be used on SHA256 state with an empty buffer. + fn tagged(&self, tag: &[u8]) -> Self; +} + +impl Tagged for H +where + ::BlockSize: PartialDiv, + <::BlockSize as PartialDiv>::Output: Unsigned, +{ + fn tagged(&self, tag: &[u8]) -> Self { + let hashed_tag = { + let mut hash = H::default(); + hash.update(tag); + hash.finalize() + }; + let mut tagged_hash = self.clone(); + let fill_block = + <>::Output as Unsigned>::to_usize(); + for _ in 0..fill_block { + tagged_hash.update(&hashed_tag[..]); + } + tagged_hash + } +} + +/// Anything that can be hashed. +/// +/// The implementations of this trait decide how the type will be converted into +/// bytes so that it can be included in the hash. +pub trait HashInto { + /// Asks the item to convert itself to bytes and add itself to `hash`. + fn hash_into(&self, hash: &mut impl digest::Digest); +} + +impl HashInto for [u8] { + fn hash_into(&self, hash: &mut impl digest::Digest) { + hash.update(self) + } +} + +impl HashInto for [u8; 32] { + fn hash_into(&self, hash: &mut impl digest::Digest) { + hash.update(self) + } +} + +impl HashInto for str { + fn hash_into(&self, hash: &mut impl digest::Digest) { + hash.update(self.as_bytes()) + } +} + +impl HashInto for Scalar { + fn hash_into(&self, hash: &mut impl digest::Digest) { + hash.update(self.b32()) + } +} + +/// Extension trait for [`digest::Digest`] to make adding things to the hash convenient. +pub trait HashAdd { + /// Converts something that implements [`HashInto`] to bytes and then incorporate the result into the digest (`self`). + fn add(self, data: &HI) -> Self; +} + +impl HashAdd for D { + fn add(mut self, data: &HI) -> Self { + data.hash_into(&mut self); + self + } +} diff --git a/mast/Cargo.toml b/mast/Cargo.toml new file mode 100644 index 0000000..55d8b61 --- /dev/null +++ b/mast/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "light-bitcoin-mast" +version = "0.2.0" +authors = ['The ChainX Authors'] +edition = "2018" +license = "GPL-3.0" + +[dependencies] +arrayref = { version = "0.3.6", default-features = false } +core2 = { version = "0.3.0", default-features = false, features = ["alloc"] } +digest = { version = "0.9.0", default-features = false } +rayon = { version = "1.5.0", optional = true } +sha2 = { version = "0.9.5", default-features = false } +hex = { version = "0.4.3", default-features = false } +bitcoin_hashes = { version = "0.10.0", default-features = false, features = ["alloc"] } + +bitcoin-bech32 = { git = "https://github.com/chainx-org/rust-bech32-bitcoin", branch = "master", default-features = false } +musig2 = { git = "https://github.com/chainx-org/Musig2", branch = "lib", default-features = false } + +light-bitcoin-keys = { path = "../keys", default-features = false } +light-bitcoin-script = { path = "../script", default-features = false } +light-bitcoin-serialization = { path = "../serialization", default-features = false } + +[dev-dependencies] +criterion = { version = "0.3", default-features = false, features = ['html_reports', 'cargo_bench_support'] } + +[features] +default = ['std'] +std = [ + "rayon", + "core2/std", + "digest/std", + "sha2/std", + "hex/std", + "bitcoin_hashes/std", + "musig2/std", + "light-bitcoin-keys/std", + "light-bitcoin-script/std", + "light-bitcoin-serialization/std", +] + +[[bench]] +name = "generate_address" +path = "benches/generate_address.rs" +harness = false \ No newline at end of file diff --git a/mast/benches/generate_address.rs b/mast/benches/generate_address.rs new file mode 100644 index 0000000..bc4489a --- /dev/null +++ b/mast/benches/generate_address.rs @@ -0,0 +1,54 @@ +use criterion::{criterion_group, criterion_main, Bencher, Criterion}; +use light_bitcoin_mast::{ + convert_hex_to_pubkey, generate_combine_index, generate_combine_pubkey, Mast, +}; +use musig2::{PrivateKey, PublicKey}; + +fn bench_generate_combine_index(b: &mut Bencher) { + let n = 20; + let m = 10; + // println!("combine:{}", compute_combine(n, m)); + b.iter(|| generate_combine_index(n, m)); +} + +fn bench_generate_combine_pubkey(b: &mut Bencher) { + let n = 100; + let m = 99; + // println!("combine:{}", compute_combine(n, m)); + let pubkey = convert_hex_to_pubkey("04f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e672"); + let pks = vec![pubkey; n]; + b.iter(|| { + generate_combine_pubkey(pks.clone(), m) + .unwrap() + .iter() + .map(|p| hex::encode(&p.serialize())) + .collect::>() + }); +} + +fn bench_generate_root(b: &mut Bencher) { + let n = 10; + let m = 5; + // println!("combine:{}", compute_combine(n, m)); + let pks = (1..n) + .map(|i| PublicKey::create_from_private_key(&PrivateKey::from_int(i as u32))) + .collect::>(); + b.iter(|| { + let mast = Mast::new(pks.clone(), m).unwrap(); + let _root = mast.calc_root().unwrap(); + }); +} + +fn bench_generate_address(c: &mut Criterion) { + c.bench_function("bench_generate_combine_index", bench_generate_combine_index); + + c.bench_function( + "bench_generate_combine_pubkey", + bench_generate_combine_pubkey, + ); + + c.bench_function("bench_generate_root", bench_generate_root); +} + +criterion_group!(benches, bench_generate_address); +criterion_main!(benches); diff --git a/mast/src/error.rs b/mast/src/error.rs new file mode 100644 index 0000000..29fb2b4 --- /dev/null +++ b/mast/src/error.rs @@ -0,0 +1,68 @@ +use super::*; +use core::result; +use hex::FromHexError; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum MastError { + /// Indicates whether the MAST build error + MastBuildError, + /// Mast generate merkle proof error + MastGenProofError, + /// Mast generate address error + MastGenAddrError, + /// Invalid constructed mast + /// Example: When partial merkle tree contains no pubkey + InvalidMast(String), + /// Format error of hex + FromHexError(String), + // Mainly used to handle io errors of encode + IoError(String), + /// Error which may occur by musig2 + Musig2Error(String), + /// Error which encode to bench32 + EncodeToBech32Error, +} + +impl From for MastError { + fn from(err: io::Error) -> Self { + MastError::IoError(err.to_string()) + } +} + +impl From for MastError { + fn from(e: FromHexError) -> Self { + match e { + FromHexError::InvalidHexCharacter { c, index } => { + MastError::FromHexError(format!("InvalidHexCharacter {}, {}", c, index)) + } + FromHexError::OddLength => MastError::FromHexError("OddLength".to_owned()), + FromHexError::InvalidStringLength => { + MastError::FromHexError("InvalidStringLength".to_owned()) + } + } + } +} + +impl From for MastError { + fn from(e: hashes::hex::Error) -> Self { + match e { + hashes::hex::Error::InvalidChar(c) => { + MastError::FromHexError(format!("InvalidChar {}", c)) + } + hashes::hex::Error::OddLengthString(c) => { + MastError::FromHexError(format!("OddLengthString {}", c)) + } + hashes::hex::Error::InvalidLength(a, b) => { + MastError::FromHexError(format!("InvalidLength {},{}", a, b)) + } + } + } +} + +impl From for MastError { + fn from(e: musig2::error::Error) -> Self { + MastError::Musig2Error(format!("Musig2Error({:?})", e)) + } +} + +pub type Result = result::Result; diff --git a/mast/src/lib.rs b/mast/src/lib.rs new file mode 100644 index 0000000..0668c2f --- /dev/null +++ b/mast/src/lib.rs @@ -0,0 +1,43 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(not(feature = "std"))] +extern crate alloc; + +#[macro_use] +pub extern crate bitcoin_hashes as hashes; + +pub mod error; +pub mod mast; +pub mod pmt; + +pub use crate::mast::*; + +#[cfg(feature = "std")] +use std::io; + +#[cfg(not(feature = "std"))] +use core2::io; + +#[cfg(not(feature = "std"))] +use alloc::{ + borrow::ToOwned, + format, + string::{String, ToString}, +}; + +use hashes::{hash_newtype, sha256d, Hash}; + +hash_newtype!( + LeafNode, + sha256d::Hash, + 32, + doc = "The leaf node of Merkle tree.", + false +); +hash_newtype!( + MerkleNode, + sha256d::Hash, + 32, + doc = "The node of Merkle tree, include leaf node.", + false +); diff --git a/mast/src/mast.rs b/mast/src/mast.rs new file mode 100644 index 0000000..f6df844 --- /dev/null +++ b/mast/src/mast.rs @@ -0,0 +1,394 @@ +#![allow(dead_code)] +#![allow(clippy::module_inception)] + +use core::cmp::min; + +use bitcoin_bech32::{constants::Network, u5, WitnessProgram}; +use light_bitcoin_script::{Builder, Opcode}; +use light_bitcoin_serialization::Stream; + +use super::{ + error::{MastError, Result}, + pmt::PartialMerkleTree, + LeafNode, MerkleNode, +}; + +#[cfg(not(feature = "std"))] +use alloc::{ + string::{String, ToString}, + vec, + vec::Vec, +}; + +use digest::Digest; +use hashes::{ + hex::{FromHex, ToHex}, + Hash, +}; +use light_bitcoin_keys::{HashAdd, Tagged}; +use musig2::{ + key::{PrivateKey, PublicKey}, + musig2::KeyAgg, +}; + +#[cfg(feature = "std")] +use rayon::prelude::*; + +const DEFAULT_TAPSCRIPT_VER: u8 = 0xc0; + +/// Data structure that represents a partial mast tree +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct Mast { + /// The threshold aggregate public key + pubkeys: Vec, + /// The pubkey of all person + inner_pubkey: PublicKey, +} + +impl Mast { + /// Create a mast instance + pub fn new(person_pubkeys: Vec, threshold: usize) -> Result { + let inner_pubkey = KeyAgg::key_aggregation_n(&person_pubkeys)?.X_tilde; + Ok(Mast { + pubkeys: generate_combine_pubkey(person_pubkeys, threshold)?, + inner_pubkey, + }) + } + + /// calculate merkle root + pub fn calc_root(&self) -> Result { + let leaf_nodes = self + .pubkeys + .iter() + .map(tagged_leaf) + .collect::>>()?; + let mut matches = vec![true]; + + // if self.pubkeys.len() < 2 { + // return Err(MastError::MastBuildError); + // } + matches.extend(&vec![false; self.pubkeys.len() - 1]); + let pmt = PartialMerkleTree::from_leaf_nodes(&leaf_nodes, &matches)?; + let mut matches_vec: Vec = vec![]; + let mut indexes_vec: Vec = vec![]; + pmt.extract_matches(&mut matches_vec, &mut indexes_vec) + } + + /// generate merkle proof + pub fn generate_merkle_proof(&self, pubkey: &PublicKey) -> Result> { + if !self.pubkeys.iter().any(|s| *s == *pubkey) { + return Err(MastError::MastGenProofError); + } + + let mut matches = vec![]; + let mut index = 9999; + for (i, s) in self.pubkeys.iter().enumerate() { + if *s == *pubkey { + matches.push(true); + index = i; + } else { + matches.push(false) + } + } + let leaf_nodes = self + .pubkeys + .iter() + .map(tagged_leaf) + .collect::>>()?; + let filter_proof = MerkleNode::from_inner(leaf_nodes[index].into_inner()); + let pmt = PartialMerkleTree::from_leaf_nodes(&leaf_nodes, &matches)?; + let mut matches_vec: Vec = vec![]; + let mut indexes_vec: Vec = vec![]; + let root = pmt.extract_matches(&mut matches_vec, &mut indexes_vec)?; + let tweak = tweak_pubkey(&self.inner_pubkey, &root)?; + let first_bytes: u8 = DEFAULT_TAPSCRIPT_VER | if tweak.is_odd_y() { 0x01 } else { 0x00 }; + Ok([ + vec![first_bytes], + self.inner_pubkey.x_coor().to_vec(), + pmt.collected_hashes(filter_proof).concat(), + ] + .concat()) + } + + /// generate threshold signature tweak pubkey + pub fn generate_tweak_pubkey(&self) -> Result { + let root = self.calc_root()?; + tweak_pubkey(&self.inner_pubkey, &root) + } + + /// generate threshold signature address + pub fn generate_address(&self, network: &str) -> Result { + let network = match network.to_lowercase().as_str() { + "regtest" => Network::Regtest, + "testnet" => Network::Testnet, + "mainnet" => Network::Bitcoin, + "signet" => Network::Signet, + _ => Network::Bitcoin, + }; + let root = self.calc_root()?; + let tweak = tweak_pubkey(&self.inner_pubkey, &root)?; + let witness = WitnessProgram::new( + u5::try_from_u8(1).map_err(|_| MastError::EncodeToBech32Error)?, + tweak.x_coor().to_vec(), + network, + ) + .map_err(|_| MastError::EncodeToBech32Error)?; + Ok(witness.to_string()) + } +} + +/// Calculate the leaf nodes from the pubkey +/// +/// tagged_hash("TapLeaf", bytes([leaf_version]) + ser_size(pubkey)) +pub fn tagged_leaf(pubkey: &PublicKey) -> Result { + let mut stream = Stream::default(); + + let version = DEFAULT_TAPSCRIPT_VER & 0xfe; + + let script = Builder::default() + .push_bytes(&pubkey.x_coor().to_vec()) + .push_opcode(Opcode::OP_CHECKSIG) + .into_script(); + stream.append(&version); + stream.append_list(&script); + let out = stream.out(); + + let hash = sha2::Sha256::default() + .tagged(b"TapLeaf") + .add(&out[..]) + .finalize(); + Ok(LeafNode::from_hex(&hash.to_hex())?) +} + +/// Calculate branch nodes from left and right children +/// +/// tagged_hash("TapBranch", left + right)). The left and right nodes are lexicographic order +pub fn tagged_branch(left_node: MerkleNode, right_node: MerkleNode) -> Result { + // If the hash of the left and right leaves is the same, it means that the total number of leaves is odd + // + // In this case, the parent hash is computed without copying + // Note: `TapLeafHash` will replace the `TapBranchHash` + if left_node != right_node { + let mut x: Vec = vec![]; + let (left_node, right_node) = lexicographical_compare(left_node, right_node); + + x.extend(left_node.to_vec().iter()); + x.extend(right_node.to_vec().iter()); + let hash = sha2::Sha256::default() + .tagged(b"TapBranch") + .add(&x[..]) + .finalize(); + Ok(MerkleNode::from_hex(&hash.to_hex())?) + } else { + Ok(left_node) + } +} + +/// Lexicographic order of left and right nodes +fn lexicographical_compare( + left_node: MerkleNode, + right_node: MerkleNode, +) -> (MerkleNode, MerkleNode) { + if right_node.to_hex() < left_node.to_hex() { + (right_node, left_node) + } else { + (left_node, right_node) + } +} + +/// Compute tweak public key +pub fn tweak_pubkey(inner_pubkey: &PublicKey, root: &MerkleNode) -> Result { + // P + hash_tweak(P||root)G + let mut stream = Stream::default(); + stream.append_slice(&inner_pubkey.x_coor().to_vec()); + stream.append_slice(&root.to_vec()); + let out = stream.out(); + + let hash = sha2::Sha256::default() + .tagged(b"TapTweak") + .add(&out[..]) + .finalize(); + let tweak_key = hash.as_slice(); + let mut bytes = [0u8; 32]; + bytes.copy_from_slice(tweak_key); + let point = PublicKey::create_from_private_key(&PrivateKey::parse(&bytes)?); + if inner_pubkey.is_odd_y() { + Ok(point.add_point(&inner_pubkey.neg())?) + } else { + Ok(point.add_point(inner_pubkey)?) + } +} + +pub fn generate_combine_index(n: usize, k: usize) -> Vec> { + let mut temp: Vec = vec![]; + let mut ans: Vec> = vec![]; + for i in 1..=k { + temp.push(i) + } + temp.push(n + 1); + + let mut j: usize = 0; + while j < k { + ans.push(temp[..k as usize].to_vec()); + j = 0; + + while j < k && temp[j] + 1 == temp[j + 1] { + temp[j] = j + 1; + j += 1; + } + temp[j] += 1; + } + ans +} + +#[cfg(feature = "std")] +pub fn generate_combine_pubkey(pubkeys: Vec, k: usize) -> Result> { + let all_indexs = generate_combine_index(pubkeys.len(), k); + let mut pks = vec![]; + for indexs in all_indexs { + let mut temp: Vec = vec![]; + for index in indexs { + temp.push(pubkeys[index - 1].clone()) + } + pks.push(temp); + } + let mut output = pks + .par_iter() + .map(|ps| Ok(KeyAgg::key_aggregation_n(ps)?.X_tilde)) + .collect::>>()?; + output.sort_by_key(|a| a.x_coor()); + Ok(output) +} + +#[cfg(not(feature = "std"))] +pub fn generate_combine_pubkey(pubkeys: Vec, k: usize) -> Result> { + let all_indexs = generate_combine_index(pubkeys.len(), k); + let mut output: Vec = vec![]; + for indexs in all_indexs { + let mut temp: Vec = vec![]; + for index in indexs { + temp.push(pubkeys[index - 1].clone()) + } + output.push(KeyAgg::key_aggregation_n(&temp)?.X_tilde) + } + output.sort_by_key(|a| a.x_coor()); + Ok(output) +} + +pub fn compute_combine(n: usize, m: usize) -> usize { + let m = min(m, n - m); + (n - m + 1..=n).product::() / (1..=m).product::() +} + +pub fn compute_min_threshold(n: usize, max_value: usize) -> usize { + if n > max_value { + return n; + } + let half = n / 2; + for i in (half..=n).rev() { + if compute_combine(n, i) > max_value { + return i + 1; + } + } + 1 +} + +pub fn convert_hex_to_pubkey(p: &str) -> PublicKey { + let p = hex::decode(p).unwrap(); + if p.len() == 65 { + let mut key = [0u8; 65]; + key.copy_from_slice(&p); + PublicKey::parse(&key).unwrap() + } else if p.len() == 33 { + let mut key = [0u8; 33]; + key.copy_from_slice(&p); + PublicKey::parse_compressed(&key).unwrap() + } else { + panic!("InvalidPublicKey"); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use hashes::hex::ToHex; + + #[test] + fn test_combine_min_threshold() { + assert_eq!(compute_min_threshold(9, 200), 1); + assert_eq!(compute_min_threshold(10, 200), 7); + assert_eq!(compute_min_threshold(20, 200), 18); + } + + #[test] + fn test_generate_combine_pubkey() { + let pubkey_a = convert_hex_to_pubkey("04f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e672"); + let pubkey_b = convert_hex_to_pubkey("04dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba6592ce19b946c4ee58546f5251d441a065ea50735606985e5b228788bec4e582898"); + let pubkey_c = convert_hex_to_pubkey("04dd308afec5777e13121fa72b9cc1b7cc0139715309b086c960e18fd969774eb8f594bb5f72b37faae396a4259ea64ed5e6fdeb2a51c6467582b275925fab1394"); + assert_eq!( + generate_combine_pubkey(vec![pubkey_a, pubkey_b, pubkey_c], 2) + .unwrap() + .iter() + .map(|p| hex::encode(&p.serialize())) + .collect::>(), + vec![ + "0443498bc300426635cd1876077e3993bec1168d6c6fa1138f893ce41a5f51bf0a22a2a7a85830e1f9facf02488328be04ece354730e19ce2766d5dca1478483cd", + "04be1979e5e167d216a1229315844990606c2aba2d582472492a9eec7c9466460a286a71973e72f8d057235855253707ba73b5436d6170e702edf2ed5df46722b2", + "04e7c92d2ef4294389c385fedd5387fba806687f5aba1c7ba285093dacd69354d9b4f9ea87450c75954ade455677475e92fb5e303db36753c2ea20e47d3e939662", + ] + ); + } + + #[test] + fn mast_generate_root_should_work() { + let pubkey_a = convert_hex_to_pubkey("04f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e672"); + let pubkey_b = convert_hex_to_pubkey("04dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba6592ce19b946c4ee58546f5251d441a065ea50735606985e5b228788bec4e582898"); + let pubkey_c = convert_hex_to_pubkey("04dd308afec5777e13121fa72b9cc1b7cc0139715309b086c960e18fd969774eb8f594bb5f72b37faae396a4259ea64ed5e6fdeb2a51c6467582b275925fab1394"); + let person_pubkeys = vec![pubkey_a, pubkey_b, pubkey_c]; + let mast = Mast::new(person_pubkeys, 2).unwrap(); + let root = mast.calc_root().unwrap(); + + assert_eq!( + "69e1de34d13d69fd894d708d656d0557cacaa18a093a6e86327a991d95c6c8e1", + root.to_hex() + ); + } + + #[test] + fn mast_generate_merkle_proof_should_work() { + let pubkey_a = convert_hex_to_pubkey("04f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e672"); + let pubkey_b = convert_hex_to_pubkey("04dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba6592ce19b946c4ee58546f5251d441a065ea50735606985e5b228788bec4e582898"); + let pubkey_c = convert_hex_to_pubkey("04dd308afec5777e13121fa72b9cc1b7cc0139715309b086c960e18fd969774eb8f594bb5f72b37faae396a4259ea64ed5e6fdeb2a51c6467582b275925fab1394"); + let person_pubkeys = vec![pubkey_a, pubkey_b, pubkey_c]; + let mast = Mast::new(person_pubkeys, 2).unwrap(); + let pubkey_ab = convert_hex_to_pubkey("04e7c92d2ef4294389c385fedd5387fba806687f5aba1c7ba285093dacd69354d9b4f9ea87450c75954ade455677475e92fb5e303db36753c2ea20e47d3e939662"); + + let proof = mast.generate_merkle_proof(&pubkey_ab).unwrap(); + + assert_eq!( + hex::encode(&proof), + "c0f4152c91b2c78a3524e7858c72ffa360da59e7c3c4d67d6787cf1e3bfe1684c1e38e30c81fc61186d0ed3956b5e49bd175178a638d1410e64f7716697a7e0ccd", + ) + } + + #[test] + fn test_final_addr() { + let pubkey_alice = convert_hex_to_pubkey( + "0283f579dd2380bd31355d066086e1b4d46b518987c1f8a64d4c0101560280eae2", + ); + let pubkey_bob = convert_hex_to_pubkey( + "027a0868a14bd18e2e45ff3ad960f892df8d0edd1a5685f0a1dc63c7986d4ad55d", + ); + let pubkey_charlie = convert_hex_to_pubkey( + "02c9929543dfa1e0bb84891acd47bfa6546b05e26b7a04af8eb6765fcc969d565f", + ); + let person_pubkeys = vec![pubkey_alice, pubkey_bob, pubkey_charlie]; + let mast = Mast::new(person_pubkeys, 2).unwrap(); + + let addr = mast.generate_address("Mainnet").unwrap(); + assert_eq!( + "bc1pn202yeugfa25nssxk2hv902kmxrnp7g9xt487u256n20jgahuwas6syxhp", + addr + ); + } +} diff --git a/mast/src/pmt.rs b/mast/src/pmt.rs new file mode 100644 index 0000000..3d72af3 --- /dev/null +++ b/mast/src/pmt.rs @@ -0,0 +1,330 @@ +// Refer from https://github.com/rust-bitcoin/rust-bitcoin/blob/master/src/util/merkleblock.rs +#![allow(dead_code)] + +use super::{ + error::{MastError, Result}, + mast::tagged_branch, + LeafNode, MerkleNode, +}; +use hashes::Hash; + +#[cfg(not(feature = "std"))] +use alloc::{borrow::ToOwned, vec, vec::Vec}; + +/// Data structure that represents a partial merkle tree. +/// +/// It represents a subset of the leaf node's of a known node, in a way that +/// allows recovery of the list of leaf node's and the merkle root, in an +/// authenticated way. +/// +/// The encoding works as follows: we traverse the tree in depth-first order, +/// storing a bit for each traversed node, signifying whether the node is the +/// parent of at least one matched leaf node (or a matched leaf node itself). In +/// case we are at the leaf level, or this bit is 0, its merkle node hash is +/// stored, and its children are not explored further. Otherwise, no hash is +/// stored, but we recurse into both (or the only) child branch. During +/// decoding, the same depth-first traversal is performed, consuming bits and +/// hashes as they written during encoding. +/// +/// The serialization is fixed and provides a hard guarantee about the +/// encoded size: +/// +/// SIZE <= 13 + ceil(36.25*N) +/// +/// Where N represents the number of leaf nodes of the partial tree. N itself +/// is bounded by: +/// +/// N <= total_leaf_nodes +/// N <= 1 + matched_leaf_nodes*tree_height +/// +/// The serialization format: +/// - uint32 total_leaf_nodes (4 bytes) +/// - varint number of hashes (1-3 bytes) +/// - uint256[] hashes in depth-first order (<= 32*N bytes) +/// - varint number of bytes of flag bits (1-3 bytes) +/// - byte[] flag bits, packed per 8 in a byte, least significant bit first (<= 2*N-1 bits) +/// - varint number of heights (1-3 bytes) +/// - uint256[] the height of hashes (<= 4*N bytes) +/// The size constraints follow from this. +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct PartialMerkleTree { + /// The total number of leaf nodes in the tree + num_leaf_nodes: u32, + /// node-is-parent-of-matched-leaf_node bits + bits: Vec, + /// pubkey hash and internal hashes + hashes: Vec, + /// The height of hashes + heights: Vec, +} + +impl PartialMerkleTree { + /// Construct a partial merkle tree + /// The `leaf_nodes` are the pubkey hashes and the `matches` is the contains flags + /// wherever a leaf_node hash should be included in the proof. + /// + /// Panics when `leaf_nodes` is empty or when `matches` has a different length + /// ``` + pub fn from_leaf_nodes(leaf_nodes: &[LeafNode], matches: &[bool]) -> Result { + // We can never have zero leaf_nodes in a merkle node + assert_ne!(leaf_nodes.len(), 0); + assert_eq!(leaf_nodes.len(), matches.len()); + + let mut pmt = PartialMerkleTree { + num_leaf_nodes: leaf_nodes.len() as u32, + bits: Vec::with_capacity(leaf_nodes.len()), + hashes: vec![], + heights: vec![], + }; + // calculate height of tree + let height = pmt.calc_tree_height(); + + // traverse the partial tree + if let Ok(()) = pmt.traverse_and_build(height, 0, leaf_nodes, matches) { + Ok(pmt) + } else { + Err(MastError::MastBuildError) + } + } + + /// Extract the matching leaf_node's represented by this partial merkle tree + /// and their respective indices within the partial tree. + /// returns the merkle root, or error in case of failure + pub fn extract_matches( + &self, + matches: &mut Vec, + indexes: &mut Vec, + ) -> Result { + matches.clear(); + indexes.clear(); + // An empty set will not work + if self.num_leaf_nodes == 0 { + return Err(MastError::InvalidMast("No Pubkeys in MAST".to_owned())); + }; + // check for excessively high numbers of leaf_nodes + // if self.num_leaf_nodes > MAX_BLOCK_WEIGHT / MIN_TRANSACTION_WEIGHT { + // return Err(TooManyTransactions); + // } + // there can never be more hashes provided than one for every leaf_node + if self.hashes.len() as u32 > self.num_leaf_nodes { + return Err(MastError::InvalidMast( + "Proof contains more hashes than leaf_nodes".to_owned(), + )); + }; + // there must be at least one bit per node in the partial tree, and at least one node per hash + if self.bits.len() < self.hashes.len() { + return Err(MastError::InvalidMast( + "Proof contains less bits than hashes".to_owned(), + )); + }; + + // calculate height of tree + let height = self.calc_tree_height(); + // traverse the partial tree + let mut bits_used = 0u32; + let mut hash_used = 0u32; + let hash_merkle_root = + self.traverse_and_extract(height, 0, &mut bits_used, &mut hash_used, matches, indexes)?; + // Verify that all bits were consumed (except for the padding caused by + // serializing it as a byte sequence) + if (bits_used + 7) / 8 != (self.bits.len() as u32 + 7) / 8 { + return Err(MastError::InvalidMast( + "Not all bit were consumed".to_owned(), + )); + } + // Verify that all hashes were consumed + if hash_used != self.hashes.len() as u32 { + return Err(MastError::InvalidMast( + "Not all hashes were consumed".to_owned(), + )); + } + Ok(MerkleNode::from_inner(hash_merkle_root.into_inner())) + } + + /// Helper function to efficiently calculate the number of nodes at given height + /// in the merkle tree + #[inline] + fn calc_tree_width(&self, height: u32) -> u32 { + (self.num_leaf_nodes + (1 << height) - 1) >> height + } + + /// Helper function to efficiently calculate the height of merkle tree + fn calc_tree_height(&self) -> u32 { + let mut height = 0u32; + while self.calc_tree_width(height) > 1 { + height += 1; + } + height + } + + /// Calculate the hash of a node in the merkle tree (at leaf level: the leaf_node's themselves) + fn calc_hash(&self, height: u32, pos: u32, leaf_nodes: &[LeafNode]) -> Result { + if height == 0 { + // Hash at height 0 is the leaf_node itself + Ok(MerkleNode::from_inner( + leaf_nodes[pos as usize].into_inner(), + )) + } else { + // Calculate left hash + let left = self.calc_hash(height - 1, pos * 2, leaf_nodes)?; + // Calculate right hash if not beyond the end of the array - copy left hash otherwise + let right = if pos * 2 + 1 < self.calc_tree_width(height - 1) { + self.calc_hash(height - 1, pos * 2 + 1, leaf_nodes)? + } else { + left + }; + // Combine subhashes + // PartialMerkleTree::parent_hash(left, right) + Ok(tagged_branch(left, right)?) + } + } + + /// Recursive function that traverses tree nodes, storing the data as bits and hashes + fn traverse_and_build( + &mut self, + height: u32, + pos: u32, + leaf_nodes: &[LeafNode], + matches: &[bool], + ) -> Result<()> { + // Determine whether this node is the parent of at least one matched leaf_node + let mut parent_of_match = false; + let mut p = pos << height; + while p < (pos + 1) << height && p < self.num_leaf_nodes { + parent_of_match |= matches[p as usize]; + p += 1; + } + // Store as flag bit + self.bits.push(parent_of_match); + + if height == 0 || !parent_of_match { + // If at height 0, or nothing interesting below, store hash and stop + let hash = self.calc_hash(height, pos, leaf_nodes)?; + self.hashes.push(hash); + self.heights.push(height); + } else { + // Otherwise, don't store any hash, but descend into the subtrees + self.traverse_and_build(height - 1, pos * 2, leaf_nodes, matches)?; + if pos * 2 + 1 < self.calc_tree_width(height - 1) { + self.traverse_and_build(height - 1, pos * 2 + 1, leaf_nodes, matches)?; + } + } + + Ok(()) + } + + /// Recursive function that traverses tree nodes, consuming the bits and hashes produced by + /// TraverseAndBuild. It returns the hash of the respective node and its respective index. + fn traverse_and_extract( + &self, + height: u32, + pos: u32, + bits_used: &mut u32, + hash_used: &mut u32, + matches: &mut Vec, + indexes: &mut Vec, + ) -> Result { + if *bits_used as usize >= self.bits.len() { + return Err(MastError::InvalidMast( + "Overflowed the bits array".to_owned(), + )); + } + let parent_of_match = self.bits[*bits_used as usize]; + *bits_used += 1; + if height == 0 || !parent_of_match { + // If at height 0, or nothing interesting below, use stored hash and do not descend + if *hash_used as usize >= self.hashes.len() { + return Err(MastError::InvalidMast( + "Overflowed the hash array".to_owned(), + )); + } + let hash = self.hashes[*hash_used as usize]; + *hash_used += 1; + if height == 0 && parent_of_match { + // in case of height 0, we have a matched leaf_node + matches.push(LeafNode::from_inner(hash.into_inner())); + indexes.push(pos); + } + Ok(hash) + } else { + // otherwise, descend into the subtrees to extract matched leaf_nodes and hashes + let left = self.traverse_and_extract( + height - 1, + pos * 2, + bits_used, + hash_used, + matches, + indexes, + )?; + let right; + if pos * 2 + 1 < self.calc_tree_width(height - 1) { + right = self.traverse_and_extract( + height - 1, + pos * 2 + 1, + bits_used, + hash_used, + matches, + indexes, + )?; + if right == left { + // The left and right branches should never be identical, as the node + // hashes covered by them must each be unique. + return Err(MastError::InvalidMast( + "Found identical node hashes".to_owned(), + )); + } + } else { + right = left; + } + // and combine them before returning + // Ok(PartialMerkleTree::parent_hash(left, right)) + Ok(tagged_branch(left, right)?) + } + } + + pub fn collected_hashes(&self, filter_proof: MerkleNode) -> Vec { + let mut zipped = self + .hashes + .iter() + .zip(&self.heights) + .filter(|(p, _)| **p != filter_proof) + .collect::>(); + zipped.sort_unstable_by_key(|(_, h)| **h); + zipped.into_iter().map(|(a, _)| *a).collect::>() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use hashes::hex::FromHex; + + #[cfg(not(feature = "std"))] + use alloc::{format, vec, vec::Vec}; + + #[test] + fn pmt_proof_generate_correct_order() { + let leaf_nodes: Vec = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + .iter() + .map(|i| LeafNode::from_hex(&format!("{:064x}", i)).unwrap()) + .collect(); + + let matches = vec![ + false, false, false, false, false, false, false, false, false, false, false, true, + ]; + let tree = PartialMerkleTree::from_leaf_nodes(&leaf_nodes, &matches).unwrap(); + let mut matches_vec = vec![]; + let mut indexes = vec![]; + let root = tree + .extract_matches(&mut matches_vec, &mut indexes) + .unwrap(); + + let filter_proof = MerkleNode::from_inner(leaf_nodes[11].into_inner()); + let proofs = tree.collected_hashes(filter_proof); + let mut root1 = filter_proof; + for i in proofs.iter() { + root1 = tagged_branch(root1, *i).unwrap(); + } + assert_eq!(root, root1) + } +} diff --git a/merkle/Cargo.toml b/merkle/Cargo.toml index 932b1d0..c20875f 100644 --- a/merkle/Cargo.toml +++ b/merkle/Cargo.toml @@ -9,6 +9,7 @@ license = "GPL-3.0" default = ["std"] std = [ "codec/std", + "scale-info/std", "light-bitcoin-chain/std", "light-bitcoin-primitives/std", @@ -17,13 +18,15 @@ std = [ [dependencies] codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false } +scale-info = { version = "1.0", default-features = false, features = ["derive"] } + light-bitcoin-chain = { path = "../chain", default-features = false } light-bitcoin-primitives = { path = "../primitives", default-features = false } light-bitcoin-serialization = { path = "../serialization", default-features = false, features = ["derive"] } [dev-dependencies] -hashbrown = "0.9" +hashbrown = "0.11" hex = "0.4" rand = "0.8" light-bitcoin-crypto = { path = "../crypto" } diff --git a/merkle/src/lib.rs b/merkle/src/lib.rs index ee50a30..517478a 100644 --- a/merkle/src/lib.rs +++ b/merkle/src/lib.rs @@ -20,7 +20,7 @@ const MAX_BLOCK_WEIGHT: u32 = 4_000_000; /// The minimum transaction weight for a valid serialized transaction const MIN_TRANSACTION_WEIGHT: u32 = 4 * 60; -#[derive(Debug)] +#[derive(Debug, scale_info::TypeInfo)] pub enum Error { /// When header merkle root don't match to the root calculated from the partial merkle tree MerkleRootMismatch, @@ -85,7 +85,7 @@ impl From<&str> for Error { /// - varint number of bytes of flag bits (1-3 bytes) /// - byte[] flag bits, packed per 8 in a byte, least significant bit first (<= 2*N-1 bits) /// The size constraints follow from this. -#[derive(PartialEq, Eq, Clone, Default)] +#[derive(PartialEq, Eq, Clone, Default, scale_info::TypeInfo)] pub struct PartialMerkleTree { /// The total number of transactions in the block pub tx_count: u32, @@ -351,7 +351,7 @@ impl Deserializable for PartialMerkleTree { impl codec::Encode for PartialMerkleTree { fn encode(&self) -> Vec { - let value = serialize::(&self); + let value = serialize::(self); value.encode() } } diff --git a/primitives/Cargo.toml b/primitives/Cargo.toml index 47b7543..29fc206 100644 --- a/primitives/Cargo.toml +++ b/primitives/Cargo.toml @@ -12,6 +12,7 @@ std = [ "fixed-hash/std", "hex/std", "serde", + "scale-info/std", "impl-serde", "impl-codec/std", "primitive-types/std", @@ -21,9 +22,10 @@ std = [ [dependencies] byteorder = { version = "1.3", default-features = false } fixed-hash = { version = "0.7", default-features = false } -hex = { version = "0.4", default-features = false, features = ["alloc"] } +hex = { version = "0.4", default-features = false} serde = { version = "1.0", features = ["derive"], optional = true } +scale-info = { version = "1.0", default-features = false, features = ["derive"] } impl-serde = { version = "0.3", optional = true } impl-codec = { version = "0.5", default-features = false } -primitive-types = { version = "0.9", default-features = false, features = ["codec"] } +primitive-types = { version = "0.10.1", default-features = false, features = ["codec", "scale-info"] } diff --git a/primitives/src/bytes.rs b/primitives/src/bytes.rs index d6a424b..d8a03a8 100644 --- a/primitives/src/bytes.rs +++ b/primitives/src/bytes.rs @@ -5,7 +5,7 @@ use alloc::{vec, vec::Vec}; use core::{fmt, marker, ops, str}; /// Wrapper around `Vec` -#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Hash, Default)] +#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Hash, Default, scale_info::TypeInfo)] pub struct Bytes(Vec); impl<'a> From<&'a [u8]> for Bytes { @@ -150,7 +150,7 @@ impl<'de> serde::de::Visitor<'de> for BytesVisitor { } /// Wrapper around `Vec` which represent associated type -#[derive(Default, PartialEq, Clone)] +#[derive(Default, PartialEq, Clone, scale_info::TypeInfo)] pub struct TaggedBytes { bytes: Bytes, label: marker::PhantomData, diff --git a/primitives/src/compact.rs b/primitives/src/compact.rs index 9519cbc..ccd507c 100644 --- a/primitives/src/compact.rs +++ b/primitives/src/compact.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use crate::U256; /// Compact representation of `U256` -#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Copy, Default, Debug)] +#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Copy, Default, Debug, scale_info::TypeInfo)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] pub struct Compact(u32); diff --git a/primitives/src/hash.rs b/primitives/src/hash.rs index ab943cd..0f15122 100644 --- a/primitives/src/hash.rs +++ b/primitives/src/hash.rs @@ -7,14 +7,17 @@ use impl_serde::impl_fixed_hash_serde; construct_fixed_hash! { /// Fixed-size uninterpreted hash type with 4 bytes (32 bits) size. + #[derive(scale_info::TypeInfo)] pub struct H32(4); } construct_fixed_hash! { /// Fixed-size uninterpreted hash type with 33 bytes (264 bits) size. + #[derive(scale_info::TypeInfo)] pub struct H264(33); } construct_fixed_hash! { /// Fixed-size uninterpreted hash type with 65 bytes (520 bits) size. + #[derive(scale_info::TypeInfo)] pub struct H520(65); } diff --git a/primitives/src/io/mod.rs b/primitives/src/io/mod.rs index 473dea2..3232cd6 100644 --- a/primitives/src/io/mod.rs +++ b/primitives/src/io/mod.rs @@ -741,7 +741,7 @@ impl<'a> Write for &'a mut [u8] { #[inline] fn write(&mut self, data: &[u8]) -> Result { let amt = cmp::min(data.len(), self.len()); - let (a, b) = mem::replace(self, &mut []).split_at_mut(amt); + let (a, b) = mem::take(self).split_at_mut(amt); a.copy_from_slice(&data[..amt]); *self = b; Ok(amt) diff --git a/script/Cargo.toml b/script/Cargo.toml index 1b03170..30840a1 100644 --- a/script/Cargo.toml +++ b/script/Cargo.toml @@ -9,6 +9,8 @@ license = "GPL-3.0" default = ["std"] std = [ "hex/std", + "sha2/std", + "libsecp256k1/std", "light-bitcoin-chain/std", "light-bitcoin-crypto/std", @@ -19,6 +21,8 @@ std = [ [dependencies] hex = { version = "0.4", default-features = false } +sha2 = { version = "0.9.5", default-features = false } +libsecp256k1 = { version = "0.3.5", default-features = false, features = ["hmac"] } light-bitcoin-chain = { path = "../chain", default-features = false } light-bitcoin-crypto = { path = "../crypto", default-features = false } diff --git a/script/src/builder.rs b/script/src/builder.rs index cfc6c8d..794f4ba 100644 --- a/script/src/builder.rs +++ b/script/src/builder.rs @@ -1,6 +1,7 @@ //! Script builder -use light_bitcoin_keys::AddressHash; +use light_bitcoin_chain::{H160, H256}; +use light_bitcoin_keys::{Address, AddressHash, AddressTypes, Type, XOnly}; use light_bitcoin_primitives::Bytes; use crate::num::Num; @@ -34,6 +35,55 @@ impl Builder { .into_script() } + /// Builds p2wpkh script pubkey + pub fn build_p2wpkh(address: &H160) -> Script { + Builder::default() + .push_opcode(Opcode::OP_0) + .push_bytes(address.as_bytes()) + .into_script() + } + + /// Builds p2wsh script pubkey + pub fn build_p2wsh(address: &H256) -> Script { + Builder::default() + .push_opcode(Opcode::OP_0) + .push_bytes(address.as_bytes()) + .into_script() + } + + /// Builds p2tr script pubkey + pub fn build_p2tr(address: &XOnly) -> Script { + Builder::default() + .push_opcode(Opcode::OP_1) + .push_bytes(&address.0) + .into_script() + } + + pub fn build_address_types(address: &Address) -> Script { + match address.kind { + Type::P2PKH => match address.hash { + AddressTypes::Legacy(h) => Self::build_p2pkh(&h), + _ => unreachable!(), + }, + Type::P2SH => match address.hash { + AddressTypes::Legacy(h) => Self::build_p2sh(&h), + _ => unreachable!(), + }, + Type::P2WPKH => match address.hash { + AddressTypes::WitnessV0KeyHash(h) => Self::build_p2wpkh(&h), + _ => unreachable!(), + }, + Type::P2WSH => match address.hash { + AddressTypes::WitnessV0ScriptHash(h) => Self::build_p2wsh(&h), + _ => unreachable!(), + }, + Type::P2TR => match address.hash { + AddressTypes::WitnessV1Taproot(h) => Self::build_p2tr(&h), + _ => unreachable!(), + }, + } + } + /// Builds op_return script pub fn build_nulldata(bytes: &[u8]) -> Script { Builder::default() diff --git a/script/src/error.rs b/script/src/error.rs index ae55b86..aa31976 100644 --- a/script/src/error.rs +++ b/script/src/error.rs @@ -65,6 +65,14 @@ pub enum Error { WitnessMalleatedP2SH, WitnessUnexpected, WitnessPubKeyType, + + // Taproot check errors + SpentOutputsNumDismatch, + NotTaprootWitness, + LastElementNotExist, + EmptyWitness, + InvalidSignature, + VerifyCommitment, } #[cfg(feature = "std")] @@ -139,6 +147,16 @@ impl core::fmt::Display for Error { Error::WitnessMalleatedP2SH => "Witness requires only-redeemscript scriptSig".fmt(f), Error::WitnessUnexpected => "Witness provided for non-witness script".fmt(f), Error::WitnessPubKeyType => "Using non-compressed keys in segwit".fmt(f), + + // Taproot check errors + Error::SpentOutputsNumDismatch => { + "Transaction inputs does not match spent outputs".fmt(f) + } + Error::NotTaprootWitness => "Not pay to taproot witness".fmt(f), + Error::LastElementNotExist => "Last element not exist in script witness".fmt(f), + Error::EmptyWitness => "Witness is empty".fmt(f), + Error::InvalidSignature => "Signature resolution failed".fmt(f), + Error::VerifyCommitment => "Failure to verify commitment".fmt(f), } } } diff --git a/script/src/lib.rs b/script/src/lib.rs index 4ac9273..e3dfc96 100644 --- a/script/src/lib.rs +++ b/script/src/lib.rs @@ -23,5 +23,8 @@ pub use self::script::{ is_witness_commitment_script, Script, ScriptAddress, ScriptType, ScriptWitness, MAX_OPS_PER_SCRIPT, MAX_PUBKEYS_PER_MULTISIG, MAX_SCRIPT_ELEMENT_SIZE, MAX_SCRIPT_SIZE, }; -pub use self::sign::{SignatureVersion, TransactionInputSigner, UnsignedTransactionInput}; +pub use self::sign::{ + check_taproot_tx, ScriptExecutionData, SignatureVersion, TransactionInputSigner, + UnsignedTransactionInput, +}; pub use self::verify::{NoopSignatureChecker, SignatureChecker, TransactionSignatureChecker}; diff --git a/script/src/script.rs b/script/src/script.rs index dc56ac1..a366749 100644 --- a/script/src/script.rs +++ b/script/src/script.rs @@ -3,9 +3,8 @@ #[cfg(not(feature = "std"))] use alloc::{vec, vec::Vec}; use core::{fmt, ops, str}; - -use light_bitcoin_keys::{self as keys, AddressHash, Public}; -use light_bitcoin_primitives::Bytes; +use light_bitcoin_keys::{self as keys, AddressHash, AddressTypes, Public, XOnly}; +use light_bitcoin_primitives::{Bytes, H160, H256}; use crate::error::Error; use crate::opcode::Opcode; @@ -31,8 +30,10 @@ pub enum ScriptType { ScriptHash, Multisig, NullData, - WitnessScript, - WitnessKey, + WitnessV0Scripthash, + WitnessV0Keyhash, + WitnessV1Taproot, + WitnessUnknown, } /// Address from Script @@ -41,7 +42,7 @@ pub struct ScriptAddress { /// The type of the address. pub kind: keys::Type, /// Public key hash. - pub hash: AddressHash, + pub hash: AddressTypes, } impl ScriptAddress { @@ -49,7 +50,7 @@ impl ScriptAddress { pub fn new_p2pkh(hash: AddressHash) -> Self { ScriptAddress { kind: keys::Type::P2PKH, - hash, + hash: AddressTypes::Legacy(hash), } } @@ -57,7 +58,31 @@ impl ScriptAddress { pub fn new_p2sh(hash: AddressHash) -> Self { ScriptAddress { kind: keys::Type::P2SH, - hash, + hash: AddressTypes::Legacy(hash), + } + } + + /// Creates P2WPKH-type ScriptAddress + pub fn new_p2wpkh(hash: H160) -> Self { + ScriptAddress { + kind: keys::Type::P2WPKH, + hash: AddressTypes::WitnessV0KeyHash(hash), + } + } + + /// Creates P2WSH-type ScriptAddress + pub fn new_p2wsh(hash: H256) -> Self { + ScriptAddress { + kind: keys::Type::P2WSH, + hash: AddressTypes::WitnessV0ScriptHash(hash), + } + } + + /// Creates P2TR-type ScriptAddress + pub fn new_p2tr(hash: XOnly) -> Self { + ScriptAddress { + kind: keys::Type::P2TR, + hash: AddressTypes::WitnessV1Taproot(hash), } } } @@ -211,6 +236,13 @@ impl Script { && self.data[1] == Opcode::OP_PUSHBYTES_32 as u8 } + /// Extra-fast test for pay-to-witness-taproot scripts. + pub fn is_pay_to_witness_taproot(&self) -> bool { + self.data.len() == 34 + && self.data[0] == Opcode::OP_1 as u8 + && self.data[1] == Opcode::OP_PUSHBYTES_32 as u8 + } + /// Extra-fast test for multisig scripts. pub fn is_multisig_script(&self) -> bool { if self.data.len() < 3 { @@ -394,9 +426,11 @@ impl Script { } else if self.is_null_data_script() { ScriptType::NullData } else if self.is_pay_to_witness_key_hash() { - ScriptType::WitnessKey + ScriptType::WitnessV0Keyhash } else if self.is_pay_to_witness_script_hash() { - ScriptType::WitnessScript + ScriptType::WitnessV0Scripthash + } else if self.is_pay_to_witness_taproot() { + ScriptType::WitnessV1Taproot } else { ScriptType::NonStandard } @@ -493,12 +527,18 @@ impl Script { Ok(addresses) } ScriptType::NullData => Ok(vec![]), - ScriptType::WitnessScript => { - Ok(vec![]) // TODO - } - ScriptType::WitnessKey => { - Ok(vec![]) // TODO + ScriptType::WitnessV0Scripthash => Ok(vec![ScriptAddress::new_p2wsh( + H256::from_slice(&self.data[2..34]), + )]), + ScriptType::WitnessV0Keyhash => Ok(vec![ScriptAddress::new_p2wpkh(H160::from_slice( + &self.data[2..22], + ))]), + ScriptType::WitnessV1Taproot => { + let mut keys = [0u8; 32]; + keys.copy_from_slice(&self.data[2..34]); + Ok(vec![ScriptAddress::new_p2tr(XOnly(keys))]) } + ScriptType::WitnessUnknown => Ok(vec![]), } } @@ -754,6 +794,14 @@ mod tests { assert!(!script2.is_pay_to_witness_script_hash()); } + #[test] + fn test_is_pay_to_taproot() { + let script: Script = "512052898a03a9f04bb83f8a48fb953089de10e6ee70658b059551ebf7c008b05b7a" + .parse() + .unwrap(); + assert!(script.is_pay_to_witness_taproot()); + } + #[test] fn test_script_debug() { let script = Builder::default() @@ -897,7 +945,7 @@ OP_ADD assert_eq!(script.script_type(), ScriptType::PubKey); assert_eq!( script.extract_destinations(), - Ok(vec![ScriptAddress::new_p2pkh(address),]) + Ok(vec![ScriptAddress::new_p2pkh(address)]) ); } @@ -912,7 +960,7 @@ OP_ADD assert_eq!(script.script_type(), ScriptType::PubKey); assert_eq!( script.extract_destinations(), - Ok(vec![ScriptAddress::new_p2pkh(address),]) + Ok(vec![ScriptAddress::new_p2pkh(address)]) ); } @@ -922,11 +970,15 @@ OP_ADD .parse::
() .unwrap() .hash; + let address = match address { + keys::AddressTypes::Legacy(h) => h, + _ => todo!(), + }; let script = Builder::build_p2pkh(&address); assert_eq!(script.script_type(), ScriptType::PubKeyHash); assert_eq!( script.extract_destinations(), - Ok(vec![ScriptAddress::new_p2pkh(address),]) + Ok(vec![ScriptAddress::new_p2pkh(address)]) ); } @@ -936,11 +988,15 @@ OP_ADD .parse::
() .unwrap() .hash; + let address = match address { + keys::AddressTypes::Legacy(h) => h, + _ => todo!(), + }; let script = Builder::build_p2sh(&address); assert_eq!(script.script_type(), ScriptType::ScriptHash); assert_eq!( script.extract_destinations(), - Ok(vec![ScriptAddress::new_p2sh(address),]) + Ok(vec![ScriptAddress::new_p2sh(address)]) ); } @@ -967,6 +1023,63 @@ OP_ADD ); } + #[test] + fn test_extract_destinations_witness_v0_keyhash() { + // signet test data + let address = "tb1q002k02rkff3d0gmprwmyg7z95gncmn4hqdsvug" + .parse::
() + .unwrap() + .hash; + let address = match address { + AddressTypes::WitnessV0KeyHash(h) => h, + _ => todo!(), + }; + let script = Builder::build_p2wpkh(&address); + assert_eq!(script.script_type(), ScriptType::WitnessV0Keyhash); + assert_eq!( + script.extract_destinations(), + Ok(vec![ScriptAddress::new_p2wpkh(address),]) + ); + } + + #[test] + fn test_extract_destinations_witness_v0_scripthash() { + // signet test data + let address = "tb1qft5p2uhsdcdc3l2ua4ap5qqfg4pjaqlp250x7us7a8qqhrxrxfsqaqh7jw" + .parse::
() + .unwrap() + .hash; + let address = match address { + AddressTypes::WitnessV0ScriptHash(h) => h, + _ => todo!(), + }; + let script = Builder::build_p2wsh(&address); + assert_eq!(script.script_type(), ScriptType::WitnessV0Scripthash); + assert_eq!( + script.extract_destinations(), + Ok(vec![ScriptAddress::new_p2wsh(address),]) + ); + } + + #[test] + fn test_extract_destinations_witness_v1_taproot() { + // signet test data + let address = "tb1p22yc5qaf7p9ms0u2frae2vyfmcgwdmnsvk9st923a0muqz9stdaqn3h0p6" + .parse::
() + .unwrap() + .hash; + let address = match address { + AddressTypes::WitnessV1Taproot(h) => h, + _ => todo!(), + }; + let script = Builder::build_p2tr(&address); + assert_eq!(script.script_type(), ScriptType::WitnessV1Taproot); + assert_eq!( + script.extract_destinations(), + Ok(vec![ScriptAddress::new_p2tr(address),]) + ); + } + #[test] fn test_num_signatures_required() { let script = Builder::default() @@ -1026,7 +1139,7 @@ OP_ADD #[test] fn redeem_script() { let script: Script = REDEEM.parse().unwrap(); - assert_eq!(script.is_multisig_script(), true); + assert!(script.is_multisig_script()); } #[test] diff --git a/script/src/sign.rs b/script/src/sign.rs index 7a7bd47..5d5bb5c 100644 --- a/script/src/sign.rs +++ b/script/src/sign.rs @@ -4,19 +4,107 @@ use alloc::{vec, vec::Vec}; use light_bitcoin_chain::{OutPoint, Transaction, TransactionInput, TransactionOutput}; -use light_bitcoin_crypto::dhash256; -use light_bitcoin_keys::KeyPair; +use light_bitcoin_crypto::{dhash256, sha256, Digest}; +use light_bitcoin_keys::{verify_schnorr, HashAdd, KeyPair, SchnorrSignature, Tagged, XOnly}; use light_bitcoin_primitives::{Bytes, H256}; use light_bitcoin_serialization::Stream; -use crate::builder::Builder; use crate::script::Script; +use crate::{builder::Builder, Error}; + +use core::{ + cmp::Ordering, + convert::{TryFrom, TryInto}, +}; + +use crate::Opcode; +use secp256k1::{ + curve::{Affine, Jacobian, Scalar, ECMULT_CONTEXT}, + PublicKey, +}; #[derive(Debug, PartialEq, Clone, Copy)] pub enum SignatureVersion { Base, WitnessV0, ForkId, + Taproot, + TapScript, +} + +#[derive(Debug)] +pub struct ScriptExecutionData { + // Whether m_tapleaf_hash is initialized. + pub m_tapleaf_hash_init: bool, + // The tapleaf hash. + pub m_tapleaf_hash: H256, + + // Whether m_codeseparator_pos is initialized. + pub m_codeseparator_pos_init: bool, + // Opcode position of the last executed OP_CODESEPARATOR (or 0xFFFFFFFF if none executed). + pub m_codeseparator_pos: u32, + + // Whether m_annex_present and (when needed) m_annex_hash are initialized. + pub m_annex_init: bool, + // Whether an annex is present. + pub m_annex_present: bool, + // Hash of the annex data. + pub m_annex_hash: H256, + + // Whether m_validation_weight_left is initialized. + pub m_validation_weight_left_init: bool, + // How much validation weight is left (decremented for every successful non-empty signature check). + pub m_validation_weight_left: i64, +} + +impl Default for ScriptExecutionData { + fn default() -> Self { + ScriptExecutionData { + m_tapleaf_hash_init: false, + m_tapleaf_hash: Default::default(), + m_codeseparator_pos_init: true, + m_codeseparator_pos: 0xFFFFFFFF, + m_annex_init: false, + m_annex_present: false, + m_annex_hash: Default::default(), + m_validation_weight_left_init: false, + m_validation_weight_left: 0, + } + } +} + +pub fn compute_leaf_hash(version: u8, script: &Script) -> H256 { + let mut stream = Stream::default(); + stream.append(&version); + stream.append_list(&**script); + let out = stream.out(); + let hash = sha2::Sha256::default() + .tagged(b"TapLeaf") + .add(&out[..]) + .finalize(); + H256::from_slice(hash.as_slice()) +} + +impl ScriptExecutionData { + // Refer: https://github.com/bitcoin/bitcoin/blob/7fcf53f7b4524572d1d0c9a5fdc388e87eb02416/test/functional/test_framework/script.py#L745-L786 + pub fn with_script(&mut self, script: &Script) { + self.m_tapleaf_hash_init = true; + self.m_tapleaf_hash = compute_leaf_hash(0xc0, script); + } + + pub fn with_annex(&mut self, annex: &Script) { + let mut stream = Stream::default(); + stream.append_list(&**annex); + let out = stream.out(); + self.m_annex_init = true; + self.m_annex_present = true; + self.m_annex_hash = sha256(&out); + } + + pub fn with_codeseparator_pos(&mut self, pos: u32) { + self.m_codeseparator_pos_init = true; + self.m_codeseparator_pos = pos; + } } #[derive(Debug, PartialEq, Clone, Copy)] @@ -127,6 +215,7 @@ impl From for TransactionInputSigner { } impl TransactionInputSigner { + /// script_pubkey - script_pubkey of input's previous_output pubkey pub fn signature_hash( &self, input_index: usize, @@ -154,6 +243,7 @@ impl TransactionInputSigner { sighashtype, sighash, ), + _ => todo!(), } } @@ -328,6 +418,95 @@ impl TransactionInputSigner { sighash, ) } + + // Refer: https://github.com/bitcoin/bitcoin/blob/a93e7a442250d3522261d6e04d5660c5fecd2d8a/src/script/interpreter.cpp#L1503-L1587 + // and https://github.com/bitcoin/bitcoin/blob/7fcf53f7b4524572d1d0c9a5fdc388e87eb02416/test/functional/test_framework/script.py#L745-L786 + pub fn signature_hash_schnorr( + &self, + input_index: usize, + spent_outputs: &[TransactionOutput], + sigversion: SignatureVersion, + hash_type: u8, + execdata: &ScriptExecutionData, + ) -> H256 { + let key_version = 0u8; + let ext_flag = if sigversion == SignatureVersion::Taproot { + 0u8 + } else { + 1u8 + }; + + let mut stream = Stream::default(); + + // Epoch + stream.append(&0u8); + // Hash type + let output_type = if hash_type == 0 { 1u8 } else { hash_type & 3 }; + let input_type = hash_type & 128; + + stream.append(&hash_type); + // Transaction level data + stream.append(&self.version); + stream.append(&self.lock_time); + + let tx_to_prevouts = compute_schnorr_hash_prevouts(&self.inputs); + let tx_to_sequence = compute_schnorr_hash_sequence(&self.inputs); + let tx_to_outputs = compute_schnorr_hash_outputs(input_index, &self.outputs); + let spent_outputs_amounts = compute_schnorr_hash_amounts(spent_outputs); + let spent_outputs_scripts = compute_schnorr_hash_scripts(spent_outputs); + if input_type != 128 { + stream.append(&tx_to_prevouts); + stream.append(&spent_outputs_amounts); + stream.append(&spent_outputs_scripts); + stream.append(&tx_to_sequence); + } + if output_type == 1 { + stream.append(&tx_to_outputs); + } + + let have_annex = if execdata.m_annex_present { 1u8 } else { 0u8 }; + let spend_type = (ext_flag << 1) + have_annex; + stream.append(&spend_type); + // L1489-1495 + if input_type == 128 { + stream.append(&self.inputs[input_index].previous_output); + stream.append(&spent_outputs[input_index].value); + stream.append_list(&spent_outputs[input_index].script_pubkey); + stream.append(&self.inputs[input_index].sequence); + } else { + stream.append(&(input_index as u32)); + } + + if execdata.m_annex_present { + stream.append(&execdata.m_annex_hash); + } + + // Data about the output (if only one). + if output_type == 3 { + // input_index >= tx_to.vout.size() return false + let mut single_output = Stream::default(); + single_output.append(&self.outputs[input_index]); + let output: Vec = single_output.out().into(); + let hash = sha2::Sha256::default().add(&output[..]).finalize(); + stream.append_slice(hash.as_slice()); + } + // Additional data for BIP 342 signatures + if sigversion == SignatureVersion::TapScript { + if execdata.m_tapleaf_hash_init { + stream.append(&execdata.m_tapleaf_hash); + stream.append(&key_version); + } + if execdata.m_codeseparator_pos_init { + stream.append(&execdata.m_codeseparator_pos); + } + } + let out: Vec = stream.out().into(); + let hash = sha2::Sha256::default() + .tagged(b"TapSighash") + .add(&out[..]) + .finalize(); + H256::from_slice(hash.as_slice()) + } } fn compute_hash_prevouts(sighash: Sighash, inputs: &[UnsignedTransactionInput]) -> H256 { @@ -377,9 +556,238 @@ fn compute_hash_outputs( } } +fn compute_schnorr_hash_prevouts(inputs: &[UnsignedTransactionInput]) -> H256 { + let mut stream = Stream::default(); + for input in inputs { + stream.append(&input.previous_output); + } + sha256(&stream.out()) +} + +fn compute_schnorr_hash_sequence(inputs: &[UnsignedTransactionInput]) -> H256 { + let mut stream = Stream::default(); + for input in inputs { + stream.append(&input.sequence); + } + sha256(&stream.out()) +} + +fn compute_schnorr_hash_outputs(_input_index: usize, outputs: &[TransactionOutput]) -> H256 { + let mut stream = Stream::default(); + for output in outputs { + stream.append(output); + } + sha256(&stream.out()) +} + +fn compute_schnorr_hash_amounts(outputs: &[TransactionOutput]) -> H256 { + let mut stream = Stream::default(); + for output in outputs { + stream.append(&output.value); + } + sha256(&stream.out()) +} + +fn compute_schnorr_hash_scripts(outputs: &[TransactionOutput]) -> H256 { + let mut stream = Stream::default(); + for output in outputs { + stream.append(&output.script_pubkey); + } + sha256(&stream.out()) +} + +/// Checked, but no test +pub fn compute_taproot_merkle_root(control: Bytes, tapleaf_hash: H256) -> H256 { + // TAPROOT_CONTROL_BASE_SIZE == 33, TAPROOT_CONTROL_NODE_SIZE == 32 + let path_len = (control.len() - 33) / 32; + let mut k = tapleaf_hash; + for i in 0..path_len { + let mut branch = Stream::default(); + + let p = 33 + 32 * i; + let node = Bytes::from(&control[p..p + 32]); + + if node.cmp(&Bytes::from(k.as_bytes())) == Ordering::Less { + branch.append_slice(&Vec::from(node)); + branch.append(&k); + } else { + branch.append(&k); + branch.append_slice(&Vec::from(node)); + } + let out = branch.out(); + let hash = sha2::Sha256::default() + .tagged(b"TapBranch") + .add(&out[..]) + .finalize(); + k = H256::from_slice(hash.as_slice()); + } + k +} + +/// Verify Taproot Commitment +/// Refer: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#script-validation-rules +pub fn verify_taproot_commitment(control: &[u8], program: &XOnly, scirpt: &Script) -> bool { + if control.len() < 33 || (control.len() - 33) % 32 != 0 { + return false; + } + let pubkey: PublicKey = if let Ok(d) = XOnly::try_from(&control[1..33]) { + if let Ok(pk) = d.try_into() { + pk + } else { + return false; + } + } else { + return false; + }; + let tapleaf_hash = compute_leaf_hash(0xfe & control[0], scirpt); + let merkle_root = compute_taproot_merkle_root(Bytes::from(control), tapleaf_hash); + + let mut stream = Stream::default(); + stream.append_slice(&control[1..33]); + stream.append(&merkle_root); + let out = stream.out(); + let hash = sha2::Sha256::default() + .tagged(b"TapTweak") + .add(&out[..]) + .finalize(); + let hash = hash.as_slice(); + if hash.len() != 32 { + return false; + } + let mut keys = [0u8; 32]; + keys.copy_from_slice(hash); + let mut t = Scalar::default(); + if bool::from(t.set_b32(&keys)) { + return false; + }; + + let mut p: Affine = pubkey.into(); + + p.y.normalize(); + let p = if p.y.is_odd() { p.neg() } else { p }; + + let mut pj = secp256k1::curve::Jacobian::default(); + pj.set_ge(&p); + + let mut rj = Jacobian::default(); + // Q = P + int(t)G. + ECMULT_CONTEXT.ecmult(&mut rj, &pj, &Scalar::from_int(1), &t); + let mut r = Affine::from_gej(&rj); + let rx: XOnly = (&mut r.x).into(); + if rx != *program { + return false; + } + true +} + +/// Check Taproot tx +pub fn check_taproot_tx( + tx: &Transaction, + spent_outputs: &[TransactionOutput], +) -> Result { + if tx.inputs.len() != spent_outputs.len() { + return Err(Error::SpentOutputsNumDismatch); + } + + for i in 0..tx.inputs.len() { + let script_pubkey: Script = spent_outputs[i].script_pubkey.clone().into(); + if !script_pubkey.is_pay_to_witness_taproot() { + return Err(Error::NotTaprootWitness); + } + let tweak_pubkey = if let Some(r) = script_pubkey.parse_witness_program() { + XOnly::try_from(r.1).map_err(|_| Error::NotTaprootWitness)? + } else { + return Err(Error::NotTaprootWitness); + }; + + let mut execdata = ScriptExecutionData::default(); + let last_element: Bytes = if let Some(s) = tx.inputs[i].script_witness.last() { + s.clone() + } else { + return Err(Error::LastElementNotExist); + }; + let wit: Vec = + if tx.inputs[i].script_witness.len() >= 2 && last_element[0] == 0x50_u8 { + // TODO: fix annex not necessarily right + execdata.with_annex(&Script::new(last_element)); + tx.inputs[i].script_witness[..tx.inputs[i].script_witness.len() - 1].to_vec() + } else { + tx.inputs[i].script_witness.clone() + }; + if wit.is_empty() { + return Err(Error::EmptyWitness); + } + let signer: TransactionInputSigner = tx.clone().into(); + if wit.len() == 1 { + let wit_element: Vec = wit[0].clone().into(); + let hash_type = if wit_element.len() == 65 { + wit_element[64] + } else if wit_element.len() == 64 { + 0 + } else { + return Err(Error::WitnessProgramWrongLength); + }; + let signature = if let Ok(s) = SchnorrSignature::try_from(&wit_element[..]) { + s + } else { + return Err(Error::InvalidSignature); + }; + + let sighash = signer.signature_hash_schnorr( + i, + spent_outputs, + SignatureVersion::Taproot, + hash_type, + &execdata, + ); + if verify_schnorr(&signature, &sighash, tweak_pubkey).is_err() { + return Err(Error::CheckSigVerify); + }; + } else { + // Simplify verification, just verify the format of witness is : [signature, script, control] + let signature = + if let Ok(s) = SchnorrSignature::try_from(&Vec::from(wit[0].clone())[..]) { + s + } else { + return Err(Error::InvalidSignature); + }; + + if !(wit[wit.len() - 2].len() == 34 + && wit[wit.len() - 2][0] == Opcode::OP_PUSHBYTES_32 as u8 + && wit[wit.len() - 2][33] == Opcode::OP_CHECKSIG as u8) + { + return Err(Error::WitnessUnexpected); + } + let mut keys = [0u8; 32]; + keys.copy_from_slice(&wit[wit.len() - 2][1..33]); + let pk = XOnly(keys); + let st: Script = wit[wit.len() - 2].clone().into(); + execdata.with_script(&st); + let sighash = signer.signature_hash_schnorr( + i, + spent_outputs, + SignatureVersion::TapScript, + 0, + &execdata, + ); + if verify_schnorr(&signature, &sighash, pk).is_err() { + return Err(Error::CheckSigVerify); + }; + if !verify_taproot_commitment( + &Vec::from(wit[wit.len() - 1].clone()), + &tweak_pubkey, + &st, + ) { + return Err(Error::VerifyCommitment); + }; + }; + } + Ok(true) +} + #[cfg(test)] mod tests { - use light_bitcoin_keys::{Address, Private}; + use light_bitcoin_keys::{Address, AddressTypes, Private}; use light_bitcoin_primitives::{h256, h256_rev}; use super::*; @@ -410,7 +818,11 @@ mod tests { // this is irrelevant let kp = KeyPair::from_private(private).unwrap(); assert_eq!(kp.address(), from); - assert_eq!(¤t_output[3..23], to.hash.as_bytes()); + let address = match to.hash { + AddressTypes::Legacy(h) => h, + _ => todo!(), + }; + assert_eq!(¤t_output[3..23], address.as_bytes()); let unsigned_input = UnsignedTransactionInput { sequence: 0xffff_ffff, @@ -442,6 +854,71 @@ mod tests { assert_eq!(hash, expected_signature_hash); } + #[test] + fn test_schnorr_sighash() { + // script path + let tx_prev: Transaction = "020000000001014d954f5ab2fdda9070e51e9096dab6c67b6ba7b06eb53365335b55b0db893e20000000000000000000028096980000000000225120dc82a9c33d787242d80fb4535bcc8d90bb13843fea52c9e78bb43c541dd607b90000000000000000326a3035516a706f3772516e7751657479736167477a6334526a376f737758534c6d4d7141754332416255364c464646476a3801405c207eadfa01aa2402cd02a73f28fd5d85302c76bcfee45a2a460af9f5674ba7105ac7e13b858e54de8135f13c5166607c189ba50caa74809460a66a5d279c2d00000000".parse().unwrap(); + let tx: Transaction = "020000000001014b8336c563851c3073fcf4aa454b2d3b35947020d16f66f18036fb1a7b2aa3ca00000000000000000001404b4c0000000000225120c9929543dfa1e0bb84891acd47bfa6546b05e26b7a04af8eb6765fcc969d565f03407f444bee6fecafc8cdc46536e790c0d6df31bb533a7613b1b5b0e515eab292fe7e60cf3a9f1976d313cbbd49ea00580eef594f3f3e9d7b436bd436741fc068cc222083f579dd2380bd31355d066086e1b4d46b518987c1f8a64d4c0101560280eae2ac61c1032513ab37143495fcf3bc088de5cdecb94f1c3d7c313075f14a9e35230bb824142b1d0be52980a4791a097a4b7a7df52a97dfe0b1b5023b97c0937a9ab884db9c0bc456a7da1e3879b4e78139e31603c6b6305dc8248e2ede5b4a5b5d45e3dd00000000".parse().unwrap(); + + let signer: TransactionInputSigner = tx.clone().into(); + let input_index = 0; + let script: Script = tx.inputs[input_index].script_witness + [tx.inputs[input_index].script_witness.len() - 2] + .clone() + .into(); + let mut execdata = ScriptExecutionData::default(); + execdata.with_script(&script); + + let sighash = signer.signature_hash_schnorr( + input_index, + &[tx_prev.outputs[0].clone()], + SignatureVersion::TapScript, + 0, + &execdata, + ); + assert_eq!( + hex::encode(sighash.as_bytes()), + "e365c3949a260ae19e87ea13ac30bb12fa379d503cbd8c6382978acb7d40c999" + ); + // key path + let tx_prev: Transaction = "020000000001014be640313b023c3c731b7e89c3f97bebcebf9772ea2f7747e5604f4483a447b601000000000000000002a0860100000000002251209a9ea267884f5549c206b2aec2bd56d98730f90532ea7f7154d4d4f923b7e3bbc027090000000000225120c9929543dfa1e0bb84891acd47bfa6546b05e26b7a04af8eb6765fcc969d565f01404dc68b31efc1468f84db7e9716a84c19bbc53c2d252fd1d72fa6469e860a74486b0990332b69718dbcb5acad9d48634d23ee9c215ab15fb16f4732bed1770fdf00000000".parse().unwrap(); + let tx: Transaction = "02000000000101aeee49e0bbf7a36f78ea4321b5c8bae0b8c72bdf2c024d2484b137fa7d0f8e1f01000000000000000003a0860100000000002251209a9ea267884f5549c206b2aec2bd56d98730f90532ea7f7154d4d4f923b7e3bb0000000000000000326a3035516a706f3772516e7751657479736167477a6334526a376f737758534c6d4d7141754332416255364c464646476a38801a060000000000225120c9929543dfa1e0bb84891acd47bfa6546b05e26b7a04af8eb6765fcc969d565f01409e325889515ed47099fdd7098e6fafdc880b21456d3f368457de923f4229286e34cef68816348a0581ae5885ede248a35ac4b09da61a7b9b90f34c200872d2e300000000".parse().unwrap(); + + let signer: TransactionInputSigner = tx.into(); + let input_index = 0; + let execdata = ScriptExecutionData::default(); + + let sighash = signer.signature_hash_schnorr( + input_index, + &[tx_prev.outputs[1].clone()], + SignatureVersion::Taproot, + 0, + &execdata, + ); + assert_eq!( + hex::encode(sighash.as_bytes()), + "96b37172b00a418004e6632c6d73a0b867ab6986b1fc470ff1432840a361edd3" + ); + } + + #[test] + fn test_check_taproot_tx() { + // script path + let tx_prev: Transaction = "020000000001014d954f5ab2fdda9070e51e9096dab6c67b6ba7b06eb53365335b55b0db893e20000000000000000000028096980000000000225120dc82a9c33d787242d80fb4535bcc8d90bb13843fea52c9e78bb43c541dd607b90000000000000000326a3035516a706f3772516e7751657479736167477a6334526a376f737758534c6d4d7141754332416255364c464646476a3801405c207eadfa01aa2402cd02a73f28fd5d85302c76bcfee45a2a460af9f5674ba7105ac7e13b858e54de8135f13c5166607c189ba50caa74809460a66a5d279c2d00000000".parse().unwrap(); + let tx: Transaction = "020000000001014b8336c563851c3073fcf4aa454b2d3b35947020d16f66f18036fb1a7b2aa3ca00000000000000000001404b4c0000000000225120c9929543dfa1e0bb84891acd47bfa6546b05e26b7a04af8eb6765fcc969d565f03407f444bee6fecafc8cdc46536e790c0d6df31bb533a7613b1b5b0e515eab292fe7e60cf3a9f1976d313cbbd49ea00580eef594f3f3e9d7b436bd436741fc068cc222083f579dd2380bd31355d066086e1b4d46b518987c1f8a64d4c0101560280eae2ac61c1032513ab37143495fcf3bc088de5cdecb94f1c3d7c313075f14a9e35230bb824142b1d0be52980a4791a097a4b7a7df52a97dfe0b1b5023b97c0937a9ab884db9c0bc456a7da1e3879b4e78139e31603c6b6305dc8248e2ede5b4a5b5d45e3dd00000000".parse().unwrap(); + assert_eq!( + check_taproot_tx(&tx, &[tx_prev.outputs[0].clone()]), + Ok(true) + ); + // key path + let tx_prev: Transaction = "020000000001011def6321e4ce48ad7ae210a97714bece4026eee96f304c608421f446061f9e56000000000000000000028096980000000000225120dc82a9c33d787242d80fb4535bcc8d90bb13843fea52c9e78bb43c541dd607b900b4c40400000000225120c9929543dfa1e0bb84891acd47bfa6546b05e26b7a04af8eb6765fcc969d565f01408d324ff1f3015e37017089df5139e235459f9c9d5e068ff35eca4aa22f89e244ac289db0e7c2df81d35f2f10af8c4b1572cf6fa8f6ce8ae2bdf4ab49c02202b300000000".parse().unwrap(); + let tx: Transaction = "0200000000010136e0e1434cacdfa67192148a1b2f1839ada76f183da6476857f8d719d4c805b200000000000000000002404b4c0000000000225120c9929543dfa1e0bb84891acd47bfa6546b05e26b7a04af8eb6765fcc969d565f00093d0000000000225120dc82a9c33d787242d80fb4535bcc8d90bb13843fea52c9e78bb43c541dd607b90140910e001c23acd1f1426fc918c7de877da84f697b1d4f2af5793374b9489459634b9f73bf911b391455d5c8a2d12bf19268f7af4db4e774877de42bc151aa5a0000000000".parse().unwrap(); + assert_eq!( + check_taproot_tx(&tx, &[tx_prev.outputs[0].clone()]), + Ok(true) + ); + } + fn run_test_sighash(tx: &str, script: &str, input_index: usize, hash_type: i32, result: &str) { let tx: Transaction = tx.parse().unwrap(); let signer: TransactionInputSigner = tx.into(); diff --git a/serialization/src/stream.rs b/serialization/src/stream.rs index 4ebb283..5b1e3f3 100644 --- a/serialization/src/stream.rs +++ b/serialization/src/stream.rs @@ -88,7 +88,7 @@ pub trait Serializable { } /// Stream used for serialization of Bitcoin structures -#[derive(Default)] +#[derive(Default, Clone)] pub struct Stream { buffer: Vec, flags: u32, diff --git a/src/lib.rs b/src/lib.rs index 1a6c048..b869cb9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ pub use light_bitcoin_chain as chain; pub use light_bitcoin_crypto as crypto; pub use light_bitcoin_keys as keys; +pub use light_bitcoin_mast as mast; pub use light_bitcoin_merkle as merkle; pub use light_bitcoin_primitives as primitives; pub use light_bitcoin_script as script;