diff --git a/Cargo.lock b/Cargo.lock index bc3fda945c..6a52f76be3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3434,6 +3434,7 @@ name = "keys" version = "0.1.0" dependencies = [ "bech32", + "bitcoin", "bitcrypto", "bs58 0.4.0", "derive_more", @@ -6494,6 +6495,7 @@ checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" name = "script" version = "0.1.0" dependencies = [ + "bitcoin", "bitcrypto", "blake2b_simd", "chain", @@ -9031,6 +9033,7 @@ name = "utxo_signer" version = "0.1.0" dependencies = [ "async-trait", + "bitcoin", "chain", "common", "crypto", @@ -9039,6 +9042,7 @@ dependencies = [ "keys", "mm2_err_handle", "primitives", + "rand 0.7.3", "rpc", "script", "serialization", diff --git a/mm2src/coins/lightning/ln_events.rs b/mm2src/coins/lightning/ln_events.rs index cc6045f9ed..f5dcfefda6 100644 --- a/mm2src/coins/lightning/ln_events.rs +++ b/mm2src/coins/lightning/ln_events.rs @@ -215,7 +215,7 @@ async fn sign_funding_transaction( let signed = sign_tx( unsigned, key_pair, - SignatureVersion::WitnessV0, + SignatureVersion::Witness, coin.as_ref().conf.fork_id, ) .map_err(|e| SignFundingTransactionError::TxSignFailed(e.to_string()))?; @@ -520,7 +520,7 @@ impl LightningEventHandler { return; }, }; - let change_destination_script = match Builder::build_p2wpkh(my_address.hash()) { + let change_destination_script = match Builder::build_p2wpkh(my_address.locking_destination()) { Ok(script) => script.to_bytes().take().into(), Err(err) => { error!( diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index 851ea65780..c57130d9dd 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -81,7 +81,7 @@ fn test_withdraw_to_p2sh_address_should_fail() { ) .as_sh( block_on(coin.as_ref().derivation_method.unwrap_single_addr()) - .hash() + .locking_destination() .clone(), ) .build() diff --git a/mm2src/coins/rpc_command/get_new_address.rs b/mm2src/coins/rpc_command/get_new_address.rs index cabbed3a43..e47c60b522 100644 --- a/mm2src/coins/rpc_command/get_new_address.rs +++ b/mm2src/coins/rpc_command/get_new_address.rs @@ -348,7 +348,13 @@ impl RpcTask for InitGetNewAddressTask { AddressFormat::Standard | AddressFormat::CashAddress { .. } => { Some(TrezorInputScriptType::SpendAddress) }, - AddressFormat::Segwit => Some(TrezorInputScriptType::SpendWitness), + AddressFormat::Segwit { version: 0 } => Some(TrezorInputScriptType::SpendWitness), + AddressFormat::Segwit { version: 1 } => Some(TrezorInputScriptType::SpendTaproot), + AddressFormat::Segwit { version: v } => { + return Err(GetNewAddressRpcError::ErrorDerivingAddress(format!( + "Segwit v{v} addresses are not supported for UTXO" + )))? + }, }; Ok(GetNewAddressResponseEnum::Map( get_new_address_helper( @@ -368,7 +374,12 @@ impl RpcTask for InitGetNewAddressTask { AddressFormat::Standard | AddressFormat::CashAddress { .. } => { Some(TrezorInputScriptType::SpendAddress) }, - AddressFormat::Segwit => Some(TrezorInputScriptType::SpendWitness), + AddressFormat::Segwit { version: 0 } => Some(TrezorInputScriptType::SpendWitness), + AddressFormat::Segwit { version: v } => { + return Err(GetNewAddressRpcError::ErrorDerivingAddress(format!( + "Segwit v{v} addresses are not supported for Qtum" + )))? + }, }; Ok(GetNewAddressResponseEnum::Map( get_new_address_helper( diff --git a/mm2src/coins/rpc_command/lightning/open_channel.rs b/mm2src/coins/rpc_command/lightning/open_channel.rs index 8f4d25f378..ba066a9c03 100644 --- a/mm2src/coins/rpc_command/lightning/open_channel.rs +++ b/mm2src/coins/rpc_command/lightning/open_channel.rs @@ -15,7 +15,7 @@ use common::{async_blocking, new_uuid, HttpStatusCode}; use db_common::sqlite::rusqlite::Error as SqlError; use derive_more::Display; use http::StatusCode; -use keys::AddressHashEnum; +use keys::LockingDestination; use lightning::util::config::UserConfig; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; @@ -185,7 +185,7 @@ pub async fn open_channel(ctx: MmArc, req: OpenChannelRequest) -> OpenChannelRes // The actual script_pubkey will replace this before signing the transaction after receiving the required // output script from the other node when the channel is accepted - let script_pubkey = match Builder::build_p2wsh(&AddressHashEnum::WitnessScriptHash(Default::default())) { + let script_pubkey = match Builder::build_p2wsh(&LockingDestination::WitnessScriptHash(Default::default())) { Ok(script) => script.to_bytes(), Err(err) => return MmError::err(OpenChannelError::InternalError(err.to_string())), }; diff --git a/mm2src/coins/rpc_command/offline_keys.rs b/mm2src/coins/rpc_command/offline_keys.rs index 572faa9f7f..f059f5784a 100644 --- a/mm2src/coins/rpc_command/offline_keys.rs +++ b/mm2src/coins/rpc_command/offline_keys.rs @@ -433,7 +433,7 @@ async fn offline_hd_keys_export_internal( let address = AddressBuilder::new(address_format, ChecksumType::DSHA256, address_prefixes, bech32_hrp) - .as_pkh_from_pk(*key_pair.public()) + .using_pk(*key_pair.public()) .build() .map_err(OfflineKeysError::Internal)?; @@ -559,7 +559,7 @@ async fn offline_iguana_keys_export_internal( let address = AddressBuilder::new(AddressFormat::Standard, ChecksumType::DSHA256, address_prefixes, None) - .as_pkh_from_pk(*key_pair.public()) + .using_pk(*key_pair.public()) .build() .map_err(OfflineKeysError::Internal)?; @@ -1013,7 +1013,7 @@ mod tests { let address = AddressBuilder::new(AddressFormat::Standard, ChecksumType::DSHA256, address_prefixes, None) - .as_pkh_from_pk(*kp.public()) + .using_pk(*kp.public()) .build() .unwrap(); diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index eaee55c44b..3b14676e1c 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -65,8 +65,8 @@ use keys::bytes::Bytes; use keys::NetworkAddressPrefixes; use keys::Signature; pub use keys::{ - Address, AddressBuilder, AddressFormat as UtxoAddressFormat, AddressHashEnum, AddressPrefix, AddressScriptType, - KeyPair, LegacyAddress, Private, Public, Secret, + Address, AddressBuilder, AddressFormat as UtxoAddressFormat, AddressPrefix, AddressScriptType, KeyPair, + LegacyAddress, LockingDestination, Private, Public, Secret, }; #[cfg(not(target_arch = "wasm32"))] use lightning_invoice::Currency as LightningCurrency; @@ -1880,7 +1880,6 @@ async fn generate_tx( where T: AsRef + UtxoTxGenerationOps + UtxoTxBroadcastOps, { - let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await); let key_pair = try_tx_s!(coin.as_ref().priv_key_policy.activated_key_or_err()); let mut builder = UtxoTxBuilder::new(coin) .await @@ -1903,15 +1902,10 @@ where }) .collect(); - let signature_version = match my_address.addr_format() { - UtxoAddressFormat::Segwit => SignatureVersion::WitnessV0, - _ => coin.as_ref().conf.signature_version, - }; - let signed = try_tx_s!(sign_tx( unsigned, key_pair, - signature_version, + coin.as_ref().conf.signature_version, coin.as_ref().conf.fork_id )); @@ -1921,10 +1915,11 @@ where /// Builds transaction output script for an Address struct pub fn output_script(address: &Address) -> Result { match address.script_type() { - AddressScriptType::P2PKH => Ok(Builder::build_p2pkh(address.hash())), - AddressScriptType::P2SH => Ok(Builder::build_p2sh(address.hash())), - AddressScriptType::P2WPKH => Builder::build_p2wpkh(address.hash()), - AddressScriptType::P2WSH => Builder::build_p2wsh(address.hash()), + AddressScriptType::P2PKH => Ok(Builder::build_p2pkh(address.locking_destination())), + AddressScriptType::P2SH => Ok(Builder::build_p2sh(address.locking_destination())), + AddressScriptType::P2WPKH => Builder::build_p2wpkh(address.locking_destination()), + AddressScriptType::P2WSH => Builder::build_p2wsh(address.locking_destination()), + AddressScriptType::P2TR => Builder::build_p2tr(address.locking_destination()), } } @@ -1965,7 +1960,7 @@ pub fn address_by_conf_and_pubkey_str( utxo_conf.address_prefixes, utxo_conf.bech32_hrp, ) - .as_pkh_from_pk(pubkey) + .using_pk(pubkey) .build()?; address.display_address() } diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index 01d5d3394b..3c2d129afe 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -45,7 +45,7 @@ use bitcrypto::sign_message_hash; use common::executor::{AbortableSystem, AbortedError}; use ethereum_types::H160; use futures::{FutureExt, TryFutureExt}; -use keys::AddressHashEnum; +use keys::LockingDestination; use mm2_metrics::MetricsArc; use mm2_number::MmNumber; use rpc::v1::types::H264 as H264Json; @@ -105,7 +105,7 @@ pub trait QtumDelegationOps { fn remove_delegation(&self) -> DelegationFut; #[allow(clippy::result_large_err)] - fn generate_pod(&self, addr_hash: AddressHashEnum) -> Result>; + fn generate_pod(&self, addr_hash: LockingDestination) -> Result>; } #[async_trait] @@ -162,7 +162,7 @@ pub trait QtumBasedCoin: UtxoCommonOps + MarketCoinOps { utxo.conf.address_prefixes.clone(), utxo.conf.bech32_hrp.clone(), ) - .as_pkh(AddressHashEnum::AddressHash(address.0.into())) + .as_pkh(LockingDestination::AddressHash(address.0.into())) .build() .expect("valid address props") } @@ -1355,11 +1355,14 @@ pub fn contract_addr_from_str(addr: &str) -> Result { } pub fn contract_addr_from_utxo_addr(address: Address) -> MmResult { - match address.hash() { - AddressHashEnum::AddressHash(h) => Ok(h.take().into()), - AddressHashEnum::WitnessScriptHash(_) => MmError::err(ScriptHashTypeNotSupported { + match address.locking_destination() { + LockingDestination::AddressHash(h) => Ok(h.take().into()), + LockingDestination::WitnessScriptHash(_) => MmError::err(ScriptHashTypeNotSupported { script_hash_type: "Witness".to_owned(), }), + LockingDestination::TweakedXOnlyPubkey(_) => MmError::err(ScriptHashTypeNotSupported { + script_hash_type: "Taproot Script".to_owned(), + }), } } diff --git a/mm2src/coins/utxo/qtum_delegation.rs b/mm2src/coins/utxo/qtum_delegation.rs index f521f80c3e..f2b25617e1 100644 --- a/mm2src/coins/utxo/qtum_delegation.rs +++ b/mm2src/coins/utxo/qtum_delegation.rs @@ -20,7 +20,7 @@ use ethabi::{Contract, Token}; use ethereum_types::H160; use futures::compat::Future01CompatExt; use futures::{FutureExt, TryFutureExt}; -use keys::{AddressHashEnum, Signature}; +use keys::{LockingDestination, Signature}; use mm2_err_handle::prelude::*; use mm2_number::bigdecimal::{BigDecimal, Zero}; use rpc::v1::types::ToTxHash; @@ -115,7 +115,7 @@ impl QtumDelegationOps for QtumCoin { Box::new(fut.boxed().compat()) } - fn generate_pod(&self, addr_hash: AddressHashEnum) -> Result> { + fn generate_pod(&self, addr_hash: LockingDestination) -> Result> { let mut buffer = b"\x15Qtum Signed Message:\n\x28".to_vec(); buffer.append(&mut addr_hash.to_string().into_bytes()); let hashed = dhash256(&buffer); @@ -131,7 +131,7 @@ impl QtumDelegationOps for QtumCoin { impl QtumCoin { async fn remove_delegation_impl(&self) -> DelegationResult { - if self.addr_format().is_segwit() { + if self.addr_format().is_segwit_v0() { return MmError::err(DelegationError::DelegationOpsNotSupported { reason: "Qtum doesn't support delegation for segwit".to_string(), }); @@ -236,7 +236,7 @@ impl QtumCoin { amount, staker, am_i_staking, - is_staking_supported: !my_address.addr_format().is_segwit(), + is_staking_supported: !my_address.addr_format().is_segwit_v0(), } .into(), }; @@ -244,7 +244,7 @@ impl QtumCoin { } async fn add_delegation_impl(&self, request: QtumDelegationRequest) -> DelegationResult { - if self.addr_format().is_segwit() { + if self.addr_format().is_segwit_v0() { return MmError::err(DelegationError::DelegationOpsNotSupported { reason: "Qtum doesn't support delegation for segwit".to_string(), }); @@ -260,7 +260,7 @@ impl QtumCoin { let staker_address_hex = qtum::contract_addr_from_utxo_addr(to_addr.clone()).map_mm_err()?; let delegation_output = self.add_delegation_output( staker_address_hex, - to_addr.hash().clone(), + to_addr.locking_destination().clone(), fee, QRC20_GAS_LIMIT_DELEGATION, QRC20_GAS_PRICE_DEFAULT, @@ -377,7 +377,7 @@ impl QtumCoin { fn add_delegation_output( &self, to_addr: H160, - addr_hash: AddressHashEnum, + addr_hash: LockingDestination, fee: u64, gas_limit: u64, gas_price: u64, diff --git a/mm2src/coins/utxo/rpc_clients/electrum_rpc/client.rs b/mm2src/coins/utxo/rpc_clients/electrum_rpc/client.rs index 197e8ff426..20ad677f13 100644 --- a/mm2src/coins/utxo/rpc_clients/electrum_rpc/client.rs +++ b/mm2src/coins/utxo/rpc_clients/electrum_rpc/client.rs @@ -863,7 +863,7 @@ impl UtxoRpcClientOps for ElectrumClient { if let Some(pubkey) = address.pubkey() { // We don't want to show P2PK outputs along with segwit ones (P2WPKH). // Allow listing the P2PK outputs only if the address is not segwit (i.e. show P2PK outputs along with P2PKH). - if !address.addr_format().is_segwit() { + if !address.addr_format().is_segwit_v0() { let p2pk_output_script = output_script_p2pk(pubkey); output_scripts.push(p2pk_output_script); } @@ -997,7 +997,7 @@ impl UtxoRpcClientOps for ElectrumClient { // If the plain pubkey is available, fetch the balance found in P2PK output as well (if any). if let Some(pubkey) = address.pubkey() { // Show the balance in P2PK outputs only for the non-segwit legacy addresses (P2PKH). - if !address.addr_format().is_segwit() { + if !address.addr_format().is_segwit_v0() { let p2pk_output_script = output_script_p2pk(pubkey); hashes.push(hex::encode(electrum_script_hash(&p2pk_output_script))); } diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index 836efd55fc..3a8249e50a 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -43,7 +43,7 @@ use futures01::Future; use hex::FromHexError; use keys::hash::H160; use keys::{ - AddressHashEnum, CashAddrType, CashAddress, CompactSignature, KeyPair, NetworkPrefix as CashAddrPrefix, Public, + CashAddrType, CashAddress, CompactSignature, KeyPair, LockingDestination, NetworkPrefix as CashAddrPrefix, Public, }; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; @@ -1624,7 +1624,7 @@ impl MmCoin for SlpToken { // TODO clarify with community whether we should support withdrawal to SLP P2SH addresses let script_pubkey = match address.address_type { CashAddrType::P2PKH => { - ScriptBuilder::build_p2pkh(&AddressHashEnum::AddressHash(address_hash)).to_bytes() + ScriptBuilder::build_p2pkh(&LockingDestination::AddressHash(address_hash)).to_bytes() }, CashAddrType::P2SH => { return MmError::err(WithdrawError::InvalidAddress( diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index 356caeff9d..a67c59391b 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -202,7 +202,7 @@ where conf.address_prefixes.clone(), conf.bech32_hrp.clone(), ) - .as_pkh_from_pk(*key_pair.public()) + .using_pk(*key_pair.public()) .build() .map_to_mm(UtxoCoinBuildError::Internal)?; @@ -331,7 +331,7 @@ where conf.address_prefixes.clone(), conf.bech32_hrp.clone(), ) - .as_pkh_from_pk(Public::Compressed(pubkey.serialize().into())) + .using_pk(Public::Compressed(pubkey.serialize().into())) .build() .map_to_mm(UtxoCoinBuildError::Internal)?; let derivation_method = DerivationMethod::SingleAddress(my_address.clone()); @@ -370,15 +370,17 @@ where } }; let addr_format = builder.address_format()?; + println!("Address format: {:?}", addr_format); let my_address = AddressBuilder::new( addr_format, conf.checksum_type, conf.address_prefixes.clone(), conf.bech32_hrp.clone(), ) - .as_pkh_from_pk(pubkey) + .using_pk(pubkey) .build() .map_to_mm(UtxoCoinBuildError::Internal)?; + println!("My address: {:?}", my_address); let my_script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; @@ -534,7 +536,7 @@ pub trait UtxoCoinBuilderCommonOps { let mut address_format = match format_from_req { Some(from_req) => { - if from_req.is_segwit() != format_from_conf.is_segwit() { + if from_req.is_legacy_or_cashaddr() != format_from_conf.is_legacy_or_cashaddr() { let error = format!( "Both conf {format_from_conf:?} and request {from_req:?} must be either Segwit or Standard/CashAddress" ); diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 3bd6dadb7c..36c0bf7b6a 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -50,8 +50,8 @@ use keys::bytes::Bytes; #[cfg(test)] use keys::prefixes::{KMD_PREFIXES, T_QTUM_PREFIXES}; use keys::{ - Address, AddressBuilder, AddressBuilderOption, AddressFormat as UtxoAddressFormat, AddressFormat, AddressHashEnum, - AddressScriptType, CompactSignature, Public, SegwitAddress, + Address, AddressBuilder, AddressBuilderOption, AddressFormat as UtxoAddressFormat, AddressFormat, + AddressScriptType, CompactSignature, LockingDestination, Public, SegwitAddress, }; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; @@ -335,14 +335,24 @@ pub fn addresses_from_script(coin: &T, script: &Script) -> Res let (addr_format, build_option) = match dst.kind { AddressScriptType::P2PKH => ( coin.addr_format_for_standard_scripts(), - AddressBuilderOption::PubkeyHash(dst.hash), + AddressBuilderOption::PubkeyHash(dst.destination), ), AddressScriptType::P2SH => ( coin.addr_format_for_standard_scripts(), - AddressBuilderOption::ScriptHash(dst.hash), + AddressBuilderOption::ScriptHash(dst.destination), + ), + AddressScriptType::P2WPKH => ( + UtxoAddressFormat::Segwit { version: 0 }, + AddressBuilderOption::PubkeyHash(dst.destination), + ), + AddressScriptType::P2WSH => ( + UtxoAddressFormat::Segwit { version: 0 }, + AddressBuilderOption::ScriptHash(dst.destination), + ), + AddressScriptType::P2TR => ( + UtxoAddressFormat::Segwit { version: 1 }, + AddressBuilderOption::TweakedXOnlyPubkey(dst.destination), ), - AddressScriptType::P2WPKH => (UtxoAddressFormat::Segwit, AddressBuilderOption::PubkeyHash(dst.hash)), - AddressScriptType::P2WSH => (UtxoAddressFormat::Segwit, AddressBuilderOption::ScriptHash(dst.hash)), }; AddressBuilder::new( @@ -441,7 +451,7 @@ pub fn tx_size_in_v_bytes(from_addr_format: &UtxoAddressFormat, tx: &UtxoTx) -> // Virtual size of the transaction // https://bitcoin.stackexchange.com/questions/87275/how-to-calculate-segwit-transaction-fee-in-bytes/87276#87276 match from_addr_format { - UtxoAddressFormat::Segwit => { + UtxoAddressFormat::Segwit { .. } => { let base_size = transaction_bytes.len(); // 4 additional bytes (2 for the marker and 2 for the flag) and 1 additional byte for every input in the witness for the SIGHASH flag let total_size = transaction_bytes.len() + 4 + tx.inputs().len() * (additional_len + 1); @@ -459,7 +469,7 @@ pub fn output_script_checked(coin: &UtxoCoinFields, addr: &Address) -> MmResult< return MmError::err(UnsupportedAddr::PrefixError(coin.conf.ticker.clone())); } }, - UtxoAddressFormat::Segwit => match (coin.conf.bech32_hrp.as_ref(), addr.hrp().as_ref()) { + UtxoAddressFormat::Segwit { .. } => match (coin.conf.bech32_hrp.as_ref(), addr.hrp().as_ref()) { (Some(conf_hrp), Some(addr_hrp)) => { if conf_hrp != addr_hrp { return MmError::err(UnsupportedAddr::HrpError { @@ -1082,7 +1092,8 @@ async fn gen_taker_funding_spend_preimage( let payment_output = TransactionOutput { value: funding_amount - fee, - script_pubkey: Builder::build_p2sh(&AddressHashEnum::AddressHash(dhash160(&payment_redeem_script))).to_bytes(), + script_pubkey: Builder::build_p2sh(&LockingDestination::AddressHash(dhash160(&payment_redeem_script))) + .to_bytes(), }; p2sh_spending_tx_preimage( @@ -1606,7 +1617,7 @@ where let mut outputs = vec![TransactionOutput { value: fee_amount, - script_pubkey: Builder::build_p2pkh(dex_address.hash()).to_bytes(), + script_pubkey: Builder::build_p2pkh(dex_address.locking_destination()).to_bytes(), }]; if let DexFee::WithBurn { @@ -1624,7 +1635,7 @@ where }, DexFeeBurnDestination::PreBurnAccount => TransactionOutput { value: burn_amount_u64, - script_pubkey: Builder::build_p2pkh(burn_address.hash()).to_bytes(), + script_pubkey: Builder::build_p2pkh(burn_address.locking_destination()).to_bytes(), }, }; outputs.push(burn_output); @@ -2407,7 +2418,7 @@ pub fn watcher_validate_taker_fee( let dex_address = dex_address(&coin).map_to_mm(ValidatePaymentError::TxDeserializationError)?; match taker_fee_tx.outputs.get(output_index) { Some(out) => { - let expected_script_pubkey = Builder::build_p2pkh(dex_address.hash()).to_bytes(); + let expected_script_pubkey = Builder::build_p2pkh(dex_address.locking_destination()).to_bytes(); if out.script_pubkey != expected_script_pubkey { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "{}: Provided dex fee tx output script_pubkey doesn't match expected {:?} {:?}", @@ -2439,7 +2450,7 @@ fn validate_dex_output( let fee_amount_u64 = sat_from_big_decimal(&fee_amount.to_decimal(), coin.as_ref().decimals).map_mm_err()?; match tx.outputs.get(output_index) { Some(out) => { - let expected_script_pubkey = Builder::build_p2pkh(dex_address.hash()).to_bytes(); + let expected_script_pubkey = Builder::build_p2pkh(dex_address.locking_destination()).to_bytes(); if out.script_pubkey != expected_script_pubkey { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "{}: Provided dex fee tx output script_pubkey doesn't match expected {:?} {:?}", @@ -2561,7 +2572,7 @@ pub fn validate_fee( validate_burn_output(&coin, &tx, output_index + 1, &burn_script_pubkey, &burn_amount)?; }, DexFeeBurnDestination::PreBurnAccount => { - let burn_script_pubkey = Builder::build_p2pkh(burn_address.hash()); + let burn_script_pubkey = Builder::build_p2pkh(burn_address.locking_destination()); validate_dex_output(&coin, &tx, output_index, &dex_address, &fee_amount)?; validate_burn_output(&coin, &tx, output_index + 1, &burn_script_pubkey, &burn_amount)?; }, @@ -3082,7 +3093,7 @@ pub fn verify_message( .map_to_mm(|err| VerificationError::SignatureDecodingError(err.to_string()))?; let recovered_pubkey = Public::recover_compact(&H256::from(message_hash), &signature)?; let received_address = checked_address_from_str(coin, address).map_mm_err()?; - Ok(AddressHashEnum::from(recovered_pubkey.address_hash()) == *received_address.hash()) + Ok(LockingDestination::from(recovered_pubkey.address_hash()) == *received_address.locking_destination()) } pub fn my_balance(coin: T) -> BalanceFut @@ -3559,16 +3570,21 @@ pub fn convert_to_address(coin: &T, from: &str, to_address_for UtxoAddressFormat::Standard => { // assuming convertion to p2pkh Ok(LegacyAddress::new( - from_address.hash(), + from_address.locking_destination(), coin.as_ref().conf.address_prefixes.p2pkh.clone(), coin.as_ref().conf.checksum_type, ) .to_string()) }, - UtxoAddressFormat::Segwit => { + UtxoAddressFormat::Segwit { version } => { let bech32_hrp = &coin.as_ref().conf.bech32_hrp; match bech32_hrp { - Some(hrp) => Ok(SegwitAddress::new(from_address.hash(), hrp.clone()).to_string()), + Some(hrp) => Ok(try_s!(SegwitAddress::new( + from_address.locking_destination(), + hrp.clone(), + version + )) + .to_string()), None => ERR!("Cannot convert to a segwit address for a coin with no bech32_hrp in config"), } }, @@ -4525,7 +4541,7 @@ pub fn coin_protocol_info(coin: &T) -> Vec { pub fn is_coin_protocol_supported(coin: &T, info: &Option>) -> bool { match info { Some(format) => rmp_serde::from_slice::(format).is_ok(), - None => !coin.addr_format().is_segwit(), + None => !coin.addr_format().is_segwit_v0(), } } @@ -4754,7 +4770,7 @@ pub fn address_from_raw_pubkey( addr_format: UtxoAddressFormat, ) -> Result { AddressBuilder::new(addr_format, checksum_type, prefixes, hrp) - .as_pkh_from_pk(try_s!(Public::from_slice(pub_key))) + .using_pk(try_s!(Public::from_slice(pub_key))) .build() } @@ -4766,7 +4782,7 @@ pub fn address_from_pubkey( addr_format: UtxoAddressFormat, ) -> Address { AddressBuilder::new(addr_format, checksum_type, prefixes, hrp) - .as_pkh_from_pk(*pubkey) + .using_pk(*pubkey) .build() .expect("valid address props") } @@ -5195,7 +5211,7 @@ pub fn addr_format(coin: &dyn AsRef) -> &UtxoAddressFormat { pub fn addr_format_for_standard_scripts(coin: &dyn AsRef) -> UtxoAddressFormat { match &coin.as_ref().conf.default_address_format { - UtxoAddressFormat::Segwit => UtxoAddressFormat::Standard, + UtxoAddressFormat::Segwit { .. } => UtxoAddressFormat::Standard, format @ (UtxoAddressFormat::Standard | UtxoAddressFormat::CashAddress { .. }) => format.clone(), } } @@ -5218,7 +5234,7 @@ where Ok(()) } }, - UtxoAddressFormat::Segwit => { + UtxoAddressFormat::Segwit { .. } => { if !conf.segwit { return MmError::err(UnsupportedAddr::SegwitNotActivated(conf.ticker.clone())); } @@ -5424,7 +5440,7 @@ where ); let expected_output = TransactionOutput { value: expected_amount_sat, - script_pubkey: Builder::build_p2sh(&AddressHashEnum::AddressHash(dhash160(&redeem_script))).into(), + script_pubkey: Builder::build_p2sh(&LockingDestination::AddressHash(dhash160(&redeem_script))).into(), }; if args.funding_tx.outputs.first() != Some(&expected_output) { @@ -5743,22 +5759,22 @@ fn test_tx_v_size() { // the transaction is deserialized without the witnesses which makes the calculation of v_size similar to how // it's calculated in generate_transaction let tx: UtxoTx = "0200000000010192a4497268107d7999e9551be733f5e0eab479be7d995a061a7bbdc43ef0e5ed0000000000feffffff02cd857a00000000001600145cb39bfcd68d520e29cadc990bceb5cd1562c507a0860100000000001600149a85cc05e9a722575feb770a217c73fd6145cf01024730440220030e0fb58889ab939c701f12d950f00b64836a1a33ec0d6697fd3053d469d244022053e33d72ef53b37b86eea8dfebbafffb0f919ef952dcb6ea6058b81576d8dc86012102225de6aed071dc29d0ca10b9f64a4b502e33e55b3c0759eedd8e333834c6a7d07a1f2000".into(); - let v_size = tx_size_in_v_bytes(&UtxoAddressFormat::Segwit, &tx); + let v_size = tx_size_in_v_bytes(&UtxoAddressFormat::Segwit { version: 0 }, &tx); assert_eq!(v_size, 141); // Segwit input with 1 P2WSH output // https://live.blockcypher.com/btc-testnet/tx/f8c1fed6f307eb131040965bd11018787567413e6437c907b1fd15de6517ad16/ let tx: UtxoTx = "010000000001017996e77b2b1f4e66da606cfc2f16e3f52e1eac4a294168985bd4dbd54442e61f0100000000ffffffff01ab36010000000000220020693090c0e291752d448826a9dc72c9045b34ed4f7bd77e6e8e62645c23d69ac502483045022100d0800719239d646e69171ede7f02af916ac778ffe384fa0a5928645b23826c9f022044072622de2b47cfc81ac5172b646160b0c48d69d881a0ce77be06dbd6f6e5ac0121031ac6d25833a5961e2a8822b2e8b0ac1fd55d90cbbbb18a780552cbd66fc02bb3735a9e61".into(); - let v_size = tx_size_in_v_bytes(&UtxoAddressFormat::Segwit, &tx); + let v_size = tx_size_in_v_bytes(&UtxoAddressFormat::Segwit { version: 0 }, &tx); assert_eq!(v_size, 122); // Multiple segwit inputs with P2PKH output // https://live.blockcypher.com/btc-testnet/tx/649d514d76702a0925a917d830e407f4f1b52d78832520e486c140ce8d0b879f/ let tx: UtxoTx = "0100000000010250c434acbad252481564d56b41990577c55d247aedf4bb853dca3567c4404c8f0000000000ffffffff55baf016f0628ecf0f0ec228e24d8029879b0491ab18bac61865afaa9d16e8bb0000000000ffffffff01e8030000000000001976a9146d9d2b554d768232320587df75c4338ecc8bf37d88ac0247304402202611c05dd0e748f7c9955ed94a172af7ed56a0cdf773e8c919bef6e70b13ec1c02202fd7407891c857d95cdad1038dcc333186815f50da2fc9a334f814dd8d0a2d63012103c6a78589e18b482aea046975e6d0acbdea7bf7dbf04d9d5bd67fda917815e3ed02483045022100bb9d483f6b2b46f8e70d62d65b33b6de056e1878c9c2a1beed69005daef2f89502201690cd44cf6b114fa0d494258f427e1ed11a21d897e407d8a1ff3b7e09b9a426012103c6a78589e18b482aea046975e6d0acbdea7bf7dbf04d9d5bd67fda917815e3ed9cf7bd60".into(); - let v_size = tx_size_in_v_bytes(&UtxoAddressFormat::Segwit, &tx); + let v_size = tx_size_in_v_bytes(&UtxoAddressFormat::Segwit { version: 0 }, &tx); assert_eq!(v_size, 181); // Multiple segwit inputs // https://live.blockcypher.com/btc-testnet/tx/a7bb128703b57058955d555ed48b65c2c9bdefab6d3acbb4243c56e430533def/ let tx: UtxoTx = "010000000001023b7308e5ca5d02000b743441f7653c1110e07275b7ab0e983f489e92bfdd2b360100000000ffffffffd6c4f22e9b1090b2584a82cf4cb6f85595dd13c16ad065711a7585cc373ae2e50000000000ffffffff02947b2a00000000001600148474e72f396d44504cd30b1e7b992b65344240c609050700000000001600141b891309c8fe1338786fa3476d5d1a9718d43a0202483045022100bfae465fcd8d2636b2513f68618eb4996334c94d47e285cb538e3416eaf4521b02201b953f46ff21c8715a0997888445ca814dfdb834ef373a29e304bee8b32454d901210226bde3bca3fe7c91e4afb22c4bc58951c60b9bd73514081b6bd35f5c09b8c9a602483045022100ba48839f7becbf8f91266140f9727edd08974fcc18017661477af1d19603ed31022042fd35af1b393eeb818b420e3a5922079776cc73f006d26dd67be932e1b4f9000121034b6a54040ad2175e4c198370ac36b70d0b0ab515b59becf100c4cd310afbfd0c00000000".into(); - let v_size = tx_size_in_v_bytes(&UtxoAddressFormat::Segwit, &tx); + let v_size = tx_size_in_v_bytes(&UtxoAddressFormat::Segwit { version: 0 }, &tx); assert_eq!(v_size, 209) } @@ -5785,7 +5801,7 @@ fn test_generate_taker_fee_tx_outputs_with_standard_dex_fee() { assert_eq!(outputs[0].value, fee_uamount); assert_eq!( outputs[0].script_pubkey, - Builder::build_p2pkh(dex_address.hash()).to_bytes() + Builder::build_p2pkh(dex_address.locking_destination()).to_bytes() ); } @@ -5814,12 +5830,12 @@ fn test_generate_taker_fee_tx_outputs_with_non_kmd_burn() { assert_eq!(outputs[0].value, fee_uamount); assert_eq!( outputs[0].script_pubkey, - Builder::build_p2pkh(dex_address.hash()).to_bytes() + Builder::build_p2pkh(dex_address.locking_destination()).to_bytes() ); assert_eq!(outputs[1].value, burn_uamount); assert_eq!( outputs[1].script_pubkey, - Builder::build_p2pkh(burn_address.hash()).to_bytes() + Builder::build_p2pkh(burn_address.locking_destination()).to_bytes() ); } @@ -5854,7 +5870,7 @@ fn test_generate_taker_fee_tx_outputs_with_kmd_burn() { assert_eq!(outputs[0].value, fee_uamount); assert_eq!( outputs[0].script_pubkey, - Builder::build_p2pkh(dex_address.hash()).to_bytes() + Builder::build_p2pkh(dex_address.locking_destination()).to_bytes() ); assert_eq!(outputs[1].value, burn_uamount); assert_eq!( diff --git a/mm2src/coins/utxo/utxo_common_tests.rs b/mm2src/coins/utxo/utxo_common_tests.rs index 3c9af239bb..85271d2d02 100644 --- a/mm2src/coins/utxo/utxo_common_tests.rs +++ b/mm2src/coins/utxo/utxo_common_tests.rs @@ -84,15 +84,15 @@ pub(super) fn utxo_coin_fields_for_test( None }; let addr_format = if is_segwit_coin { - UtxoAddressFormat::Segwit + UtxoAddressFormat::Segwit { version: 0 } } else { UtxoAddressFormat::Standard }; let my_address = AddressBuilder::new(addr_format, checksum_type, prefixes, hrp) - .as_pkh_from_pk(*key_pair.public()) + .using_pk(*key_pair.public()) .build() .expect("valid address props"); - let my_script_pubkey = Builder::build_p2pkh(my_address.hash()).to_bytes(); + let my_script_pubkey = Builder::build_p2pkh(my_address.locking_destination()).to_bytes(); let priv_key_policy = PrivKeyPolicy::Iguana(key_pair); let derivation_method = DerivationMethod::SingleAddress(my_address); diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index f03c5d6c6e..74863da6ab 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -254,8 +254,10 @@ fn test_generate_transaction() { }]; let outputs = vec![TransactionOutput { - script_pubkey: Builder::build_p2pkh(block_on(coin.as_ref().derivation_method.unwrap_single_addr()).hash()) - .to_bytes(), + script_pubkey: Builder::build_p2pkh( + block_on(coin.as_ref().derivation_method.unwrap_single_addr()).locking_destination(), + ) + .to_bytes(), value: 100000, }]; @@ -1021,8 +1023,10 @@ fn test_utxo_lock() { let coin = utxo_coin_for_test(client.into(), None, false); let output = TransactionOutput { value: 1000000, - script_pubkey: Builder::build_p2pkh(block_on(coin.as_ref().derivation_method.unwrap_single_addr()).hash()) - .to_bytes(), + script_pubkey: Builder::build_p2pkh( + block_on(coin.as_ref().derivation_method.unwrap_single_addr()).locking_destination(), + ) + .to_bytes(), }; let mut futures = vec![]; for _ in 0..5 { @@ -1752,8 +1756,10 @@ fn test_spam_rick() { let output = TransactionOutput { value: 1000000, - script_pubkey: Builder::build_p2pkh(block_on(coin.as_ref().derivation_method.unwrap_single_addr()).hash()) - .to_bytes(), + script_pubkey: Builder::build_p2pkh( + block_on(coin.as_ref().derivation_method.unwrap_single_addr()).locking_destination(), + ) + .to_bytes(), }; let mut futures = vec![]; for _ in 0..5 { @@ -1852,7 +1858,7 @@ fn test_qtum_generate_pod() { &coin.as_ref().conf.address_prefixes, ) .unwrap(); - let res = coin.generate_pod(address.hash().clone()).unwrap(); + let res = coin.generate_pod(address.locking_destination().clone()).unwrap(); assert_eq!(expected_res, res.to_string()); } @@ -3568,7 +3574,7 @@ fn test_withdraw_to_p2pkh() { ) .as_pkh( block_on(coin.as_ref().derivation_method.unwrap_single_addr()) - .hash() + .locking_destination() .clone(), ) .build() @@ -3584,7 +3590,7 @@ fn test_withdraw_to_p2pkh() { let transaction: UtxoTx = deserialize(tx_details.tx.tx_hex().unwrap().as_slice()).unwrap(); let output_script: Script = transaction.outputs[0].script_pubkey.clone().into(); - let expected_script = Builder::build_p2pkh(p2pkh_address.hash()); + let expected_script = Builder::build_p2pkh(p2pkh_address.locking_destination()); assert_eq!(output_script, expected_script); } @@ -3624,7 +3630,7 @@ fn test_withdraw_to_p2sh() { ) .as_sh( block_on(coin.as_ref().derivation_method.unwrap_single_addr()) - .hash() + .locking_destination() .clone(), ) .build() @@ -3640,7 +3646,7 @@ fn test_withdraw_to_p2sh() { let transaction: UtxoTx = deserialize(tx_details.tx.tx_hex().unwrap().as_slice()).unwrap(); let output_script: Script = transaction.outputs[0].script_pubkey.clone().into(); - let expected_script = Builder::build_p2sh(p2sh_address.hash()); + let expected_script = Builder::build_p2sh(p2sh_address.locking_destination()); assert_eq!(output_script, expected_script); } @@ -3673,14 +3679,14 @@ fn test_withdraw_to_p2wpkh() { // Create a p2wpkh address for the test coin let p2wpkh_address = AddressBuilder::new( - UtxoAddressFormat::Segwit, + UtxoAddressFormat::Segwit { version: 0 }, *block_on(coin.as_ref().derivation_method.unwrap_single_addr()).checksum_type(), NetworkAddressPrefixes::default(), coin.as_ref().conf.bech32_hrp.clone(), ) .as_pkh( block_on(coin.as_ref().derivation_method.unwrap_single_addr()) - .hash() + .locking_destination() .clone(), ) .build() @@ -3696,7 +3702,7 @@ fn test_withdraw_to_p2wpkh() { let transaction: UtxoTx = deserialize(tx_details.tx.tx_hex().unwrap().as_slice()).unwrap(); let output_script: Script = transaction.outputs[0].script_pubkey.clone().into(); - let expected_script = Builder::build_p2wpkh(p2wpkh_address.hash()).expect("valid p2wpkh script"); + let expected_script = Builder::build_p2wpkh(p2wpkh_address.locking_destination()).expect("valid p2wpkh script"); assert_eq!(output_script, expected_script); } @@ -3748,7 +3754,7 @@ fn test_withdraw_p2pk_balance() { // The change should be in a p2pkh script. let output_script: Script = transaction.outputs[1].script_pubkey.clone().into(); - let expected_script = Builder::build_p2pkh(my_p2pkh_address.hash()); + let expected_script = Builder::build_p2pkh(my_p2pkh_address.locking_destination()); assert_eq!(output_script, expected_script); // And it should have this value (p2pk balance - amount sent - fees). @@ -3925,11 +3931,14 @@ fn test_split_qtum() { // fee_amount must be higher than the minimum fee assert!(data.fee_amount > 400_000); log!("Unsigned tx = {:?}", unsigned); - let signature_version = match p2pkh_address.addr_format() { - UtxoAddressFormat::Segwit => SignatureVersion::WitnessV0, - _ => coin.as_ref().conf.signature_version, - }; - let signed = sign_tx(unsigned, key_pair, signature_version, coin.as_ref().conf.fork_id).unwrap(); + + let signed = sign_tx( + unsigned, + key_pair, + coin.as_ref().conf.signature_version, + coin.as_ref().conf.fork_id, + ) + .unwrap(); log!("Signed tx = {:?}", signed); let res = block_on(coin.broadcast_tx(&signed)).unwrap(); log!("Res = {:?}", res); diff --git a/mm2src/coins/utxo/utxo_withdraw.rs b/mm2src/coins/utxo/utxo_withdraw.rs index 1d56867eda..64f5108a3e 100644 --- a/mm2src/coins/utxo/utxo_withdraw.rs +++ b/mm2src/coins/utxo/utxo_withdraw.rs @@ -21,7 +21,7 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use rpc::v1::types::ToTxHash; use rpc_task::RpcTaskError; -use script::{SignatureVersion, TransactionInputSigner}; +use script::TransactionInputSigner; use serialization::{serialize, serialize_with_flags, SERIALIZE_TRANSACTION_WITNESS}; use std::iter::once; use std::sync::Arc; @@ -138,15 +138,6 @@ where fn request(&self) -> &WithdrawRequest; - fn signature_version(&self) -> SignatureVersion { - match self.sender_address().addr_format() { - UtxoAddressFormat::Segwit => SignatureVersion::WitnessV0, - UtxoAddressFormat::Standard | UtxoAddressFormat::CashAddress { .. } => { - self.coin().as_ref().conf.signature_version - }, - } - } - #[allow(clippy::result_large_err)] fn on_generating_transaction(&self) -> Result<(), MmError>; @@ -222,7 +213,7 @@ where amount: big_decimal_from_sat(data.fee_amount as i64, decimals), }; let tx_hex = match coin.addr_format() { - UtxoAddressFormat::Segwit => serialize_with_flags(&signed, SERIALIZE_TRANSACTION_WITNESS).into(), + UtxoAddressFormat::Segwit { .. } => serialize_with_flags(&signed, SERIALIZE_TRANSACTION_WITNESS).into(), _ => serialize(&signed).into(), }; Ok(TransactionDetails { @@ -312,21 +303,25 @@ where let mut sign_params = UtxoSignTxParamsBuilder::new(); // TODO refactor [`UtxoTxBuilder::build`] to return `SpendingInputInfo` and `SendingOutputInfo` within `AdditionalTxData`. - sign_params.add_inputs_infos( - unsigned_tx - .inputs - .iter() - .map(|_input| match self.from_address.addr_format() { - AddressFormat::Segwit => SpendingInputInfo::P2WPKH { - address_derivation_path: self.from_derivation_path.clone(), - address_pubkey: self.from_pubkey, - }, - AddressFormat::Standard | AddressFormat::CashAddress { .. } => SpendingInputInfo::P2PKH { - address_derivation_path: self.from_derivation_path.clone(), - address_pubkey: self.from_pubkey, - }, - }), - ); + let spending_input_info = match self.from_address.addr_format() { + AddressFormat::Standard | AddressFormat::CashAddress { .. } => SpendingInputInfo::P2PKH { + address_derivation_path: self.from_derivation_path.clone(), + address_pubkey: self.from_pubkey, + }, + AddressFormat::Segwit { version: 0 } => SpendingInputInfo::P2WPKH { + address_derivation_path: self.from_derivation_path.clone(), + address_pubkey: self.from_pubkey, + }, + AddressFormat::Segwit { version: 1 } => SpendingInputInfo::P2TR { + address_derivation_path: self.from_derivation_path.clone(), + }, + AddressFormat::Segwit { version: v } => { + return Err(WithdrawError::InvalidAddress(format!( + "Unsupported segwit version v{v}" + )))?; + }, + }; + sign_params.add_inputs_infos(std::iter::repeat_n(spending_input_info, unsigned_tx.inputs.len())); sign_params.add_outputs_infos(once(SendingOutputInfo { destination_address: OutputDestination::plain(self.req.to.clone()), })); @@ -349,7 +344,7 @@ where } sign_params - .with_signature_version(self.signature_version()) + .with_signature_version(self.coin.as_ref().conf.signature_version) .with_unsigned_tx(unsigned_tx); let sign_params = sign_params.build().map_mm_err()?; @@ -483,7 +478,7 @@ where Ok(with_key_pair::sign_tx( unsigned_tx, &self.key_pair, - self.signature_version(), + self.coin.as_ref().conf.signature_version, self.coin.as_ref().conf.fork_id, ) .map_mm_err()?) diff --git a/mm2src/coins/utxo_signer/Cargo.toml b/mm2src/coins/utxo_signer/Cargo.toml index c37b1c675b..52875b7ecf 100644 --- a/mm2src/coins/utxo_signer/Cargo.toml +++ b/mm2src/coins/utxo_signer/Cargo.toml @@ -19,3 +19,7 @@ primitives = { path = "../../mm2_bitcoin/primitives" } rpc = { path = "../../mm2_bitcoin/rpc" } script = { path = "../../mm2_bitcoin/script" } serialization = { path = "../../mm2_bitcoin/serialization" } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +bitcoin.workspace = true +rand.workspace = true \ No newline at end of file diff --git a/mm2src/coins/utxo_signer/src/sign_common.rs b/mm2src/coins/utxo_signer/src/sign_common.rs index aa2ab355ef..63c3735eb1 100644 --- a/mm2src/coins/utxo_signer/src/sign_common.rs +++ b/mm2src/coins/utxo_signer/src/sign_common.rs @@ -106,6 +106,22 @@ pub(crate) fn p2wpkh_spend_with_signature( } } +/// Create TransactionInput spending P2TR output, adding script witness with signature +pub(crate) fn p2tr_spend_with_signature( + unsigned_input: &UnsignedTransactionInput, + fork_id: u32, + signature: Signature, +) -> TransactionInput { + let script_sig = script_sig(signature, fork_id); + + TransactionInput { + previous_output: unsigned_input.previous_output, + script_sig: Bytes::from(Vec::new()), + sequence: unsigned_input.sequence, + script_witness: vec![script_sig], + } +} + pub(crate) fn script_sig_with_pub(public_key: &PublicKey, fork_id: u32, signature: Signature) -> Bytes { let script_sig = script_sig(signature, fork_id); let builder = Builder::default(); diff --git a/mm2src/coins/utxo_signer/src/sign_params.rs b/mm2src/coins/utxo_signer/src/sign_params.rs index e1aa4b391c..27b988cda4 100644 --- a/mm2src/coins/utxo_signer/src/sign_params.rs +++ b/mm2src/coins/utxo_signer/src/sign_params.rs @@ -16,6 +16,7 @@ impl UtxoSignTxError { } /// An additional info of a spending input. +#[derive(Clone)] pub enum SpendingInputInfo { P2PKH { address_derivation_path: DerivationPath, @@ -25,6 +26,9 @@ pub enum SpendingInputInfo { address_derivation_path: DerivationPath, address_pubkey: PublicKey, }, + P2TR { + address_derivation_path: DerivationPath, + }, // The fields are used to generate `trezor::proto::messages_bitcoin::MultisigRedeemScriptType` // P2SH {} } @@ -64,9 +68,12 @@ impl SendingOutputInfo { #[inline] pub fn trezor_output_script_type(&self) -> TrezorOutputScriptType { match self.destination_address { - OutputDestination::Change { ref addr_format, .. } if *addr_format == AddressFormat::Segwit => { + OutputDestination::Change { ref addr_format, .. } if addr_format.is_segwit_v0() => { TrezorOutputScriptType::PayToWitness }, + OutputDestination::Change { ref addr_format, .. } if addr_format.is_segwit_v1() => { + TrezorOutputScriptType::PayToTaproot + }, OutputDestination::Change { .. } | OutputDestination::Plain { .. } => TrezorOutputScriptType::PayToAddress, } } diff --git a/mm2src/coins/utxo_signer/src/with_key_pair.rs b/mm2src/coins/utxo_signer/src/with_key_pair.rs index bf4ffb1f38..5054ff034a 100644 --- a/mm2src/coins/utxo_signer/src/with_key_pair.rs +++ b/mm2src/coins/utxo_signer/src/with_key_pair.rs @@ -53,7 +53,8 @@ pub fn sign_tx( .enumerate() .map(|(i, input)| { match input.prev_script.script_type() { - ScriptType::WitnessKey => p2wpkh_spend(&unsigned, i, key_pair, SignatureVersion::WitnessV0, fork_id), + ScriptType::Taproot => p2tr_spend(&unsigned, i, key_pair, fork_id), + ScriptType::WitnessKey => p2wpkh_spend(&unsigned, i, key_pair, fork_id), ScriptType::PubKeyHash => p2pkh_spend(&unsigned, i, key_pair, signature_version, fork_id), // Allow spending legacy P2PK utxos. ScriptType::PubKey => p2pk_spend(&unsigned, i, key_pair, signature_version, fork_id), @@ -168,7 +169,6 @@ pub fn p2wpkh_spend( signer: &TransactionInputSigner, input_index: usize, key_pair: &KeyPair, - signature_version: SignatureVersion, fork_id: u32, ) -> UtxoSignWithKeyPairResult { let unsigned_input = get_input(signer, input_index)?; @@ -188,7 +188,7 @@ pub fn p2wpkh_spend( input_index, &script_code, key_pair, - signature_version, + SignatureVersion::Witness, SIGHASH_ALL, fork_id, )?; @@ -200,6 +200,88 @@ pub fn p2wpkh_spend( )) } +/// Creates signed input spending p2tr output +#[cfg(not(target_arch = "wasm32"))] +pub fn p2tr_spend( + signer: &TransactionInputSigner, + input_index: usize, + key_pair: &KeyPair, + fork_id: u32, +) -> UtxoSignWithKeyPairResult { + use crate::sign_common::p2tr_spend_with_signature; + use bitcoin::psbt::Prevouts; + use bitcoin::schnorr::TapTweak; + use bitcoin::secp256k1::{KeyPair, Secp256k1}; + use bitcoin::util::sighash::SighashCache; + use bitcoin::SchnorrSighashType; + + // Note that we can't use our secp256k1 dependency and must use the one re-exported by `rust-bitcoin` + // since that one will have the key tweaking implementations over secp256k1 keys. + // Only if our secp256k1 dependency was of the same version as `rust-bitcoin`'s they would be interchangeable. + // TODO: Once https://github.com/KomodoPlatform/komodo-defi-framework/pull/2623 is merged, + // we should use the global signing SECP object found in mm2_bitcoin::keys and possibly + // move signing and tweaking function calls over there too. + let secp = Secp256k1::new(); + + // Convert the key pair to the secp256k1::KeyPair. + let key_pair = KeyPair::from_seckey_slice(&secp, &key_pair.private_bytes()) + .map_err(|_| UtxoSignWithKeyPairError::ErrorSigning(keys::Error::InvalidSecret))?; + + // Tweak the key pair for taproot script constructions and signing later. + let tweaked_keypair = key_pair.tap_tweak(&secp, None).to_inner(); + let (x_only_pub, _) = key_pair.x_only_public_key(); + let (tweaked_pub, _) = x_only_pub.tap_tweak(&secp, None); + + // Make sure our key is authorized to spend this input (i.e. make sure we got the expected `prev_script`). + let script_pub_key = Builder::build_p2tr(&keys::LockingDestination::TweakedXOnlyPubkey( + tweaked_pub.serialize().into(), + ))?; + let unsigned_input = get_input(signer, input_index)?; + if script_pub_key != unsigned_input.prev_script { + return MmError::err(UtxoSignWithKeyPairError::MismatchScript { + script_type: "P2TR".to_owned(), + script: script_pub_key, + prev_script: unsigned_input.prev_script.clone(), + }); + } + + // Calculate the sighash that we want to sign. + let prevouts: Vec<_> = signer + .inputs + .iter() + .map(|i| bitcoin::TxOut { + value: i.amount, + script_pubkey: i.prev_script.to_vec().into(), + }) + .collect(); + let sighash = SighashCache::new(&mut signer.clone().into()) + .taproot_key_spend_signature_hash(input_index, &Prevouts::All(&prevouts), SchnorrSighashType::All) + .map_err(|_| UtxoSignWithKeyPairError::ErrorSigning(keys::Error::InvalidSecret))?; + + // Sign the sighash + let signature = secp.sign_schnorr_with_aux_rand(&sighash.into(), &tweaked_keypair, &rand::random()); + + Ok(p2tr_spend_with_signature( + unsigned_input, + fork_id, + Bytes::from(signature.as_ref().to_vec()), + )) +} + +#[cfg(target_arch = "wasm32")] +pub fn p2tr_spend( + signer: &TransactionInputSigner, + input_index: usize, + _key_pair: &KeyPair, + _fork_id: u32, +) -> UtxoSignWithKeyPairResult { + // TODO: remove this function and the non-wasm cfg in the function above to enable taproot support + // for wasm once https://github.com/KomodoPlatform/komodo-defi-framework/pull/2623 is merged. + MmError::err(UtxoSignWithKeyPairError::UnspendableUTXO { + script: get_input(signer, input_index)?.prev_script.clone(), + }) +} + /// Calculates the input script hash and sign it using `key_pair`. pub fn calc_and_sign_sighash( signer: &TransactionInputSigner, diff --git a/mm2src/coins/utxo_signer/src/with_trezor.rs b/mm2src/coins/utxo_signer/src/with_trezor.rs index 0b747260e2..5ad961ff89 100644 --- a/mm2src/coins/utxo_signer/src/with_trezor.rs +++ b/mm2src/coins/utxo_signer/src/with_trezor.rs @@ -1,4 +1,6 @@ -use crate::sign_common::{complete_tx, p2pkh_spend_with_signature, p2wpkh_spend_with_signature}; +use crate::sign_common::{ + complete_tx, p2pkh_spend_with_signature, p2tr_spend_with_signature, p2wpkh_spend_with_signature, +}; use crate::sign_params::{OutputDestination, SendingOutputInfo, SpendingInputInfo, UtxoSignTxParams}; use crate::{TxProvider, UtxoSignTxError, UtxoSignTxResult}; use chain::{Transaction as UtxoTx, TransactionOutput}; @@ -49,6 +51,9 @@ impl TrezorTxSigner<'_, TxP> { SpendingInputInfo::P2WPKH { address_pubkey, .. } => { p2wpkh_spend_with_signature(unsigned_input, address_pubkey, self.fork_id, Bytes::from(signature)) }, + SpendingInputInfo::P2TR { .. } => { + p2tr_spend_with_signature(unsigned_input, self.fork_id, Bytes::from(signature)) + }, }) .collect(); Ok(complete_tx(self.params.unsigned_tx, signed_inputs)) @@ -57,7 +62,7 @@ impl TrezorTxSigner<'_, TxP> { async fn get_trezor_unsigned_tx(&self) -> UtxoSignTxResult { let mut inputs = Vec::with_capacity(self.params.unsigned_tx.inputs.len()); for (unsigned_input, input_info) in self.params.inputs() { - let unsigned_input = self + let unsigned_input: UnsignedTxInput = self .get_trezor_unsigned_input(unsigned_input, input_info) .await .map_mm_err()?; @@ -120,6 +125,12 @@ impl TrezorTxSigner<'_, TxP> { Some(address_derivation_path.clone()), TrezorInputScriptType::SpendWitness, ), + SpendingInputInfo::P2TR { + address_derivation_path, + } => ( + Some(address_derivation_path.clone()), + TrezorInputScriptType::SpendTaproot, + ), }; Ok(UnsignedTxInput { diff --git a/mm2src/coins_activation/src/lightning_activation.rs b/mm2src/coins_activation/src/lightning_activation.rs index 97c59bc9ff..532ebe9d17 100644 --- a/mm2src/coins_activation/src/lightning_activation.rs +++ b/mm2src/coins_activation/src/lightning_activation.rs @@ -260,7 +260,7 @@ impl InitL2ActivationOps for LightningCoin { // Channel funding transactions need to spend segwit outputs // and while the witness script can be generated from pubkey and be used // it's better for the coin to be enabled in segwit to check if balance is enough for funding transaction, etc... - if !platform_coin.addr_format().is_segwit() { + if !platform_coin.addr_format().is_segwit_v0() { return MmError::err( LightningValidationErr::UnsupportedMode("Lightning network".into(), "segwit".into()).into(), ); diff --git a/mm2src/crypto/src/standard_hd_path.rs b/mm2src/crypto/src/standard_hd_path.rs index 6f63bb2f59..0fc6218aa4 100644 --- a/mm2src/crypto/src/standard_hd_path.rs +++ b/mm2src/crypto/src/standard_hd_path.rs @@ -247,6 +247,7 @@ pub enum Bip43Purpose { Bip44 = 44, Bip49 = 49, Bip84 = 84, + Bip86 = 86, } #[derive(Clone, PartialEq)] @@ -280,11 +281,12 @@ impl Bip32ChildValue for Bip32PurposeValue { 44 => Bip43Purpose::Bip44, 49 => Bip43Purpose::Bip49, 84 => Bip43Purpose::Bip84, + 86 => Bip43Purpose::Bip86, _chain => { return Err(Bip32DerPathError::UnexpectedChildValue { child_at, actual: child_number.0, - expected: "one of the following: 32, 44, 49, 84".to_string(), + expected: "one of the following: 32, 44, 49, 84, 86".to_string(), }) }, }; diff --git a/mm2src/mm2_bitcoin/keys/Cargo.toml b/mm2src/mm2_bitcoin/keys/Cargo.toml index 9acf887d18..29a5e0d8fd 100644 --- a/mm2src/mm2_bitcoin/keys/Cargo.toml +++ b/mm2src/mm2_bitcoin/keys/Cargo.toml @@ -19,3 +19,6 @@ rustc-hex.workspace = true secp256k1 = { workspace = true, features = ["rand", "recovery"] } serde = { workspace = true, features = ["derive"] } serde_derive.workspace = true + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +bitcoin.workspace = true diff --git a/mm2src/mm2_bitcoin/keys/src/address.rs b/mm2src/mm2_bitcoin/keys/src/address.rs index ee4e4fef5b..ad4b96075a 100644 --- a/mm2src/mm2_bitcoin/keys/src/address.rs +++ b/mm2src/mm2_bitcoin/keys/src/address.rs @@ -13,7 +13,7 @@ use std::str::FromStr; use std::{fmt, hash::Hash}; use { - AddressHashEnum, AddressPrefix, CashAddrType, CashAddress, Error, LegacyAddress, NetworkAddressPrefixes, + AddressPrefix, CashAddrType, CashAddress, Error, LegacyAddress, LockingDestination, NetworkAddressPrefixes, SegwitAddress, }; @@ -43,6 +43,11 @@ pub enum AddressScriptType { /// as the scripthash, eg: bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3. /// https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki P2WSH, + /// Pay to Taproot + /// Segwit v1 P2TR which begins with the human readable part followed by 1 followed by 59 base32 characters + /// as the scripthash, eg: bc1p6gps4j04duwphrhkwx0vhl6r9kkq8m8n7r9r02rvwzrekjt0f4pskz8zas. + /// https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki + P2TR, } #[derive(Clone, Debug, Default, Display, Deserialize, Eq, Hash, PartialEq, Serialize)] @@ -55,9 +60,13 @@ pub enum AddressFormat { #[default] Standard, /// Segwit Address - /// https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki + /// https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki (bech32 for v0) + /// https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki (bech32m for v1+) #[serde(rename = "segwit")] - Segwit, + Segwit { + #[serde(default)] + version: u8, + }, /// Bitcoin Cash specific address format. /// https://github.com/bitcoincashorg/bitcoincash.org/blob/master/spec/cashaddr.md #[serde(rename = "cashaddress")] @@ -73,7 +82,15 @@ pub enum AddressFormat { impl AddressFormat { pub fn is_segwit(&self) -> bool { - matches!(*self, AddressFormat::Segwit) + matches!(*self, AddressFormat::Segwit { .. }) + } + + pub fn is_segwit_v0(&self) -> bool { + matches!(*self, AddressFormat::Segwit { version: 0 }) + } + + pub fn is_segwit_v1(&self) -> bool { + matches!(*self, AddressFormat::Segwit { version: 1 }) } pub fn is_cashaddress(&self) -> bool { @@ -83,6 +100,10 @@ impl AddressFormat { pub fn is_legacy(&self) -> bool { matches!(*self, AddressFormat::Standard) } + + pub fn is_legacy_or_cashaddr(&self) -> bool { + self.is_legacy() || self.is_cashaddress() + } } // Todo: add segwit checksum detection @@ -112,8 +133,8 @@ pub struct Address { hrp: Option, /// The public key of the address. pubkey: Option, - /// Public key/Script hash. - hash: AddressHashEnum, + /// Public key hash/Script hash/witness script hash/tweaked x-only pubkey + locking_destination: LockingDestination, /// Checksum type checksum_type: ChecksumType, /// Address Format @@ -129,7 +150,7 @@ impl PartialEq for Address { fn eq(&self, other: &Self) -> bool { self.prefix == other.prefix && self.hrp == other.hrp - && self.hash == other.hash + && self.locking_destination == other.locking_destination && self.checksum_type == other.checksum_type && self.addr_format == other.addr_format && self.script_type == other.script_type @@ -141,7 +162,7 @@ impl Hash for Address { fn hash(&self, state: &mut H) { self.prefix.hash(state); self.hrp.hash(state); - self.hash.hash(state); + self.locking_destination.hash(state); self.checksum_type.hash(state); self.addr_format.hash(state); self.script_type.hash(state); @@ -158,8 +179,8 @@ impl Address { pub fn pubkey(&self) -> &Option { &self.pubkey } - pub fn hash(&self) -> &AddressHashEnum { - &self.hash + pub fn locking_destination(&self) -> &LockingDestination { + &self.locking_destination } pub fn checksum_type(&self) -> &ChecksumType { &self.checksum_type @@ -173,20 +194,24 @@ impl Address { /// Returns true if output script type is pubkey hash (p2pkh or p2wpkh) pub fn is_pubkey_hash(&self) -> bool { - if matches!(self.addr_format, AddressFormat::Segwit) { + if self.addr_format.is_segwit_v0() { self.script_type == AddressScriptType::P2WPKH - } else { + } else if self.addr_format.is_legacy_or_cashaddr() { self.script_type == AddressScriptType::P2PKH + } else { + false } } pub fn display_address(&self) -> Result { match &self.addr_format { AddressFormat::Standard => { - Ok(LegacyAddress::new(&self.hash, self.prefix.clone(), self.checksum_type).to_string()) + Ok(LegacyAddress::new(&self.locking_destination, self.prefix.clone(), self.checksum_type).to_string()) }, - AddressFormat::Segwit => match &self.hrp { - Some(hrp) => Ok(SegwitAddress::new(&self.hash, hrp.clone()).to_string()), + AddressFormat::Segwit { version } => match &self.hrp { + Some(hrp) => Ok(SegwitAddress::new(&self.locking_destination, hrp.clone(), *version) + .map_err(|e| e.to_string())? + .to_string()), None => Err("Cannot display segwit address for a coin with no bech32_hrp in config".into()), }, AddressFormat::CashAddress { @@ -210,8 +235,8 @@ impl Address { if address.hash.len() != 20 { return Err("Expect 20 bytes long hash".into()); } - let mut hash = AddressHashEnum::default_address_hash(); - hash.copy_from_slice(address.hash.as_slice()); + let mut locking_destination = LockingDestination::default_address_hash(); + locking_destination.copy_from_slice(address.hash.as_slice()); let script_type = if address.prefix == prefixes.p2pkh { AddressScriptType::P2PKH @@ -223,7 +248,7 @@ impl Address { Ok(Address { prefix: address.prefix, - hash, + locking_destination, checksum_type: address.checksum_type, hrp: None, pubkey: None, @@ -243,8 +268,8 @@ impl Address { return Err("Expect 20 bytes long hash".into()); } - let mut hash = AddressHashEnum::default_address_hash(); - hash.copy_from_slice(address.hash.as_slice()); + let mut locking_destination = LockingDestination::default_address_hash(); + locking_destination.copy_from_slice(address.hash.as_slice()); let (script_type, addr_prefix) = match address.address_type { CashAddrType::P2PKH => (AddressScriptType::P2PKH, net_addr_prefixes.p2pkh.clone()), @@ -253,7 +278,7 @@ impl Address { Ok(Address { prefix: addr_prefix, - hash, + locking_destination, checksum_type, hrp: None, pubkey: None, @@ -281,48 +306,67 @@ impl Address { self.prefix, network_addr_prefixes.p2pkh, network_addr_prefixes.p2sh )); }; - CashAddress::new(network_prefix, self.hash.to_vec(), address_type) + CashAddress::new(network_prefix, self.locking_destination.to_vec(), address_type) } pub fn from_segwitaddress(segaddr: &str, checksum_type: ChecksumType) -> Result { let address = SegwitAddress::from_str(segaddr).map_err(|e| e.to_string())?; - let (script_type, mut hash) = if address.program.len() == 20 { - (AddressScriptType::P2WPKH, AddressHashEnum::default_address_hash()) - } else if address.program.len() == 32 { - (AddressScriptType::P2WSH, AddressHashEnum::default_witness_script_hash()) - } else { - return Err("Expect either 20 or 32 bytes long hash".into()); + let (script_type, mut locking_destination) = match (address.version_as_u8(), address.program.len()) { + (0, 20) => (AddressScriptType::P2WPKH, LockingDestination::default_address_hash()), + (0, 32) => ( + AddressScriptType::P2WSH, + LockingDestination::default_witness_script_hash(), + ), + (1, 32) => ( + AddressScriptType::P2TR, + LockingDestination::default_tweaked_xonly_pubkey(), + ), + (0, _) => return Err("Expect either 20 or 32 bytes long hash for witness v0 address".into()), + (1, _) => return Err("Expect 32 bytes long public key for witness v1 address".into()), + (v, _) => return Err(format!("Unsupported segwit version: {v}")), + }; + + locking_destination.copy_from_slice(address.program.as_slice()); + + let addr_format = AddressFormat::Segwit { + version: address.version_as_u8(), }; - hash.copy_from_slice(address.program.as_slice()); let hrp = Some(address.hrp); Ok(Address { prefix: AddressPrefix::default(), - hash, + locking_destination, checksum_type, hrp, pubkey: None, - addr_format: AddressFormat::Segwit, + addr_format, script_type, }) } pub fn to_segwitaddress(&self) -> Result { - match &self.hrp { - Some(hrp) => Ok(SegwitAddress::new(&self.hash, hrp.to_string())), - None => Err("hrp must be provided for segwit address".into()), - } + let Some(hrp) = &self.hrp else { + return Err("hrp must be provided for segwit address".into()); + }; + let AddressFormat::Segwit { version } = self.addr_format else { + return Err("address format must be segwit".into()); + }; + SegwitAddress::new(&self.locking_destination, hrp.to_string(), version).map_err(|e| e.to_string()) } } impl fmt::Display for Address { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self.addr_format { - AddressFormat::Segwit => { - SegwitAddress::new(&self.hash, self.hrp.clone().expect("Segwit address should have an hrp")).fmt(f) - }, + AddressFormat::Segwit { version } => SegwitAddress::new( + &self.locking_destination, + self.hrp.clone().expect("Segwit address should have an hrp"), + *version, + ) + .expect("Segwit address should be valid") + .fmt(f), AddressFormat::CashAddress { network, pub_addr_prefix, @@ -339,14 +383,16 @@ impl fmt::Display for Address { .expect("A valid address"); cash_address.encode().expect("A valid address").fmt(f) }, - AddressFormat::Standard => LegacyAddress::new(&self.hash, self.prefix.clone(), self.checksum_type).fmt(f), + AddressFormat::Standard => { + LegacyAddress::new(&self.locking_destination, self.prefix.clone(), self.checksum_type).fmt(f) + }, } } } #[cfg(test)] mod tests { - use super::{Address, AddressBuilder, AddressFormat, AddressHashEnum, CashAddrType, CashAddress, ChecksumType}; + use super::{Address, AddressBuilder, AddressFormat, CashAddrType, CashAddress, ChecksumType, LockingDestination}; use crate::address_prefixes::prefixes::*; use crate::{NetworkAddressPrefixes, NetworkPrefix}; @@ -358,7 +404,7 @@ mod tests { (*BTC_PREFIXES).clone(), None, ) - .as_pkh(AddressHashEnum::AddressHash( + .as_pkh(LockingDestination::AddressHash( "3f4aa1fedf1f54eeb03b759deadb36676b184911".into(), )) .build() @@ -375,7 +421,7 @@ mod tests { (*KMD_PREFIXES).clone(), None, ) - .as_pkh(AddressHashEnum::AddressHash( + .as_pkh(LockingDestination::AddressHash( "05aab5342166f8594baf17a7d9bef5d567443327".into(), )) .build() @@ -392,7 +438,7 @@ mod tests { (*T_ZCASH_PREFIXES).clone(), None, ) - .as_pkh(AddressHashEnum::AddressHash( + .as_pkh(LockingDestination::AddressHash( "05aab5342166f8594baf17a7d9bef5d567443327".into(), )) .build() @@ -409,7 +455,7 @@ mod tests { (*KMD_PREFIXES).clone(), None, ) - .as_sh(AddressHashEnum::AddressHash( + .as_sh(LockingDestination::AddressHash( "ca0c3786c96ff7dacd40fdb0f7c196528df35f85".into(), )) .build() @@ -426,7 +472,7 @@ mod tests { (*BTC_PREFIXES).clone(), None, ) - .as_pkh(AddressHashEnum::AddressHash( + .as_pkh(LockingDestination::AddressHash( "3f4aa1fedf1f54eeb03b759deadb36676b184911".into(), )) .build() @@ -447,7 +493,7 @@ mod tests { (*KMD_PREFIXES).clone(), None, ) - .as_pkh(AddressHashEnum::AddressHash( + .as_pkh(LockingDestination::AddressHash( "05aab5342166f8594baf17a7d9bef5d567443327".into(), )) .build() @@ -468,7 +514,7 @@ mod tests { (*T_ZCASH_PREFIXES).clone(), None, ) - .as_pkh(AddressHashEnum::AddressHash( + .as_pkh(LockingDestination::AddressHash( "05aab5342166f8594baf17a7d9bef5d567443327".into(), )) .build() @@ -489,7 +535,7 @@ mod tests { (*KMD_PREFIXES).clone(), None, ) - .as_sh(AddressHashEnum::AddressHash( + .as_sh(LockingDestination::AddressHash( "ca0c3786c96ff7dacd40fdb0f7c196528df35f85".into(), )) .build() @@ -510,7 +556,7 @@ mod tests { (*GRS_PREFIXES).clone(), None, ) - .as_pkh(AddressHashEnum::AddressHash( + .as_pkh(LockingDestination::AddressHash( "c3f710deb7320b0efa6edb14e3ebeeb9155fa90d".into(), )) .build() @@ -531,7 +577,7 @@ mod tests { (*SYS_PREFIXES).clone(), None, ) - .as_pkh(AddressHashEnum::AddressHash( + .as_pkh(LockingDestination::AddressHash( "56bb05aa20f5a80cf84e90e5dab05be331333e27".into(), )) .build() @@ -562,7 +608,7 @@ mod tests { Address::from_cashaddress(cashaddresses[i], ChecksumType::DSHA256, &BCH_PREFIXES).unwrap(); let expected_address: Address = Address::from_legacyaddress(expected[i], &BCH_PREFIXES).unwrap(); // comparing only hashes here as Address::from_cashaddress has a different internal format from into() - assert_eq!(actual_address.hash, expected_address.hash); + assert_eq!(actual_address.locking_destination, expected_address.locking_destination); let actual_cashaddress = actual_address .to_cashaddress("bitcoincash", &BCH_PREFIXES) .unwrap() @@ -601,7 +647,7 @@ mod tests { unknown_prefixes, None, ) - .as_sh(AddressHashEnum::AddressHash( + .as_sh(LockingDestination::AddressHash( [ 140, 0, 44, 191, 189, 83, 144, 173, 47, 216, 127, 59, 80, 232, 159, 100, 156, 132, 78, 192, ] diff --git a/mm2src/mm2_bitcoin/keys/src/address/address_builder.rs b/mm2src/mm2_bitcoin/keys/src/address/address_builder.rs index 84e08a5e9b..ce24efbb10 100644 --- a/mm2src/mm2_bitcoin/keys/src/address/address_builder.rs +++ b/mm2src/mm2_bitcoin/keys/src/address/address_builder.rs @@ -1,17 +1,25 @@ use crate::Public; use crypto::ChecksumType; -use {Address, AddressFormat, AddressHashEnum, AddressPrefix, AddressScriptType, NetworkAddressPrefixes}; +#[cfg(not(target_arch = "wasm32"))] +use bitcoin::schnorr::TapTweak; +#[cfg(not(target_arch = "wasm32"))] +use bitcoin::secp256k1::{PublicKey, Secp256k1}; + +use {Address, AddressFormat, AddressPrefix, AddressScriptType, LockingDestination, NetworkAddressPrefixes}; /// Params for AddressBuilder to select output script type #[derive(PartialEq)] pub enum AddressBuilderOption { /// build for pay to pubkey hash output (witness or legacy) - PubkeyHash(AddressHashEnum), + PubkeyHash(LockingDestination), /// build for pay to script hash output (witness or legacy) - ScriptHash(AddressHashEnum), + ScriptHash(LockingDestination), /// build for pay to pubkey hash but using a public key as an input (not pubkey hash) FromPubKey(Public), + /// build for pay to taproot output using a tweaked x-only pubkey as an input. + /// Note that the address format in this case must be segwit and v1. + TweakedXOnlyPubkey(LockingDestination), } /// Builds Address struct depending on addr_format, validates params to build Address @@ -51,57 +59,70 @@ impl AddressBuilder { } /// Sets Address tx output script type as p2pkh or p2wpkh, but also keep the public key stored. - pub fn as_pkh_from_pk(mut self, pubkey: Public) -> Self { + pub fn using_pk(mut self, pubkey: Public) -> Self { self.build_option = Some(AddressBuilderOption::FromPubKey(pubkey)); self } /// Sets Address tx output script type as p2pkh or p2wpkh - pub fn as_pkh(mut self, hash: AddressHashEnum) -> Self { - self.build_option = Some(AddressBuilderOption::PubkeyHash(hash)); + pub fn as_pkh(mut self, address_hash: LockingDestination) -> Self { + self.build_option = Some(AddressBuilderOption::PubkeyHash(address_hash)); self } /// Sets Address tx output script type as p2sh or p2wsh - pub fn as_sh(mut self, hash: AddressHashEnum) -> Self { - self.build_option = Some(AddressBuilderOption::ScriptHash(hash)); + pub fn as_sh(mut self, script_hash: LockingDestination) -> Self { + self.build_option = Some(AddressBuilderOption::ScriptHash(script_hash)); + self + } + + /// Sets Address tx output script type as p2tr + pub fn as_tr(mut self, x_only_pubkey: LockingDestination) -> Self { + self.build_option = Some(AddressBuilderOption::TweakedXOnlyPubkey(x_only_pubkey)); self } pub fn build(&self) -> Result { let build_option = self.build_option.as_ref().ok_or("no address builder option set")?; match &self.addr_format { - AddressFormat::Standard => Ok(Address { - prefix: self.get_address_prefix(build_option)?, - hrp: None, - hash: self.get_hash(build_option), - pubkey: self.get_pubkey(build_option), - checksum_type: self.checksum_type, - addr_format: self.addr_format.clone(), - script_type: self.get_legacy_script_type(build_option), - }), - AddressFormat::Segwit => { - self.check_segwit_hrp()?; + AddressFormat::Standard => { + self.check_legacy_hash(build_option)?; + Ok(Address { + prefix: self.get_address_prefix(build_option)?, + hrp: None, + locking_destination: self.get_locking_destination(build_option)?, + pubkey: self.get_pubkey(build_option), + checksum_type: self.checksum_type, + addr_format: self.addr_format.clone(), + script_type: self.get_legacy_script_type(build_option)?, + }) + }, + AddressFormat::Segwit { .. } => { self.check_segwit_hash(build_option)?; + self.check_segwit_version(build_option)?; + self.check_segwit_hrp()?; Ok(Address { prefix: AddressPrefix::default(), hrp: self.hrp.clone(), - hash: self.get_hash(build_option), + locking_destination: self.get_locking_destination(build_option)?, + pubkey: self.get_pubkey(build_option), + checksum_type: self.checksum_type, + addr_format: self.addr_format.clone(), + script_type: self.get_segwit_script_type(build_option)?, + }) + }, + AddressFormat::CashAddress { .. } => { + self.check_legacy_hash(build_option)?; + Ok(Address { + prefix: self.get_address_prefix(build_option)?, + hrp: None, + locking_destination: self.get_locking_destination(build_option)?, pubkey: self.get_pubkey(build_option), checksum_type: self.checksum_type, addr_format: self.addr_format.clone(), - script_type: self.get_segwit_script_type(build_option), + script_type: self.get_legacy_script_type(build_option)?, }) }, - AddressFormat::CashAddress { .. } => Ok(Address { - prefix: self.get_address_prefix(build_option)?, - hrp: None, - hash: self.get_hash(build_option), - pubkey: self.get_pubkey(build_option), - checksum_type: self.checksum_type, - addr_format: self.addr_format.clone(), - script_type: self.get_legacy_script_type(build_option), - }), } } @@ -109,6 +130,7 @@ impl AddressBuilder { let prefix = match build_option { AddressBuilderOption::PubkeyHash(_) | AddressBuilderOption::FromPubKey(_) => &self.prefixes.p2pkh, AddressBuilderOption::ScriptHash(_) => &self.prefixes.p2sh, + AddressBuilderOption::TweakedXOnlyPubkey(_) => return Err("No prefixes for segwit v1 address".to_owned()), }; if prefix.is_empty() { return Err("no prefix for address set".to_owned()); @@ -116,26 +138,65 @@ impl AddressBuilder { Ok(prefix.clone()) } - fn get_legacy_script_type(&self, build_option: &AddressBuilderOption) -> AddressScriptType { + fn get_legacy_script_type(&self, build_option: &AddressBuilderOption) -> Result { match build_option { - AddressBuilderOption::PubkeyHash(_) | AddressBuilderOption::FromPubKey(_) => AddressScriptType::P2PKH, - AddressBuilderOption::ScriptHash(_) => AddressScriptType::P2SH, + AddressBuilderOption::PubkeyHash(_) | AddressBuilderOption::FromPubKey(_) => Ok(AddressScriptType::P2PKH), + AddressBuilderOption::ScriptHash(_) => Ok(AddressScriptType::P2SH), + AddressBuilderOption::TweakedXOnlyPubkey(_) => { + Err("Tweaked x-only pubkey is not valid for legacy address".into()) + }, } } - fn get_segwit_script_type(&self, build_option: &AddressBuilderOption) -> AddressScriptType { - match build_option { - AddressBuilderOption::PubkeyHash(_) | AddressBuilderOption::FromPubKey(_) => AddressScriptType::P2WPKH, + fn get_segwit_script_type(&self, build_option: &AddressBuilderOption) -> Result { + let script_type = match build_option { + AddressBuilderOption::PubkeyHash(_) => AddressScriptType::P2WPKH, AddressBuilderOption::ScriptHash(_) => AddressScriptType::P2WSH, - } + AddressBuilderOption::TweakedXOnlyPubkey(_) => AddressScriptType::P2TR, + AddressBuilderOption::FromPubKey(_) => match self.addr_format { + AddressFormat::Segwit { version: 0 } => AddressScriptType::P2WPKH, + AddressFormat::Segwit { version: 1 } => AddressScriptType::P2TR, + AddressFormat::Segwit { version } => { + return Err(format!( + "unsupported segwit version {version} for FromPubKey build option" + )) + }, + AddressFormat::Standard | AddressFormat::CashAddress { .. } => { + return Err("The address format is wrongfully assumed to be segwit?".into()) + }, + }, + }; + Ok(script_type) } - fn get_hash(&self, build_option: &AddressBuilderOption) -> AddressHashEnum { - match build_option { + fn get_locking_destination(&self, build_option: &AddressBuilderOption) -> Result { + let locking_destination = match build_option { AddressBuilderOption::PubkeyHash(hash) => hash.clone(), AddressBuilderOption::ScriptHash(hash) => hash.clone(), - AddressBuilderOption::FromPubKey(pubkey) => AddressHashEnum::AddressHash(pubkey.address_hash()), - } + AddressBuilderOption::FromPubKey(pubkey) => match self.addr_format { + // For legacy, segwit v0 and cashaddr use address hash (dhash160). + AddressFormat::Standard | AddressFormat::Segwit { version: 0 } | AddressFormat::CashAddress { .. } => { + LockingDestination::AddressHash(pubkey.address_hash()) + }, + // For segwit v1 (taproot) use the x coordinate of the tweaked pubkey. + #[cfg(not(target_arch = "wasm32"))] + AddressFormat::Segwit { version: 1 } => { + let public_key = PublicKey::from_slice(pubkey).map_err(|e| e.to_string())?; + let (x_only_pub, _) = public_key.x_only_public_key(); + let (tweaked_pub, _) = x_only_pub.tap_tweak(&Secp256k1::new(), None); + LockingDestination::TweakedXOnlyPubkey(tweaked_pub.serialize().into()) + }, + // TODO: remove this match arm and the non-wasm cfg in the arm above to enable taproot support + // for wasm once https://github.com/KomodoPlatform/komodo-defi-framework/pull/2623 is merged. + #[cfg(target_arch = "wasm32")] + AddressFormat::Segwit { version: 1 } => { + return Err("taproot address extraction is not yet supported in wasm".into()) + }, + _ => return Err("Don't know how to get address hash/pubkey of advanced segwit format!".to_owned()), + }, + AddressBuilderOption::TweakedXOnlyPubkey(tweaked_x_only_pubkey) => tweaked_x_only_pubkey.clone(), + }; + Ok(locking_destination) } fn get_pubkey(&self, build_option: &AddressBuilderOption) -> Option { @@ -152,15 +213,49 @@ impl AddressBuilder { Ok(()) } + fn check_legacy_hash(&self, build_option: &AddressBuilderOption) -> Result<(), String> { + let is_hash_valid = match build_option { + AddressBuilderOption::PubkeyHash(hash) => hash.is_address_or_script_hash(), + AddressBuilderOption::ScriptHash(hash) => hash.is_address_or_script_hash(), + AddressBuilderOption::FromPubKey(_) => true, + AddressBuilderOption::TweakedXOnlyPubkey(_) => false, + }; + if !is_hash_valid { + return Err("invalid hash for legacy address".to_owned()); + } + Ok(()) + } + fn check_segwit_hash(&self, build_option: &AddressBuilderOption) -> Result<(), String> { let is_hash_valid = match build_option { - AddressBuilderOption::PubkeyHash(hash) => hash.is_address_hash(), + AddressBuilderOption::PubkeyHash(hash) => hash.is_address_or_script_hash(), AddressBuilderOption::ScriptHash(hash) => hash.is_witness_script_hash(), AddressBuilderOption::FromPubKey(_) => true, + AddressBuilderOption::TweakedXOnlyPubkey(pubkey) => pubkey.is_tweaked_xonly_pubkey(), }; if !is_hash_valid { return Err("invalid hash for segwit address".to_owned()); } Ok(()) } + + fn check_segwit_version(&self, build_option: &AddressBuilderOption) -> Result<(), String> { + match self.addr_format { + AddressFormat::Segwit { version: 0 } => match build_option { + AddressBuilderOption::PubkeyHash(_) + | AddressBuilderOption::ScriptHash(_) + | AddressBuilderOption::FromPubKey(_) => Ok(()), + AddressBuilderOption::TweakedXOnlyPubkey(_) => { + Err("Tweaked x-only pubkey is not valid for segwit v0 address".to_owned()) + }, + }, + AddressFormat::Segwit { version: 1 } => match build_option { + AddressBuilderOption::FromPubKey(_) | AddressBuilderOption::TweakedXOnlyPubkey(_) => Ok(()), + AddressBuilderOption::PubkeyHash(_) | AddressBuilderOption::ScriptHash(_) => { + Err("Pubkey/Script-Hash build options are not supported for segwit v1 address".to_owned()) + }, + }, + _ => Err("only segwit v0 and v1 are supported".to_owned()), + } + } } diff --git a/mm2src/mm2_bitcoin/keys/src/legacyaddress.rs b/mm2src/mm2_bitcoin/keys/src/legacyaddress.rs index d4da2243d2..d2a7408883 100644 --- a/mm2src/mm2_bitcoin/keys/src/legacyaddress.rs +++ b/mm2src/mm2_bitcoin/keys/src/legacyaddress.rs @@ -3,7 +3,7 @@ use std::{convert::TryInto, fmt}; use crypto::{checksum, ChecksumType}; use std::ops::Deref; -use {AddressHashEnum, AddressPrefix, DisplayLayout}; +use {AddressPrefix, DisplayLayout, LockingDestination}; use crate::{address::detect_checksum, Error}; @@ -101,11 +101,11 @@ impl fmt::Display for LegacyAddress { } impl LegacyAddress { - pub fn new(hash: &AddressHashEnum, prefix: AddressPrefix, checksum_type: ChecksumType) -> LegacyAddress { + pub fn new(address_hash: &LockingDestination, prefix: AddressPrefix, checksum_type: ChecksumType) -> LegacyAddress { LegacyAddress { prefix, checksum_type, - hash: hash.to_vec(), + hash: address_hash.to_vec(), } } } diff --git a/mm2src/mm2_bitcoin/keys/src/lib.rs b/mm2src/mm2_bitcoin/keys/src/lib.rs index ac087c480e..9835f4f6d4 100644 --- a/mm2src/mm2_bitcoin/keys/src/lib.rs +++ b/mm2src/mm2_bitcoin/keys/src/lib.rs @@ -5,6 +5,8 @@ #![allow(unused_assignments)] extern crate bech32; +#[cfg(not(target_arch = "wasm32"))] +extern crate bitcoin; extern crate bitcrypto as crypto; extern crate bs58; extern crate derive_more; @@ -56,57 +58,70 @@ pub type Secret = H256; pub type Message = H256; #[derive(Clone, Debug, Eq, Hash, PartialEq)] -pub enum AddressHashEnum { +pub enum LockingDestination { /// 20 bytes long hash derived from public `ripemd160(sha256(public/script))` used in P2PKH, P2SH, P2WPKH AddressHash(H160), /// 32 bytes long hash derived from script `sha256(script)` used in P2WSH WitnessScriptHash(H256), + /// 32 byte long tweaked x-only pubkey used in P2TR + TweakedXOnlyPubkey(H256), } -impl AddressHashEnum { +impl LockingDestination { pub fn default_address_hash() -> Self { - AddressHashEnum::AddressHash(H160::default()) + LockingDestination::AddressHash(H160::default()) } pub fn default_witness_script_hash() -> Self { - AddressHashEnum::WitnessScriptHash(H256::default()) + LockingDestination::WitnessScriptHash(H256::default()) + } + + pub fn default_tweaked_xonly_pubkey() -> Self { + LockingDestination::TweakedXOnlyPubkey(H256::default()) } pub fn copy_from_slice(&mut self, src: &[u8]) { match self { - AddressHashEnum::AddressHash(h) => h.copy_from_slice(src), - AddressHashEnum::WitnessScriptHash(s) => s.copy_from_slice(src), + LockingDestination::AddressHash(h) => h.copy_from_slice(src), + LockingDestination::WitnessScriptHash(s) => s.copy_from_slice(src), + LockingDestination::TweakedXOnlyPubkey(p) => p.copy_from_slice(src), } } pub fn to_vec(&self) -> Vec { match self { - AddressHashEnum::AddressHash(h) => h.to_vec(), - AddressHashEnum::WitnessScriptHash(s) => s.to_vec(), + LockingDestination::AddressHash(h) => h.to_vec(), + LockingDestination::WitnessScriptHash(s) => s.to_vec(), + LockingDestination::TweakedXOnlyPubkey(p) => p.to_vec(), } } - pub fn is_address_hash(&self) -> bool { - matches!(*self, AddressHashEnum::AddressHash(_)) + pub fn is_address_or_script_hash(&self) -> bool { + matches!(*self, LockingDestination::AddressHash(_)) } pub fn is_witness_script_hash(&self) -> bool { - matches!(*self, AddressHashEnum::WitnessScriptHash(_)) + matches!(*self, LockingDestination::WitnessScriptHash(_)) + } + + pub fn is_tweaked_xonly_pubkey(&self) -> bool { + matches!(*self, LockingDestination::TweakedXOnlyPubkey(_)) } } -impl fmt::Display for AddressHashEnum { +impl fmt::Display for LockingDestination { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - AddressHashEnum::AddressHash(h) => f.write_str(&h.to_string()), - AddressHashEnum::WitnessScriptHash(s) => f.write_str(&s.to_string()), + LockingDestination::AddressHash(h) => f.write_str(&h.to_string()), + LockingDestination::WitnessScriptHash(s) => f.write_str(&s.to_string()), + LockingDestination::TweakedXOnlyPubkey(p) => f.write_str(&p.to_string()), } } } -impl From for AddressHashEnum { +impl From for LockingDestination { fn from(hash: H160) -> Self { - AddressHashEnum::AddressHash(hash) + LockingDestination::AddressHash(hash) } } diff --git a/mm2src/mm2_bitcoin/keys/src/segwitaddress.rs b/mm2src/mm2_bitcoin/keys/src/segwitaddress.rs index 500c7e4df6..ad53ac1239 100644 --- a/mm2src/mm2_bitcoin/keys/src/segwitaddress.rs +++ b/mm2src/mm2_bitcoin/keys/src/segwitaddress.rs @@ -2,7 +2,7 @@ use std::fmt; use std::str::FromStr; use bech32; -use AddressHashEnum; +use LockingDestination; /// Address error. #[derive(Debug, PartialEq)] @@ -59,9 +59,12 @@ impl From for Error { /// The different types of segwit addresses. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum SegwitAddrType { + /// pay-to-witness-public-key-hash P2wpkh, /// pay-to-witness-script-hash P2wsh, + /// pay-to-taproot + P2tr, } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -76,24 +79,30 @@ pub struct SegwitAddress { } impl SegwitAddress { - pub fn new(hash: &AddressHashEnum, hrp: String) -> SegwitAddress { - SegwitAddress { - hrp, - version: bech32::u5::try_from_u8(0).expect("0<32"), - program: hash.to_vec(), + pub fn new(program: &LockingDestination, hrp: String, version: u8) -> Result { + if version > 16 { + return Err(Error::InvalidWitnessVersion(version)); } + Ok(SegwitAddress { + hrp, + version: bech32::u5::try_from_u8(version).expect("version must be < 16, thus also < 32"), + program: program.to_vec(), + }) + } + + pub fn version_as_u8(&self) -> u8 { + self.version.to_u8() } /// Get the address type of the address. /// None if unknown or non-standard. pub fn address_type(&self) -> Option { // BIP-141 p2wpkh or p2wsh addresses. - match self.version.to_u8() { - 0 => match self.program.len() { - 20 => Some(SegwitAddrType::P2wpkh), - 32 => Some(SegwitAddrType::P2wsh), - _ => None, - }, + match self.version_as_u8() { + 0 if self.program.len() == 20 => Some(SegwitAddrType::P2wpkh), + 0 if self.program.len() == 32 => Some(SegwitAddrType::P2wsh), + 1 if self.program.len() == 32 => Some(SegwitAddrType::P2tr), + // Future versions or non-standard program sizes. _ => None, } } @@ -130,7 +139,14 @@ impl fmt::Display for SegwitAddress { } else { fmt as &mut dyn fmt::Write }; - let mut bech32_writer = bech32::Bech32Writer::new(self.hrp.as_str(), bech32::Variant::Bech32, writer)?; + let bech32_version = match self.version.to_u8() { + 0 => bech32::Variant::Bech32, + 1 => bech32::Variant::Bech32m, + // Ideally, all v1+ segwit addresses should be formatted using Bech32m. + // But let's error on such attempts unless we explicitly support higher versions. + _ => return Err(fmt::Error), + }; + let mut bech32_writer = bech32::Bech32Writer::new(self.hrp.as_str(), bech32_version, writer)?; bech32::WriteBase32::write_u5(&mut bech32_writer, self.version)?; bech32::ToBase32::write_base32(&self.program, &mut bech32_writer) } @@ -147,10 +163,11 @@ impl FromStr for SegwitAddress { if payload.is_empty() { return Err(Error::EmptyBech32Payload); } + + // We perform this match to trigger a compilation error if a new variant gets added that we didn't handle yet. match variant { bech32::Variant::Bech32 => (), - bech32::Variant::Bech32m => return Err(Error::UnsupportedAddressVariant("Bech32m".into())), - // Important: If a new variant is added we should return an error until we support the new variant + bech32::Variant::Bech32m => (), } // Get the script version and program (converted from 5-bit to 8-bit) @@ -163,22 +180,39 @@ impl FromStr for SegwitAddress { if version.to_u8() > 16 { return Err(Error::InvalidWitnessVersion(version.to_u8())); } + + // Only support segwit v0 and v1. Note that we are relaxing this check in tests + // so to test the detection of other errors (invalid length, wrong encoding used, etc...) + #[cfg(not(test))] + if ![0, 1].contains(&version.to_u8()) { + return Err(Error::UnsupportedWitnessVersion(version.to_u8())); + } + if program.len() < 2 || program.len() > 40 { return Err(Error::InvalidWitnessProgramLength(program.len())); } - // Specific segwit v0 check. - if version.to_u8() != 0 { - return Err(Error::UnsupportedWitnessVersion(version.to_u8())); + if version.to_u8() == 0 { + // Bech32 length check for segwit v0 (later versions use bech32m which isn't vulnerable to this problem). + // Important: we should be careful when using new program lengths since a valid Bech32 string can be modified according to + // the below 2 links while still having a valid checksum. + // https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki#motivation + // https://github.com/sipa/bech32/issues/51 + if program.len() != 20 && program.len() != 32 { + return Err(Error::InvalidSegwitV0ProgramLength(program.len())); + } + if variant == bech32::Variant::Bech32m { + return Err(Error::UnsupportedAddressVariant( + "Bech32m is not supported for witness version 0. Bech32 should be used instead.".into(), + )); + } } - // Bech32 length check. - // Important: we should be careful when using new program lengths since a valid Bech32 string can be modified according to - // the below 2 links while still having a valid checksum. - // https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki#motivation - // https://github.com/sipa/bech32/issues/51 - if program.len() != 20 && program.len() != 32 { - return Err(Error::InvalidSegwitV0ProgramLength(program.len())); + if version.to_u8() != 0 && variant == bech32::Variant::Bech32 { + return Err(Error::UnsupportedAddressVariant(format!( + "Bech32 is not supported for witness version {}. Bech32m should be used instead.", + version.to_u8() + ))); } Ok(SegwitAddress { hrp, version, program }) @@ -190,6 +224,7 @@ mod tests { use super::*; use crypto::sha256; use hex::ToHex; + use primitives::hash::H256; use Public; fn hex_to_bytes(s: &str) -> Option> { @@ -211,7 +246,7 @@ mod tests { let public_key = Public::from_slice(&bytes).unwrap(); let hash = public_key.address_hash(); let hrp = "bc"; - let addr = SegwitAddress::new(&AddressHashEnum::AddressHash(hash), hrp.to_string()); + let addr = SegwitAddress::new(&LockingDestination::AddressHash(hash), hrp.to_string(), 0).unwrap(); assert_eq!(&addr.to_string(), "bc1qvzvkjn4q3nszqxrv3nraga2r822xjty3ykvkuw"); assert_eq!(addr.address_type(), Some(SegwitAddrType::P2wpkh)); } @@ -222,7 +257,7 @@ mod tests { let bytes = hex_to_bytes(script).unwrap(); let hash = sha256(&bytes); let hrp = "bc"; - let addr = SegwitAddress::new(&AddressHashEnum::WitnessScriptHash(hash), hrp.to_string()); + let addr = SegwitAddress::new(&LockingDestination::WitnessScriptHash(hash), hrp.to_string(), 0).unwrap(); assert_eq!( &addr.to_string(), "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3" @@ -230,9 +265,24 @@ mod tests { assert_eq!(addr.address_type(), Some(SegwitAddrType::P2wsh)); } + #[test] + fn test_p2tr_address() { + let x_only_pub = "d5e89e0b73605abba690ba5e00484e279d006283bed0055a0530fb6a8c9adac7"; + let bytes = hex_to_bytes(x_only_pub).unwrap(); + let x_only_pub = H256::from_slice(&bytes).unwrap(); + let hrp = "tb"; + let addr = SegwitAddress::new(&LockingDestination::TweakedXOnlyPubkey(x_only_pub), hrp.to_string(), 1).unwrap(); + assert_eq!( + &addr.to_string(), + "tb1p6h5fuzmnvpdthf5shf0qqjzwy7wsqc5rhmgq2ks9xrak4ry6mtrscsqvzp" + ); + assert_eq!(addr.address_type(), Some(SegwitAddrType::P2tr)); + } + #[test] // https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#test-vectors fn test_valid_segwit() { + // p2wpkh let addr = "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4"; let segwit_addr = SegwitAddress::from_str(addr).unwrap(); assert_eq!(0, segwit_addr.version.to_u8()); @@ -240,7 +290,7 @@ mod tests { "751e76e8199196d454941c45d1b3a323f1433bd6", segwit_addr.program.to_hex::() ); - + // p2wsh let addr = "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7"; let segwit_addr = SegwitAddress::from_str(addr).unwrap(); assert_eq!(0, segwit_addr.version.to_u8()); @@ -248,7 +298,7 @@ mod tests { "1863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262", segwit_addr.program.to_hex::() ); - + // p2wsh let addr = "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy"; let segwit_addr = SegwitAddress::from_str(addr).unwrap(); assert_eq!(0, segwit_addr.version.to_u8()); @@ -256,10 +306,19 @@ mod tests { "000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433", segwit_addr.program.to_hex::() ); + // p2tr + let addr = "tb1p6h5fuzmnvpdthf5shf0qqjzwy7wsqc5rhmgq2ks9xrak4ry6mtrscsqvzp"; + let segwit_addr = SegwitAddress::from_str(addr).unwrap(); + assert_eq!(1, segwit_addr.version.to_u8()); + assert_eq!( + "d5e89e0b73605abba690ba5e00484e279d006283bed0055a0530fb6a8c9adac7", + segwit_addr.program.to_hex::() + ); } #[test] // https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#test-vectors + // https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki#test-vectors fn test_invalid_segwit_addresses() { // Invalid checksum let invalid_address = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5"; @@ -271,11 +330,16 @@ mod tests { let err = SegwitAddress::from_str(invalid_address).unwrap_err(); assert_eq!(err, Error::InvalidWitnessVersion(17)); - // Invalid program length + // Invalid program length (bech32) let invalid_address = "bc1rw5uspcuh"; let err = SegwitAddress::from_str(invalid_address).unwrap_err(); assert_eq!(err, Error::InvalidWitnessProgramLength(1)); + // Invalid program length (bech32m) + let invalid_address = "bc1pw5dgrnzv"; + let err = SegwitAddress::from_str(invalid_address).unwrap_err(); + assert_eq!(err, Error::InvalidWitnessProgramLength(1)); + // Invalid program length let invalid_address = "bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90"; let err = SegwitAddress::from_str(invalid_address).unwrap_err(); @@ -308,21 +372,43 @@ mod tests { // Version 1 shouldn't be used with bech32 variant although the below address is given as valid in BIP173 // https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki#abstract - // If the version byte is 1 to 16, no further interpretation of the witness program or witness stack happens, - // and there is no size restriction for the witness stack. These versions are reserved for future extensions - // https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#witness-program let invalid_address = "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx"; let err = SegwitAddress::from_str(invalid_address).unwrap_err(); - assert_eq!(err, Error::UnsupportedWitnessVersion(1)); + assert_eq!( + err, + Error::UnsupportedAddressVariant( + "Bech32 is not supported for witness version 1. Bech32m should be used instead.".into() + ) + ); + + // Invalid checksum for version 0 (bech32m instead of bech32) + let invalid_address = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kemeawh"; + let err = SegwitAddress::from_str(invalid_address).unwrap_err(); + assert_eq!( + err, + Error::UnsupportedAddressVariant( + "Bech32m is not supported for witness version 0. Bech32 should be used instead.".into() + ) + ); - // Version 16 shouldn't be used with bech32 variant although the below address is given as valid in BIP173 + // Version 16 shouldn't be used with bech32 let invalid_address = "BC1SW50QA3JX3S"; let err = SegwitAddress::from_str(invalid_address).unwrap_err(); - assert_eq!(err, Error::UnsupportedWitnessVersion(16)); + assert_eq!( + err, + Error::UnsupportedAddressVariant( + "Bech32 is not supported for witness version 16. Bech32m should be used instead.".into() + ) + ); - // Version 2 shouldn't be used with bech32 variant although the below address is given as valid in BIP173 + // Version 2 shouldn't be used with bech32 let invalid_address = "bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj"; let err = SegwitAddress::from_str(invalid_address).unwrap_err(); - assert_eq!(err, Error::UnsupportedWitnessVersion(2)); + assert_eq!( + err, + Error::UnsupportedAddressVariant( + "Bech32 is not supported for witness version 2. Bech32m should be used instead.".into() + ) + ); } } diff --git a/mm2src/mm2_bitcoin/script/Cargo.toml b/mm2src/mm2_bitcoin/script/Cargo.toml index 61cfd06e2b..88377b7e9e 100644 --- a/mm2src/mm2_bitcoin/script/Cargo.toml +++ b/mm2src/mm2_bitcoin/script/Cargo.toml @@ -16,3 +16,6 @@ serde.workspace = true serialization = { path = "../serialization" } log.workspace = true blake2b_simd.workspace = true + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +bitcoin.workspace = true diff --git a/mm2src/mm2_bitcoin/script/src/builder.rs b/mm2src/mm2_bitcoin/script/src/builder.rs index 537344f036..480730a0e3 100644 --- a/mm2src/mm2_bitcoin/script/src/builder.rs +++ b/mm2src/mm2_bitcoin/script/src/builder.rs @@ -1,7 +1,7 @@ //! Script builder use bytes::Bytes; -use keys::{AddressHashEnum, Error, Public}; +use keys::{Error, LockingDestination, Public}; use {Num, Opcode, Script}; /// Script builder @@ -12,11 +12,11 @@ pub struct Builder { impl Builder { /// Builds p2pkh script pubkey - pub fn build_p2pkh(address: &AddressHashEnum) -> Script { + pub fn build_p2pkh(address_hash: &LockingDestination) -> Script { Builder::default() .push_opcode(Opcode::OP_DUP) .push_opcode(Opcode::OP_HASH160) - .push_data(&address.to_vec()) + .push_data(&address_hash.to_vec()) .push_opcode(Opcode::OP_EQUALVERIFY) .push_opcode(Opcode::OP_CHECKSIG) .into_script() @@ -31,33 +31,44 @@ impl Builder { } /// Builds p2sh script pubkey - pub fn build_p2sh(address: &AddressHashEnum) -> Script { + pub fn build_p2sh(script_hash: &LockingDestination) -> Script { Builder::default() .push_opcode(Opcode::OP_HASH160) - .push_data(&address.to_vec()) + .push_data(&script_hash.to_vec()) .push_opcode(Opcode::OP_EQUAL) .into_script() } /// Builds p2wpkh script pubkey - pub fn build_p2wpkh(address_hash: &AddressHashEnum) -> Result { + pub fn build_p2wpkh(address_hash: &LockingDestination) -> Result { match address_hash { - AddressHashEnum::AddressHash(wpkh_hash) => Ok(Builder::default() + LockingDestination::AddressHash(wpkh_hash) => Ok(Builder::default() .push_opcode(Opcode::OP_0) .push_data(wpkh_hash.as_ref()) .into_script()), - AddressHashEnum::WitnessScriptHash(_) => Err(Error::WitnessHashMismatched), + _ => Err(Error::WitnessHashMismatched), } } /// Builds p2wsh script pubkey - pub fn build_p2wsh(address_hash: &AddressHashEnum) -> Result { - match address_hash { - AddressHashEnum::WitnessScriptHash(wsh_hash) => Ok(Builder::default() + pub fn build_p2wsh(witness_script_hash: &LockingDestination) -> Result { + match witness_script_hash { + LockingDestination::WitnessScriptHash(wsh_hash) => Ok(Builder::default() .push_opcode(Opcode::OP_0) .push_data(wsh_hash.as_ref()) .into_script()), - AddressHashEnum::AddressHash(_) => Err(Error::WitnessHashMismatched), + _ => Err(Error::WitnessHashMismatched), + } + } + + /// Builds p2tr script pubkey + pub fn build_p2tr(x_only_pubkey: &LockingDestination) -> Result { + match x_only_pubkey { + LockingDestination::TweakedXOnlyPubkey(x_only_pubkey) => Ok(Builder::default() + .push_opcode(Opcode::OP_1) + .push_data(x_only_pubkey.as_ref()) + .into_script()), + _ => Err(Error::WitnessHashMismatched), } } diff --git a/mm2src/mm2_bitcoin/script/src/lib.rs b/mm2src/mm2_bitcoin/script/src/lib.rs index 4e43f8f474..a89ab53e85 100644 --- a/mm2src/mm2_bitcoin/script/src/lib.rs +++ b/mm2src/mm2_bitcoin/script/src/lib.rs @@ -1,3 +1,5 @@ +#[cfg(not(target_arch = "wasm32"))] +extern crate bitcoin as ext_bitcoin; extern crate bitcrypto as crypto; extern crate blake2b_simd; extern crate chain; diff --git a/mm2src/mm2_bitcoin/script/src/script.rs b/mm2src/mm2_bitcoin/script/src/script.rs index 5f0c81e84d..99119ce10e 100644 --- a/mm2src/mm2_bitcoin/script/src/script.rs +++ b/mm2src/mm2_bitcoin/script/src/script.rs @@ -1,7 +1,7 @@ //! Serialized script, used inside transaction inputs and outputs. use bytes::Bytes; -use keys::{self, AddressHashEnum, Public}; +use keys::{self, LockingDestination, Public}; use std::convert::TryInto; use std::{fmt, ops}; use {Error, Opcode}; @@ -36,40 +36,48 @@ pub enum ScriptType { pub struct ScriptAddress { /// The type of the address. pub kind: keys::AddressScriptType, - /// Public key hash. - pub hash: AddressHashEnum, + /// The locking destination (address hash, script hash, witness script hash, or tweaked x-only pubkey). + pub destination: LockingDestination, } impl ScriptAddress { /// Creates P2PKH-type ScriptAddress - pub fn new_p2pkh(hash: AddressHashEnum) -> Self { + pub fn new_p2pkh(address_hash: LockingDestination) -> Self { ScriptAddress { kind: keys::AddressScriptType::P2PKH, - hash, + destination: address_hash, } } /// Creates P2SH-type ScriptAddress - pub fn new_p2sh(hash: AddressHashEnum) -> Self { + pub fn new_p2sh(script_hash: LockingDestination) -> Self { ScriptAddress { kind: keys::AddressScriptType::P2SH, - hash, + destination: script_hash, } } /// Creates P2WPKH-type ScriptAddress - pub fn new_p2wpkh(hash: AddressHashEnum) -> Self { + pub fn new_p2wpkh(address_hash: LockingDestination) -> Self { ScriptAddress { kind: keys::AddressScriptType::P2WPKH, - hash, + destination: address_hash, } } /// Creates P2WSH-type ScriptAddress - pub fn new_p2wsh(hash: AddressHashEnum) -> Self { + pub fn new_p2wsh(witness_script_hash: LockingDestination) -> Self { ScriptAddress { kind: keys::AddressScriptType::P2WSH, - hash, + destination: witness_script_hash, + } + } + + /// Creates P2TR-type ScriptAddress + pub fn new_p2tr(tweaked_x_only_pubkey: LockingDestination) -> Self { + ScriptAddress { + kind: keys::AddressScriptType::P2TR, + destination: tweaked_x_only_pubkey, } } } @@ -161,6 +169,11 @@ impl Script { self.data.len() == 22 && self.data[0] == Opcode::OP_0 as u8 && self.data[1] == Opcode::OP_PUSHBYTES_20 as u8 } + /// Extra-fast test for pay-to-taproot scripts. + pub fn is_pay_to_taproot(&self) -> bool { + self.data.len() == 34 && self.data[0] == Opcode::OP_1 as u8 && self.data[1] == Opcode::OP_PUSHBYTES_32 as u8 + } + /// Parse witness program. Returns Some(witness program version, code) or None if not a witness program. pub fn parse_witness_program(&self) -> Option<(u8, &[u8])> { if self.data.len() < 4 || self.data.len() > 42 || self.data.len() != self.data[1] as usize + 2 { @@ -370,8 +383,11 @@ impl Script { ScriptType::WitnessKey } else if self.is_pay_to_witness_script_hash() { ScriptType::WitnessScript - // TODO add Call - } else { + } else if self.is_pay_to_taproot() { + ScriptType::Taproot + } + // TODO add Call + else { ScriptType::NonStandard } } @@ -440,7 +456,7 @@ impl Script { _ => unreachable!(), // because we are relying on script_type() checks here }) .map(|public| { - vec![ScriptAddress::new_p2pkh(AddressHashEnum::AddressHash( + vec![ScriptAddress::new_p2pkh(LockingDestination::AddressHash( public.address_hash(), ))] }) @@ -448,14 +464,14 @@ impl Script { ScriptType::PubKeyHash => { let bytes = self.data.get(3..23).ok_or(keys::Error::InvalidAddress)?; let hash: [u8; 20] = bytes.try_into().map_err(|_| keys::Error::InvalidAddress)?; - let address_hash = AddressHashEnum::AddressHash(hash.into()); + let address_hash = LockingDestination::AddressHash(hash.into()); Ok(vec![ScriptAddress::new_p2pkh(address_hash)]) }, ScriptType::ScriptHash => { let bytes = self.data.get(2..22).ok_or(keys::Error::InvalidAddress)?; let hash: [u8; 20] = bytes.try_into().map_err(|_| keys::Error::InvalidAddress)?; - let address_hash = AddressHashEnum::AddressHash(hash.into()); - Ok(vec![ScriptAddress::new_p2sh(address_hash)]) + let script_hash = LockingDestination::AddressHash(hash.into()); + Ok(vec![ScriptAddress::new_p2sh(script_hash)]) }, ScriptType::Multisig => { let mut addresses: Vec = Vec::new(); @@ -468,7 +484,7 @@ impl Script { .data .expect("this method depends on previous check in script_type()"); let address = Public::from_slice(data)?.address_hash(); - addresses.push(ScriptAddress::new_p2pkh(AddressHashEnum::AddressHash(address))); + addresses.push(ScriptAddress::new_p2pkh(LockingDestination::AddressHash(address))); pc += instruction.step; } Ok(addresses) @@ -477,17 +493,20 @@ impl Script { ScriptType::WitnessScript => { let bytes = self.data.get(2..34).ok_or(keys::Error::InvalidAddress)?; let hash: [u8; 32] = bytes.try_into().map_err(|_| keys::Error::InvalidAddress)?; - let address_hash = AddressHashEnum::WitnessScriptHash(hash.into()); - Ok(vec![ScriptAddress::new_p2wsh(address_hash)]) + let witness_script_hash = LockingDestination::WitnessScriptHash(hash.into()); + Ok(vec![ScriptAddress::new_p2wsh(witness_script_hash)]) }, ScriptType::WitnessKey => { let bytes = self.data.get(2..22).ok_or(keys::Error::InvalidAddress)?; let hash: [u8; 20] = bytes.try_into().map_err(|_| keys::Error::InvalidAddress)?; - let address_hash = AddressHashEnum::AddressHash(hash.into()); + let address_hash = LockingDestination::AddressHash(hash.into()); Ok(vec![ScriptAddress::new_p2wpkh(address_hash)]) }, ScriptType::Taproot => { - Ok(vec![]) // TODO + let bytes = self.data.get(2..34).ok_or(keys::Error::InvalidAddress)?; + let tweaked_pub: [u8; 32] = bytes.try_into().map_err(|_| keys::Error::InvalidAddress)?; + let x_only_pubkey = LockingDestination::TweakedXOnlyPubkey(tweaked_pub.into()); + Ok(vec![ScriptAddress::new_p2tr(x_only_pubkey)]) }, ScriptType::CallSender => { Ok(vec![]) // TODO @@ -850,7 +869,7 @@ OP_ADD fn test_extract_destinations_pub_key_hash() { let address = Address::from_legacyaddress("13NMTpfNVVJQTNH4spP4UeqBGqLdqDo27S", &BTC_PREFIXES) .unwrap() - .hash() + .locking_destination() .clone(); let script = Builder::build_p2pkh(&address); assert_eq!(script.script_type(), ScriptType::PubKeyHash); @@ -864,7 +883,7 @@ OP_ADD fn test_extract_destinations_script_hash() { let address = Address::from_legacyaddress("13NMTpfNVVJQTNH4spP4UeqBGqLdqDo27S", &BTC_PREFIXES) .unwrap() - .hash() + .locking_destination() .clone(); let script = Builder::build_p2sh(&address); assert_eq!(script.script_type(), ScriptType::ScriptHash); @@ -879,7 +898,7 @@ OP_ADD let address_hash = Address::from_segwitaddress("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", ChecksumType::DSHA256) .unwrap() - .hash() + .locking_destination() .clone(); let script = Builder::build_p2wpkh(&address_hash).expect("build p2wpkh ok"); assert_eq!(script.script_type(), ScriptType::WitnessKey); @@ -896,7 +915,7 @@ OP_ADD ChecksumType::DSHA256, ) .unwrap() - .hash() + .locking_destination() .clone(); let script = Builder::build_p2wsh(&address_hash).expect("build p2wsh ok"); assert_eq!(script.script_type(), ScriptType::WitnessScript); diff --git a/mm2src/mm2_bitcoin/script/src/sign.rs b/mm2src/mm2_bitcoin/script/src/sign.rs index b4a3399a7e..20baa6431f 100644 --- a/mm2src/mm2_bitcoin/script/src/sign.rs +++ b/mm2src/mm2_bitcoin/script/src/sign.rs @@ -13,6 +13,9 @@ use serde::Deserialize; use std::convert::TryInto; use {Builder, Script}; +#[cfg(not(target_arch = "wasm32"))] +use ext_bitcoin; + const ZCASH_PREVOUTS_HASH_PERSONALIZATION: &[u8] = b"ZcashPrevoutHash"; const ZCASH_SEQUENCE_HASH_PERSONALIZATION: &[u8] = b"ZcashSequencHash"; const ZCASH_OUTPUTS_HASH_PERSONALIZATION: &[u8] = b"ZcashOutputsHash"; @@ -25,8 +28,12 @@ const ZCASH_SIG_HASH_PERSONALIZATION: &[u8] = b"ZcashSigHash"; pub enum SignatureVersion { #[serde(rename = "base")] Base, - #[serde(rename = "witness_v0")] - WitnessV0, + // Disallow deserializing the witness variant. This variant is only used internally for marking + // and shouldn't be supplied from an outside source (i.e. coins config). + // We can already internally detect that a signature should use witness format based + // the address format of the coin. + #[serde(skip_deserializing)] + Witness, #[serde(rename = "fork_id")] ForkId, } @@ -50,29 +57,11 @@ impl From for u32 { pub struct Sighash { pub base: SighashBase, pub anyone_can_pay: bool, - pub fork_id: bool, -} - -impl From for u32 { - fn from(s: Sighash) -> Self { - let base = s.base as u32; - let base = if s.anyone_can_pay { base | 0x80 } else { base }; - - if s.fork_id { - base | 0x40 - } else { - base - } - } } impl Sighash { - pub fn new(base: SighashBase, anyone_can_pay: bool, fork_id: bool) -> Self { - Sighash { - base, - anyone_can_pay, - fork_id, - } + pub fn new(base: SighashBase, anyone_can_pay: bool) -> Self { + Sighash { base, anyone_can_pay } } /// Used by SCRIPT_VERIFY_STRICTENC @@ -88,16 +77,15 @@ impl Sighash { } /// Creates Sighash from any u, even if is_defined() == false - pub fn from_u32(version: SignatureVersion, u: u32) -> Self { + pub fn from_u32(u: u32) -> Self { let anyone_can_pay = (u & 0x80) == 0x80; - let fork_id = version == SignatureVersion::ForkId && (u & 0x40) == 0x40; let base = match u & 0x1f { 2 => SighashBase::None, 3 => SighashBase::Single, _ => SighashBase::All, }; - Sighash::new(base, anyone_can_pay, fork_id) + Sighash::new(base, anyone_can_pay) } } @@ -121,6 +109,18 @@ impl From for UnsignedTransactionInput { } } +#[cfg(not(target_arch = "wasm32"))] +impl From for ext_bitcoin::TxIn { + fn from(i: UnsignedTransactionInput) -> Self { + ext_bitcoin::TxIn { + previous_output: i.previous_output.into(), + script_sig: ext_bitcoin::Script::default(), + sequence: ext_bitcoin::Sequence(i.sequence), + witness: ext_bitcoin::Witness::default(), + } + } +} + #[allow(clippy::upper_case_acronyms)] #[derive(Clone, Copy, Debug)] pub enum SignerHashAlgo { @@ -232,6 +232,18 @@ impl From for Transaction { } } +#[cfg(not(target_arch = "wasm32"))] +impl From for ext_bitcoin::Transaction { + fn from(tx: TransactionInputSigner) -> Self { + ext_bitcoin::Transaction { + version: tx.version, + lock_time: ext_bitcoin::PackedLockTime(tx.lock_time), + input: tx.inputs.into_iter().map(|i| i.into()).collect(), + output: tx.outputs.into_iter().map(|o| o.into()).collect(), + } + } +} + impl TransactionInputSigner { pub fn signature_hash( &self, @@ -241,15 +253,19 @@ impl TransactionInputSigner { sigversion: SignatureVersion, sighashtype: u32, ) -> H256 { - let sighash = Sighash::from_u32(sigversion, sighashtype); + let sighash = Sighash::from_u32(sighashtype); match sigversion { - SignatureVersion::ForkId if sighash.fork_id => { - self.signature_hash_fork_id(input_index, input_amount, script_pubkey, sighashtype, sighash) - }, - SignatureVersion::Base | SignatureVersion::ForkId => { - self.signature_hash_original(input_index, script_pubkey, sighashtype, sighash) + SignatureVersion::ForkId => { + // Make sure the `fork_id` bit (0x40) is set. + if (sighashtype & 0x40) != 0x40 { + return 1.into(); + } + // For a `fork_id` chain that has the `fork_id` bit (0x40) set in the sighash, + // we should use segwit v0 sighash. Examples of these chains are: BCH, XRG, XEC. + self.signature_hash_witness0(input_index, input_amount, script_pubkey, sighashtype, sighash) }, - SignatureVersion::WitnessV0 => { + SignatureVersion::Base => self.signature_hash_original(input_index, script_pubkey, sighashtype, sighash), + SignatureVersion::Witness => { self.signature_hash_witness0(input_index, input_amount, script_pubkey, sighashtype, sighash) }, } @@ -397,6 +413,14 @@ impl TransactionInputSigner { sighashtype: u32, sighash: Sighash, ) -> H256 { + if input_index >= self.inputs.len() { + return 1u8.into(); + } + + if sighash.base == SighashBase::Single && input_index >= self.outputs.len() { + return 1u8.into(); + } + let hash_prevouts = compute_hash_prevouts(sighash, &self.inputs); let hash_sequence = compute_hash_sequence(sighash, &self.inputs); let hash_outputs = compute_hash_outputs(sighash, input_index, &self.outputs); @@ -416,25 +440,6 @@ impl TransactionInputSigner { dhash256(&out) } - fn signature_hash_fork_id( - &self, - input_index: usize, - input_amount: u64, - script_pubkey: &Script, - sighashtype: u32, - sighash: Sighash, - ) -> H256 { - if input_index >= self.inputs.len() { - return 1u8.into(); - } - - if sighash.base == SighashBase::Single && input_index >= self.outputs.len() { - return 1u8.into(); - } - - self.signature_hash_witness0(input_index, input_amount, script_pubkey, sighashtype, sighash) - } - /// https://github.com/zcash/zips/blob/master/zip-0243.rst#notes /// This method doesn't cover all possible Sighash combinations so it doesn't fully match the /// specification, however I don't need other cases yet as BarterDEX marketmaker always uses @@ -638,7 +643,7 @@ mod tests { use hash::{H160, H256}; use keys::{ prefixes::{BTC_PREFIXES, T_BTC_PREFIXES}, - Address, AddressHashEnum, Private, + Address, LockingDestination, Private, }; use script::Script; use ser::deserialize; @@ -654,7 +659,7 @@ mod tests { H256::from_reversed_str("81b4c832d70cb56ff957589752eb4125a4cab78a25a8fc52d6a09e5bd4404d48"); let previous_output_index = 0; let to: Address = Address::from_legacyaddress("1KKKK6N21XKo48zWKuQKXdvSsCf95ibHFa", &BTC_PREFIXES).unwrap(); - assert!(to.hash().is_address_hash()); + assert!(to.locking_destination().is_address_or_script_hash()); let previous_output = "76a914df3bd30160e6c6145baaf2c88a8844c13a00d1d588ac".into(); let current_output: Bytes = "76a914c8e90996c7c6080ee06284600c684ed904d14c5c88ac".into(); let value = 91234; @@ -662,7 +667,7 @@ mod tests { // this is irrelevant let mut hash = H160::default(); - if let AddressHashEnum::AddressHash(h) = to.hash() { + if let LockingDestination::AddressHash(h) = to.locking_destination() { hash = *h; } assert_eq!(¤t_output[3..23], &*hash); @@ -714,7 +719,7 @@ mod tests { H256::from_reversed_str("0bc54ed426950f50bf2c2776034a03592e844757b42330eb908eb04492dad2c6"); let previous_output_index = 1; let to: Address = Address::from_legacyaddress("msj7SEQmH7pUCUx8YU6R87DrAHYzcABdzw", &T_BTC_PREFIXES).unwrap(); - assert!(to.hash().is_address_hash()); + assert!(to.locking_destination().is_address_or_script_hash()); let previous_output = "76a914df3bd30160e6c6145baaf2c88a8844c13a00d1d588ac".into(); let current_output: Bytes = "76a91485ee21a7f8cdd9034fb55004e0d8ed27db1c03c288ac".into(); let value = 100000000; @@ -773,7 +778,7 @@ mod tests { let script: Script = script.into(); let expected = H256::from_reversed_str(result); - let sighash = Sighash::from_u32(SignatureVersion::Base, hash_type as u32); + let sighash = Sighash::from_u32(hash_type as u32); let hash = signer.signature_hash_original(input_index, &script, hash_type as u32, sighash); assert_eq!(expected, hash); } @@ -813,7 +818,7 @@ mod tests { signer.inputs[0].amount = 50000000; signer.consensus_branch_id = 0x76b809bb; - let sig_hash = Sighash::from_u32(SignatureVersion::Base, 1); + let sig_hash = Sighash::from_u32(1); let hash = signer.signature_hash_overwintered( 0, &Script::from("1976a914507173527b4c3318a2aecd793bf1cfed705950cf88ac"), @@ -847,7 +852,7 @@ mod tests { signer.inputs[0].amount = 9924260; signer.consensus_branch_id = 0x76b809bb; - let sig_hash = Sighash::from_u32(SignatureVersion::Base, 1); + let sig_hash = Sighash::from_u32(1); let hash = signer.signature_hash_overwintered( 0, &Script::from("76a91405aab5342166f8594baf17a7d9bef5d56744332788ac"), @@ -881,7 +886,7 @@ mod tests { signer.inputs[0].amount = 100000000; signer.consensus_branch_id = 0x76b809bb; - let sig_hash = Sighash::from_u32(SignatureVersion::Base, 1); + let sig_hash = Sighash::from_u32(1); let hash = signer.signature_hash_overwintered( 0, &Script::from("6304e5928060b17521031c632dad67a611de77d9666cbc61e65957c7d7544c25e384f4e76de729e6a1bfac6782012088a914b78f0b837e2c710f8b28e59d06473d489e5315c88821037310a8fb9fd8f198a1a21db830252ad681fccda580ed4101f3f6bfb98b34fab5ac68"), diff --git a/mm2src/mm2_main/tests/docker_tests/helpers/slp.rs b/mm2src/mm2_main/tests/docker_tests/helpers/slp.rs index 9b79d25092..2c87751236 100644 --- a/mm2src/mm2_main/tests/docker_tests/helpers/slp.rs +++ b/mm2src/mm2_main/tests/docker_tests/helpers/slp.rs @@ -226,7 +226,7 @@ impl BchDockerOps { self.coin.as_ref().conf.address_prefixes.clone(), None, ) - .as_pkh_from_pk(*key_pair.public()) + .using_pk(*key_pair.public()) .build() .expect("valid address props"); diff --git a/mm2src/mm2_main/tests/docker_tests/slp_tests.rs b/mm2src/mm2_main/tests/docker_tests/slp_tests.rs index 7fd9c7930f..ca9488cd58 100644 --- a/mm2src/mm2_main/tests/docker_tests/slp_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/slp_tests.rs @@ -4,7 +4,7 @@ use bitcrypto::ChecksumType; use coins::utxo::UtxoAddressFormat; use common::block_on; use http::StatusCode; -use keys::{Address, AddressBuilder, AddressHashEnum, AddressPrefix, NetworkAddressPrefixes}; +use keys::{Address, AddressBuilder, AddressPrefix, LockingDestination, NetworkAddressPrefixes}; use mm2_number::BigDecimal; use mm2_rpc::data::legacy::{BalanceResponse, CoinInitResponse}; use mm2_test_helpers::for_tests::{ @@ -66,7 +66,7 @@ fn utxo_burn_address() -> Address { }, None, ) - .as_pkh(AddressHashEnum::default_address_hash()) + .as_pkh(LockingDestination::default_address_hash()) .build() .expect("valid address props") } diff --git a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs index 073c63bbec..370787c8a7 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -14,13 +14,13 @@ use mm2_test_helpers::electrums::*; use mm2_test_helpers::for_tests::wait_check_stats_swap_status; use mm2_test_helpers::for_tests::{ account_balance, btc_segwit_conf, btc_with_spv_conf, btc_with_sync_starting_header, check_recent_swaps, - delete_wallet, enable_qrc20, enable_utxo_v2_electrum, eth_dev_conf, find_metrics_in_json, from_env_file, + delete_wallet, doc_conf, enable_qrc20, enable_utxo_v2_electrum, eth_dev_conf, find_metrics_in_json, from_env_file, get_new_address, get_shared_db_id, get_wallet_names, mm_spat, morty_conf, my_balance, rick_conf, sign_message, - start_swaps, tbtc_conf, tbtc_segwit_conf, tbtc_with_spv_conf, test_qrc20_history_impl, tqrc20_conf, verify_message, - wait_for_swaps_finish_and_check_status, wait_till_history_has_records, MarketMakerIt, Mm2InitPrivKeyPolicy, - Mm2TestConf, Mm2TestConfForSwap, RaiiDump, DOC_ELECTRUM_ADDRS, ETH_MAINNET_NODES, ETH_MAINNET_SWAP_CONTRACT, - ETH_SEPOLIA_NODES, ETH_SEPOLIA_SWAP_CONTRACT, MARTY_ELECTRUM_ADDRS, MORTY, QRC20_ELECTRUMS, RICK, - RICK_ELECTRUM_ADDRS, TBTC_ELECTRUMS, T_BCH_ELECTRUMS, + start_swaps, tbtc_conf, tbtc_segwit_conf, tbtc_taproot_conf, tbtc_with_spv_conf, test_qrc20_history_impl, + tqrc20_conf, verify_message, wait_for_swaps_finish_and_check_status, wait_till_history_has_records, MarketMakerIt, + Mm2InitPrivKeyPolicy, Mm2TestConf, Mm2TestConfForSwap, RaiiDump, DOC_ELECTRUM_ADDRS, ETH_MAINNET_NODES, + ETH_MAINNET_SWAP_CONTRACT, ETH_SEPOLIA_NODES, ETH_SEPOLIA_SWAP_CONTRACT, MARTY_ELECTRUM_ADDRS, MORTY, + QRC20_ELECTRUMS, RICK, RICK_ELECTRUM_ADDRS, TBTC_ELECTRUMS, T_BCH_ELECTRUMS, }; use mm2_test_helpers::get_passphrase; use mm2_test_helpers::structs::*; @@ -695,9 +695,10 @@ async fn trade_base_rel_electrum( volume: f64, ) { let coins = json!([ - rick_conf(), + doc_conf(), morty_conf(), {"coin":"ZOMBIE","asset":"ZOMBIE","fname":"ZOMBIE (TESTCOIN)","txversion":4,"overwintered":1,"mm2":1,"protocol":{"type":"ZHTLC"},"required_confirmations":0}, + tbtc_taproot_conf(), ]); let bob_conf = Mm2TestConfForSwap::bob_conf_with_policy(&bob_priv_key_policy, &coins); @@ -752,24 +753,60 @@ async fn trade_base_rel_electrum( log!("enable ZOMBIE alice {:?}", zombie_alice); } // Enable coins on Bob side. Print the replies in case we need the address. - let rc = enable_utxo_v2_electrum(&mm_bob, "RICK", doc_electrums(), bob_path_to_address.clone(), 600, None).await; - log!("enable RICK (bob): {:?}", rc); - let rc = enable_utxo_v2_electrum(&mm_bob, "MORTY", marty_electrums(), bob_path_to_address, 600, None).await; + let rc = enable_utxo_v2_electrum(&mm_bob, "DOC", doc_electrums(), bob_path_to_address.clone(), 600, None).await; + log!("enable DOC (bob): {:?}", rc); + let rc = enable_utxo_v2_electrum( + &mm_bob, + "MORTY", + marty_electrums(), + bob_path_to_address.clone(), + 600, + None, + ) + .await; log!("enable MORTY (bob): {:?}", rc); + let rc = enable_utxo_v2_electrum( + &mm_bob, + "tBTC-taproot", + tbtc_electrums(), + bob_path_to_address, + 600, + None, + ) + .await; + log!("enable tBTC-taproot (bob): {:?}", rc); // Enable coins on Alice side. Print the replies in case we need the address. let rc = enable_utxo_v2_electrum( &mm_alice, - "RICK", + "DOC", doc_electrums(), alice_path_to_address.clone(), 600, None, ) .await; - log!("enable RICK (alice): {:?}", rc); - let rc = enable_utxo_v2_electrum(&mm_alice, "MORTY", marty_electrums(), alice_path_to_address, 600, None).await; + log!("enable DOC (alice): {:?}", rc); + let rc = enable_utxo_v2_electrum( + &mm_alice, + "MORTY", + marty_electrums(), + alice_path_to_address.clone(), + 600, + None, + ) + .await; log!("enable MORTY (alice): {:?}", rc); + let rc = enable_utxo_v2_electrum( + &mm_alice, + "tBTC-taproot", + tbtc_electrums(), + alice_path_to_address, + 600, + None, + ) + .await; + log!("enable tBTC-taproot (alice): {:?}", rc); let uuids = start_swaps(&mut mm_bob, &mut mm_alice, pairs, maker_price, taker_price, volume).await; @@ -844,10 +881,10 @@ async fn trade_base_rel_electrum( #[test] #[cfg(all(not(target_arch = "wasm32"), feature = "zhtlc-native-tests"))] -fn trade_test_electrum_rick_zombie() { +fn trade_test_electrum_doc_zombie() { let bob_policy = Mm2InitPrivKeyPolicy::Iguana; let alice_policy = Mm2InitPrivKeyPolicy::Iguana; - let pairs = &[("RICK", "ZOMBIE")]; + let pairs = &[("DOC", "ZOMBIE")]; block_on(trade_base_rel_electrum( bob_policy, alice_policy, @@ -860,6 +897,26 @@ fn trade_test_electrum_rick_zombie() { )); } +// Taproot testnet swap coverage between tBTC-taproot and DOC. +// Ignored by default because it requires funded taproot/DOC balances on the public cipig electrum servers. +#[test] +#[ignore] +fn trade_test_taproot_tbtc_with_doc() { + let bob_policy = Mm2InitPrivKeyPolicy::GlobalHDAccount; + let alice_policy = Mm2InitPrivKeyPolicy::GlobalHDAccount; + let pairs = &[("tBTC-taproot", "DOC")]; + block_on(trade_base_rel_electrum( + bob_policy, + alice_policy, + None, + None, + pairs, + 1., + 2., + 0.0001, + )); +} + #[cfg(not(target_arch = "wasm32"))] fn withdraw_and_send( mm: &MarketMakerIt, @@ -1227,7 +1284,7 @@ fn test_withdraw_segwit() { assert!(withdraw_error.get("error_type").is_none()); assert!(withdraw_error.get("error_data").is_none()); - // Withdraw to taproot addresses should fail + // Withdraw to taproot addresses should also work let withdraw = block_on(mm_alice.rpc(&json!({ "userpass": mm_alice.userpass, "method": "withdraw", @@ -1237,13 +1294,8 @@ fn test_withdraw_segwit() { }))) .unwrap(); - assert!(withdraw.0.is_server_error(), "tBTC withdraw: {}", withdraw.1); - log!("{:?}", withdraw.1); - let withdraw_error: Json = json::from_str(&withdraw.1).unwrap(); - assert!(withdraw_error["error"] - .as_str() - .expect("Expected 'error' field") - .contains("address variant/format Bech32m is not supported yet")); + assert!(withdraw.0.is_success(), "tBTC withdraw: {}", withdraw.1); + let _: TransactionDetails = json::from_str(&withdraw.1).expect("Expected 'TransactionDetails'"); block_on(mm_alice.stop()).unwrap(); } diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index d103e9c6ad..84c5403d1a 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -832,6 +832,33 @@ pub fn tbtc_segwit_conf() -> Json { }) } +pub fn tbtc_taproot_conf() -> Json { + json!({ + "coin": "tBTC-taproot", + "asset":"tBTC-taproot", + "rpcport": 18332, + "pubtype": 111, + "p2shtype": 196, + "wiftype": 239, + "segwit": true, + "bech32_hrp": "tb", + "address_format": { + "format": "segwit", + "version": 1 + }, + "orderbook_ticker": "tBTC", + "txfee": 0, + "estimate_fee_mode": "ECONOMICAL", + "mm2": 1, + "is_testnet": true, + "required_confirmations": 0, + "protocol": { + "type": "UTXO" + }, + "derivation_path": "m/86'/1'" + }) +} + pub fn tbtc_with_spv_conf() -> Json { json!({ "coin": "tBTC-TEST", diff --git a/mm2src/trezor/src/proto/messages_bitcoin.rs b/mm2src/trezor/src/proto/messages_bitcoin.rs index 333d299d0c..52d17af0f9 100644 --- a/mm2src/trezor/src/proto/messages_bitcoin.rs +++ b/mm2src/trezor/src/proto/messages_bitcoin.rs @@ -827,6 +827,8 @@ pub enum InputScriptType { Spendwitness = 3, /// SegWit over P2SH (backward compatible) Spendp2shwitness = 4, + /// Taproot + Spendtaproot = 5, } ///* /// Type of script which will be used for transaction output @@ -845,6 +847,8 @@ pub enum OutputScriptType { Paytowitness = 4, /// only for change output Paytop2shwitness = 5, + /// only for change output + Paytotaproot = 6, } ///* /// Type of script which will be used for decred stake transaction input diff --git a/mm2src/trezor/src/utxo/unsigned_tx.rs b/mm2src/trezor/src/utxo/unsigned_tx.rs index 0d8b86f236..14aa6f617b 100644 --- a/mm2src/trezor/src/utxo/unsigned_tx.rs +++ b/mm2src/trezor/src/utxo/unsigned_tx.rs @@ -17,6 +17,8 @@ pub enum TrezorInputScriptType { SpendWitness, /// SegWit over P2SH (backward compatible). SpendP2SHWitness, + /// Taproot input (P2TR). + SpendTaproot, } impl From for proto_bitcoin::InputScriptType { @@ -27,6 +29,7 @@ impl From for proto_bitcoin::InputScriptType { TrezorInputScriptType::External => proto_bitcoin::InputScriptType::External, TrezorInputScriptType::SpendWitness => proto_bitcoin::InputScriptType::Spendwitness, TrezorInputScriptType::SpendP2SHWitness => proto_bitcoin::InputScriptType::Spendp2shwitness, + TrezorInputScriptType::SpendTaproot => proto_bitcoin::InputScriptType::Spendtaproot, } } } @@ -39,6 +42,8 @@ pub enum TrezorOutputScriptType { PayToOpReturn, /// pay to witness v0, used for the change output PayToWitness, + /// Taproot output (P2TR), used for the change output + PayToTaproot, } impl From for proto_bitcoin::OutputScriptType { @@ -47,6 +52,7 @@ impl From for proto_bitcoin::OutputScriptType { TrezorOutputScriptType::PayToAddress => proto_bitcoin::OutputScriptType::Paytoaddress, TrezorOutputScriptType::PayToOpReturn => proto_bitcoin::OutputScriptType::Paytoopreturn, TrezorOutputScriptType::PayToWitness => proto_bitcoin::OutputScriptType::Paytowitness, + TrezorOutputScriptType::PayToTaproot => proto_bitcoin::OutputScriptType::Paytotaproot, } } }