From 4c0b148bb231d5c2b16592b334c261d5629e7313 Mon Sep 17 00:00:00 2001 From: KaffinPX <73744616+KaffinPX@users.noreply.github.com> Date: Tue, 16 Jul 2024 19:05:49 +0300 Subject: [PATCH] Custom input signatureScript ability (#69) * Refactorize some addresses and Script related parts * A ScriptBuilder example on TypeScript * addOps Opcodes are now BinaryT * Move txscript bindings to relevant folders * Sort lines of deps and imports * Lint * Prototype of custom sighash operations * Experimental * Add SighashType enum and option on SignInput * Format and a small fix * Clippy --- consensus/core/src/hashing/mod.rs | 2 + consensus/core/src/hashing/wasm.rs | 28 +++++++++++++ wallet/core/src/account/mod.rs | 2 +- wallet/core/src/tx/generator/pending.rs | 42 ++++++++++++++++++-- wallet/core/src/wasm/tx/generator/pending.rs | 19 ++++++++- 5 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 consensus/core/src/hashing/wasm.rs diff --git a/consensus/core/src/hashing/mod.rs b/consensus/core/src/hashing/mod.rs index aa0ac2d9b..edcca1d03 100644 --- a/consensus/core/src/hashing/mod.rs +++ b/consensus/core/src/hashing/mod.rs @@ -5,6 +5,8 @@ pub mod header; pub mod sighash; pub mod sighash_type; pub mod tx; +#[cfg(feature = "wasm32-sdk")] +pub mod wasm; pub trait HasherExtensions { /// Writes the len as u64 little endian bytes diff --git a/consensus/core/src/hashing/wasm.rs b/consensus/core/src/hashing/wasm.rs new file mode 100644 index 000000000..1c3b51a7a --- /dev/null +++ b/consensus/core/src/hashing/wasm.rs @@ -0,0 +1,28 @@ +use super::sighash_type::{self, SigHashType}; +use wasm_bindgen::prelude::*; + +/// Kaspa Sighash types allowed by consensus +/// @see {@link signInput} +/// @category Consensus +#[wasm_bindgen] +pub enum SighashType { + All, + None, + Single, + AllAnyOneCanPay, + NoneAnyOneCanPay, + SingleAnyOneCanPay, +} + +impl From for SigHashType { + fn from(sighash_type: SighashType) -> SigHashType { + match sighash_type { + SighashType::All => sighash_type::SIG_HASH_ALL, + SighashType::None => sighash_type::SIG_HASH_NONE, + SighashType::Single => sighash_type::SIG_HASH_SINGLE, + SighashType::AllAnyOneCanPay => sighash_type::SIG_HASH_ANY_ONE_CAN_PAY, + SighashType::NoneAnyOneCanPay => SigHashType(sighash_type::SIG_HASH_NONE.0 | sighash_type::SIG_HASH_ANY_ONE_CAN_PAY.0), + SighashType::SingleAnyOneCanPay => SigHashType(sighash_type::SIG_HASH_SINGLE.0 | sighash_type::SIG_HASH_ANY_ONE_CAN_PAY.0), + } + } +} diff --git a/wallet/core/src/account/mod.rs b/wallet/core/src/account/mod.rs index 56abdc787..325ead81c 100644 --- a/wallet/core/src/account/mod.rs +++ b/wallet/core/src/account/mod.rs @@ -538,7 +538,7 @@ pub trait DerivationCapableAccount: Account { let mut stream = generator.stream(); while let Some(transaction) = stream.try_next().await? { - transaction.try_sign_with_keys(&keys)?; + transaction.try_sign_with_keys(&keys, None)?; let id = transaction.try_submit(&rpc).await?; if let Some(notifier) = notifier { notifier(index, aggregate_utxo_count, balance, Some(id)); diff --git a/wallet/core/src/tx/generator/pending.rs b/wallet/core/src/tx/generator/pending.rs index cd757e54b..7d07af74e 100644 --- a/wallet/core/src/tx/generator/pending.rs +++ b/wallet/core/src/tx/generator/pending.rs @@ -8,7 +8,9 @@ use crate::result::Result; use crate::rpc::DynRpcApi; use crate::tx::{DataKind, Generator}; use crate::utxo::{UtxoContext, UtxoEntryId, UtxoEntryReference}; -use kaspa_consensus_core::sign::sign_with_multiple_v2; +use kaspa_consensus_core::hashing::sighash::{calc_schnorr_signature_hash, SigHashReusedValues}; +use kaspa_consensus_core::hashing::sighash_type::SigHashType; +use kaspa_consensus_core::sign::{sign_with_multiple_v2, Signed}; use kaspa_consensus_core::tx::{SignableTransaction, Transaction, TransactionId}; use kaspa_rpc_core::{RpcTransaction, RpcTransactionId}; @@ -223,9 +225,43 @@ impl PendingTransaction { Ok(()) } - pub fn try_sign_with_keys(&self, privkeys: &[[u8; 32]]) -> Result<()> { + pub fn sign_input(&self, input_index: usize, private_key: &[u8; 32], hash_type: SigHashType) -> Result> { + // TODO: Move to sign.rs let mutable_tx = self.inner.signable_tx.lock()?.clone(); - let signed_tx = sign_with_multiple_v2(mutable_tx, privkeys).fully_signed()?; + let mut reused_values = SigHashReusedValues::new(); + + let hash = calc_schnorr_signature_hash(&mutable_tx.as_verifiable(), input_index, hash_type, &mut reused_values); + let msg = secp256k1::Message::from_digest_slice(hash.as_bytes().as_slice()).unwrap(); + let schnorr_key = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, private_key).unwrap(); + let sig: [u8; 64] = *schnorr_key.sign_schnorr(msg).as_ref(); + + // This represents OP_DATA_65 (since signature length is 64 bytes and SIGHASH_TYPE is one byte) + Ok(std::iter::once(65u8).chain(sig).chain([hash_type.to_u8()]).collect()) + } + + pub fn fill_input(&self, input_index: usize, signature_script: Vec) -> Result<()> { + let mut mutable_tx = self.inner.signable_tx.lock()?.clone(); + mutable_tx.tx.inputs[input_index].signature_script = signature_script; + *self.inner.signable_tx.lock().unwrap() = mutable_tx; + + Ok(()) + } + + pub fn try_sign_with_keys(&self, privkeys: &[[u8; 32]], check_fully_signed: Option) -> Result<()> { + let mutable_tx = self.inner.signable_tx.lock()?.clone(); + let signed = sign_with_multiple_v2(mutable_tx, privkeys); + + let signed_tx = match signed { + Signed::Fully(tx) => tx, + Signed::Partially(_) => { + if check_fully_signed.unwrap_or(true) { + signed.fully_signed()? + } else { + signed.unwrap() + } + } + }; + *self.inner.signable_tx.lock().unwrap() = signed_tx; Ok(()) } diff --git a/wallet/core/src/wasm/tx/generator/pending.rs b/wallet/core/src/wasm/tx/generator/pending.rs index 8de697e55..2257440d9 100644 --- a/wallet/core/src/wasm/tx/generator/pending.rs +++ b/wallet/core/src/wasm/tx/generator/pending.rs @@ -4,7 +4,9 @@ use crate::tx::generator as native; use crate::wasm::PrivateKeyArrayT; use kaspa_consensus_client::{numeric, string}; use kaspa_consensus_client::{Transaction, TransactionT}; +use kaspa_consensus_core::hashing::wasm::SighashType; use kaspa_wallet_keys::privatekey::PrivateKey; +use kaspa_wasm_core::types::{BinaryT, HexString}; use kaspa_wrpc_wasm::RpcClient; /// @category Wallet SDK @@ -70,16 +72,29 @@ impl PendingTransaction { self.inner.utxo_entries().values().map(|utxo_entry| JsValue::from(utxo_entry.clone())).collect() } + #[wasm_bindgen(js_name = signInput)] + pub fn sign_input(&self, input_index: u8, private_key: &PrivateKey, sighash_type: Option) -> Result { + let signature = + self.inner.sign_input(input_index.into(), &private_key.secret_bytes(), sighash_type.unwrap_or(SighashType::All).into())?; + + Ok(signature.to_hex().into()) + } + + #[wasm_bindgen(js_name = fillInput)] + pub fn fill_input(&self, input_index: u8, signature_script: BinaryT) -> Result<()> { + self.inner.fill_input(input_index.into(), signature_script.try_as_vec_u8()?) + } + /// Sign transaction with supplied [`Array`] or [`PrivateKey`] or an array of /// raw private key bytes (encoded as `Uint8Array` or as hex strings) - pub fn sign(&self, js_value: PrivateKeyArrayT) -> Result<()> { + pub fn sign(&self, js_value: PrivateKeyArrayT, check_fully_signed: Option) -> Result<()> { if let Ok(keys) = js_value.dyn_into::() { let keys = keys .iter() .map(PrivateKey::try_cast_from) .collect::, kaspa_wallet_keys::error::Error>>()?; let mut keys = keys.iter().map(|key| key.as_ref().secret_bytes()).collect::>(); - self.inner.try_sign_with_keys(&keys)?; + self.inner.try_sign_with_keys(&keys, check_fully_signed)?; keys.zeroize(); Ok(()) } else {