diff --git a/Cargo.lock b/Cargo.lock index 2512149b3..a41b22aeb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3493,7 +3493,6 @@ dependencies = [ "kaspa-wasm-core", "kaspa-wrpc-client", "kaspa-wrpc-wasm", - "lazy_static", "md-5", "pad", "pbkdf2", diff --git a/Cargo.toml b/Cargo.toml index 56834b528..463a49ab6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -192,7 +192,6 @@ ipnet = "2.9.0" itertools = "0.11.0" js-sys = "0.3.67" keccak = "0.1.4" -lazy_static = "1.4.0" local-ip-address = "0.5.6" log = "0.4.20" log4rs = "1.2.0" diff --git a/consensus/client/src/input.rs b/consensus/client/src/input.rs index d25e5f7d9..81b1d009a 100644 --- a/consensus/client/src/input.rs +++ b/consensus/client/src/input.rs @@ -13,7 +13,7 @@ const TS_TRANSACTION: &'static str = r#" */ export interface ITransactionInput { previousOutpoint: ITransactionOutpoint; - signatureScript: HexString; + signatureScript?: HexString; sequence: bigint; sigOpCount: number; utxo?: UtxoEntryReference; @@ -95,6 +95,10 @@ impl TransactionInput { self.inner().sig_op_count } + pub fn signature_script_length(&self) -> usize { + self.inner().signature_script.len() + } + pub fn utxo(&self) -> Option { self.inner().utxo.clone() } @@ -187,7 +191,7 @@ impl TryCastFromJs for TransactionInput { Self::resolve_cast(&value, || { if let Some(object) = Object::try_from(value.as_ref()) { let previous_outpoint: TransactionOutpoint = object.get_value("previousOutpoint")?.as_ref().try_into()?; - let signature_script = object.get_vec_u8("signatureScript")?; + let signature_script = object.get_vec_u8("signatureScript").unwrap_or_default(); let sequence = object.get_u64("sequence")?; let sig_op_count = object.get_u8("sigOpCount")?; let utxo = object.try_get_cast::("utxo")?.map(Cast::into_owned); diff --git a/consensus/client/src/output.rs b/consensus/client/src/output.rs index 4dddb5067..f4ee1ad1e 100644 --- a/consensus/client/src/output.rs +++ b/consensus/client/src/output.rs @@ -65,7 +65,7 @@ impl TransactionOutput { self.inner.lock().unwrap() } - pub fn script_length(&self) -> usize { + pub fn script_public_key_length(&self) -> usize { self.inner().script_public_key.script().len() } } @@ -79,7 +79,7 @@ impl TransactionOutput { } #[wasm_bindgen(getter, js_name = value)] - pub fn get_value(&self) -> u64 { + pub fn value(&self) -> u64 { self.inner().value } diff --git a/consensus/client/src/sign.rs b/consensus/client/src/sign.rs index fdab66a60..b4afc11e6 100644 --- a/consensus/client/src/sign.rs +++ b/consensus/client/src/sign.rs @@ -44,7 +44,7 @@ pub fn sign_with_multiple_v3(tx: Transaction, privkeys: &[[u8; 32]]) -> crate::r let mut additional_signatures_required = false; { let input_len = tx.inner().inputs.len(); - let (cctx, utxos) = tx.tx_and_utxos(); + let (cctx, utxos) = tx.tx_and_utxos()?; let populated_transaction = PopulatedTransaction::new(&cctx, utxos); for i in 0..input_len { let script_pub_key = match tx.inner().inputs[i].script_public_key() { diff --git a/consensus/client/src/transaction.rs b/consensus/client/src/transaction.rs index d03e6a512..0c1c8fa6a 100644 --- a/consensus/client/src/transaction.rs +++ b/consensus/client/src/transaction.rs @@ -359,18 +359,18 @@ impl Transaction { }) } - pub fn tx_and_utxos(&self) -> (cctx::Transaction, Vec) { - let mut utxos = vec![]; + pub fn tx_and_utxos(&self) -> Result<(cctx::Transaction, Vec)> { + let mut inputs = vec![]; let inner = self.inner(); - let inputs: Vec = inner + let utxos: Vec = inner .inputs .clone() .into_iter() .map(|input| { - utxos.push((&input.get_utxo().unwrap().entry()).into()); - input.as_ref().into() + inputs.push(input.as_ref().into()); + Ok(input.get_utxo().ok_or(Error::MissingUtxoEntry)?.entry().as_ref().into()) }) - .collect::>(); + .collect::>>()?; let outputs: Vec = inner.outputs.clone().into_iter().map(|output| output.as_ref().into()).collect::>(); let tx = cctx::Transaction::new( @@ -383,7 +383,37 @@ impl Transaction { inner.payload.clone(), ); - (tx, utxos) + Ok((tx, utxos)) + } + + pub fn utxo_entry_references(&self) -> Result> { + let inner = self.inner(); + let utxo_entry_references = inner + .inputs + .clone() + .into_iter() + .map(|input| input.get_utxo().ok_or(Error::MissingUtxoEntry)) + .collect::>>()?; + Ok(utxo_entry_references) + } + + pub fn outputs(&self) -> Vec { + let inner = self.inner(); + let outputs = inner.outputs.iter().map(|output| output.into()).collect::>(); + outputs + } + + pub fn inputs(&self) -> Vec { + let inner = self.inner(); + let inputs = inner.inputs.iter().map(Into::into).collect::>(); + inputs + } + + pub fn inputs_outputs(&self) -> (Vec, Vec) { + let inner = self.inner(); + let inputs = inner.inputs.iter().map(Into::into).collect::>(); + let outputs = inner.outputs.iter().map(Into::into).collect::>(); + (inputs, outputs) } pub fn set_signature_script(&self, input_index: usize, signature_script: Vec) -> Result<()> { @@ -393,6 +423,14 @@ impl Transaction { self.inner().inputs[input_index].set_signature_script(signature_script); Ok(()) } + + pub fn payload(&self) -> Vec { + self.inner().payload.clone() + } + + pub fn payload_len(&self) -> usize { + self.inner().payload.len() + } } #[wasm_bindgen] diff --git a/consensus/client/src/utxo.rs b/consensus/client/src/utxo.rs index 8c248a6d3..afcf87c4b 100644 --- a/consensus/client/src/utxo.rs +++ b/consensus/client/src/utxo.rs @@ -101,6 +101,12 @@ impl UtxoEntry { } } +impl AsRef for UtxoEntry { + fn as_ref(&self) -> &UtxoEntry { + self + } +} + impl From<&UtxoEntry> for cctx::UtxoEntry { fn from(utxo: &UtxoEntry) -> Self { cctx::UtxoEntry { diff --git a/consensus/core/src/mass/mod.rs b/consensus/core/src/mass/mod.rs index 6e348299c..f58d10466 100644 --- a/consensus/core/src/mass/mod.rs +++ b/consensus/core/src/mass/mod.rs @@ -4,6 +4,17 @@ use crate::{ }; use kaspa_hashes::HASH_SIZE; +/// Temp enum for the transition phases of KIP9 +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Kip9Version { + /// Initial KIP9 mass calculation, w/o the relaxed formula and summing storage mass and compute mass + Alpha, + + /// Currently proposed KIP9 mass calculation, with the relaxed formula (for the cases `|O| = 1 OR |O| <= |I| <= 2`), + /// and using a maximum operator over storage and compute mass + Beta, +} + // transaction_estimated_serialized_size is the estimated size of a transaction in some // serialization. This has to be deterministic, but not necessarily accurate, since // it's only used as the size component in the transaction and block mass limit diff --git a/consensus/src/processes/mass.rs b/consensus/src/processes/mass.rs index 8bb5f3339..e6198d346 100644 --- a/consensus/src/processes/mass.rs +++ b/consensus/src/processes/mass.rs @@ -1,19 +1,9 @@ +pub use kaspa_consensus_core::mass::Kip9Version; use kaspa_consensus_core::{ mass::transaction_estimated_serialized_size, tx::{Transaction, VerifiableTransaction}, }; -/// Temp enum for the transition phases of KIP9 -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum Kip9Version { - /// Initial KIP9 mass calculation, w/o the relaxed formula and summing storage mass and compute mass - Alpha, - - /// Currently proposed KIP9 mass calculation, with the relaxed formula (for the cases `|O| = 1 OR |O| <= |I| <= 2`), - /// and using a maximum operator over storage and compute mass - Beta, -} - // TODO (aspect) - review and potentially merge this with the new MassCalculator currently located in the wallet core // (i.e. migrate mass calculator from wallet core here or to consensus core) #[derive(Clone)] diff --git a/wallet/core/Cargo.toml b/wallet/core/Cargo.toml index 1e528e369..9a6c4cfa6 100644 --- a/wallet/core/Cargo.toml +++ b/wallet/core/Cargo.toml @@ -74,7 +74,6 @@ kaspa-wallet-macros.workspace = true kaspa-wasm-core.workspace = true kaspa-wrpc-client.workspace = true kaspa-wrpc-wasm.workspace = true -lazy_static.workspace = true md-5.workspace = true pad.workspace = true pbkdf2.workspace = true diff --git a/wallet/core/src/imports.rs b/wallet/core/src/imports.rs index 2d2ce79fd..5ad48498c 100644 --- a/wallet/core/src/imports.rs +++ b/wallet/core/src/imports.rs @@ -17,7 +17,6 @@ pub use crate::rpc::Rpc; pub use crate::rpc::{DynRpcApi, RpcCtl}; pub use crate::serializer::*; pub use crate::storage::*; -pub use crate::tx::MassCombinationStrategy; pub use crate::utxo::balance::Balance; pub use crate::utxo::scan::{Scan, ScanExtent}; pub use crate::utxo::{Maturity, NetworkParams, OutgoingTransaction, UtxoContext, UtxoEntryReference, UtxoProcessor}; @@ -49,7 +48,7 @@ pub use std::collections::{HashMap, HashSet}; pub use std::pin::Pin; pub use std::str::FromStr; pub use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; -pub use std::sync::{Arc, Mutex, MutexGuard, RwLock}; +pub use std::sync::{Arc, Mutex, MutexGuard, OnceLock, RwLock}; pub use std::task::{Context, Poll}; pub use wasm_bindgen::prelude::*; pub use workflow_core::prelude::*; diff --git a/wallet/core/src/tx/generator/generator.rs b/wallet/core/src/tx/generator/generator.rs index 5ef2869f7..359ee634a 100644 --- a/wallet/core/src/tx/generator/generator.rs +++ b/wallet/core/src/tx/generator/generator.rs @@ -66,6 +66,7 @@ use crate::tx::{ use crate::utxo::{NetworkParams, UtxoContext, UtxoEntryReference}; use kaspa_consensus_client::UtxoEntry; use kaspa_consensus_core::constants::UNACCEPTED_DAA_SCORE; +use kaspa_consensus_core::mass::Kip9Version; use kaspa_consensus_core::subnets::SUBNETWORK_ID_NATIVE; use kaspa_consensus_core::tx::{Transaction, TransactionInput, TransactionOutpoint, TransactionOutput}; use kaspa_txscript::pay_to_address_script; @@ -215,7 +216,7 @@ struct Data { impl Data { fn new(calc: &MassCalculator) -> Self { - let aggregate_mass = calc.blank_transaction_mass(); + let aggregate_mass = calc.blank_transaction_compute_mass(); Data { inputs: vec![], @@ -266,7 +267,7 @@ struct Inner { // Current network id network_id: NetworkId, // Current network params - network_params: NetworkParams, + network_params: &'static NetworkParams, // Source Utxo Context (Used for source UtxoEntry aggregation) source_utxo_context: Option, @@ -357,7 +358,7 @@ impl Generator { let network_type = NetworkType::from(network_id); let network_params = NetworkParams::from(network_id); - let mass_calculator = MassCalculator::new(&network_id.into(), &network_params); + let mass_calculator = MassCalculator::new(&network_id.into(), network_params); let (final_transaction_outputs, final_transaction_amount) = match final_transaction_destination { PaymentDestination::Change => { @@ -402,11 +403,11 @@ impl Generator { } let standard_change_output_mass = - mass_calculator.calc_mass_for_output(&TransactionOutput::new(0, pay_to_address_script(&change_address))); - let signature_mass_per_input = mass_calculator.calc_signature_mass(minimum_signatures); - let final_transaction_outputs_compute_mass = mass_calculator.calc_mass_for_outputs(&final_transaction_outputs); + mass_calculator.calc_compute_mass_for_output(&TransactionOutput::new(0, pay_to_address_script(&change_address))); + let signature_mass_per_input = mass_calculator.calc_compute_mass_for_signature(minimum_signatures); + let final_transaction_outputs_compute_mass = mass_calculator.calc_compute_mass_for_outputs(&final_transaction_outputs); let final_transaction_payload = final_transaction_payload.unwrap_or_default(); - let final_transaction_payload_mass = mass_calculator.calc_mass_for_payload(final_transaction_payload.len()); + let final_transaction_payload_mass = mass_calculator.calc_compute_mass_for_payload(final_transaction_payload.len()); let final_transaction_outputs_harmonic = mass_calculator.calc_storage_mass_output_harmonic(&final_transaction_outputs).ok_or(Error::MassCalculationError)?; @@ -477,7 +478,7 @@ impl Generator { /// Returns current [`NetworkParams`] pub fn network_params(&self) -> &NetworkParams { - &self.inner.network_params + self.inner.network_params } /// The underlying [`UtxoContext`] (if available). @@ -662,7 +663,7 @@ impl Generator { let input = TransactionInput::new(utxo.outpoint.clone().into(), vec![], 0, self.inner.sig_op_count); let input_amount = utxo.amount(); - let input_compute_mass = calc.calc_mass_for_input(&input) + self.inner.signature_mass_per_input; + let input_compute_mass = calc.calc_compute_mass_for_input(&input) + self.inner.signature_mass_per_input; // NOTE: relay transactions have no storage mass // mass threshold reached, yield transaction @@ -865,8 +866,11 @@ impl Generator { calc.calc_storage_mass_output_harmonic_single(change_value) + self.inner.final_transaction_outputs_harmonic; let storage_mass_with_change = self.calc_storage_mass(data, output_harmonic_with_change); + // TODO - review and potentially simplify: + // this profiles the storage mass with change and without change + // and decides which one to use based on the fees if storage_mass_with_change == 0 - || (self.inner.network_params.mass_combination_strategy() == MassCombinationStrategy::Max + || (self.inner.network_params.kip9_version() == Kip9Version::Beta // max(compute vs storage) && storage_mass_with_change < compute_mass_with_change) { 0 diff --git a/wallet/core/src/tx/generator/test.rs b/wallet/core/src/tx/generator/test.rs index d7f5e7f83..045128a7b 100644 --- a/wallet/core/src/tx/generator/test.rs +++ b/wallet/core/src/tx/generator/test.rs @@ -170,10 +170,10 @@ fn validate(pt: &PendingTransaction) { let calc = MassCalculator::new(&pt.network_type().into(), network_params); let additional_mass = if pt.is_final() { 0 } else { network_params.additional_compound_transaction_mass() }; - let compute_mass = calc.calc_mass_for_signed_transaction(&tx, 1); + let compute_mass = calc.calc_compute_mass_for_signed_transaction(&tx, 1); let utxo_entries = pt.utxo_entries().values().cloned().collect::>(); - let storage_mass = calc.calc_storage_mass_for_transaction(false, &utxo_entries, &tx.outputs).unwrap_or_default(); + let storage_mass = calc.calc_storage_mass_for_transaction_parts(&utxo_entries, &tx.outputs).unwrap_or_default(); let calculated_mass = calc.combine_mass(compute_mass, storage_mass) + additional_mass; @@ -201,10 +201,10 @@ where let calc = MassCalculator::new(&pt.network_type().into(), network_params); let additional_mass = if pt.is_final() { 0 } else { network_params.additional_compound_transaction_mass() }; - let compute_mass = calc.calc_mass_for_signed_transaction(&tx, 1); + let compute_mass = calc.calc_compute_mass_for_signed_transaction(&tx, 1); let utxo_entries = pt.utxo_entries().values().cloned().collect::>(); - let storage_mass = calc.calc_storage_mass_for_transaction(false, &utxo_entries, &tx.outputs).unwrap_or_default(); + let storage_mass = calc.calc_storage_mass_for_transaction_parts(&utxo_entries, &tx.outputs).unwrap_or_default(); if DISPLAY_LOGS && storage_mass != 0 { println!( "calculated storage mass: {} calculated_compute_mass: {} total: {}", diff --git a/wallet/core/src/tx/mass.rs b/wallet/core/src/tx/mass.rs index c76d10905..09545bf13 100644 --- a/wallet/core/src/tx/mass.rs +++ b/wallet/core/src/tx/mass.rs @@ -2,20 +2,15 @@ //! Transaction mass calculator. //! +use crate::result::Result; use crate::utxo::NetworkParams; +use kaspa_consensus_client as kcc; use kaspa_consensus_client::UtxoEntryReference; +use kaspa_consensus_core::mass::Kip9Version; use kaspa_consensus_core::tx::{Transaction, TransactionInput, TransactionOutput, SCRIPT_VECTOR_SIZE}; use kaspa_consensus_core::{config::params::Params, constants::*, subnets::SUBNETWORK_ID_SIZE}; use kaspa_hashes::HASH_SIZE; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum MassCombinationStrategy { - /// `MassCombinator::Add` adds the storage and compute mass. - Add, - /// `MassCombinator::Max` returns the maximum of the storage and compute mass. - Max, -} - // pub const ECDSA_SIGNATURE_SIZE: u64 = 64; // pub const SCHNORR_SIGNATURE_SIZE: u64 = 64; pub const SIGNATURE_SIZE: u64 = 1 + 64 + 1; //1 byte for OP_DATA_65 + 64 (length of signature) + 1 byte for sig hash type @@ -222,7 +217,7 @@ pub struct MassCalculator { mass_per_script_pub_key_byte: u64, mass_per_sig_op: u64, storage_mass_parameter: u64, - mass_combination_strategy: MassCombinationStrategy, + kip9_version: Kip9Version, } impl MassCalculator { @@ -232,7 +227,7 @@ impl MassCalculator { mass_per_script_pub_key_byte: consensus_params.mass_per_script_pub_key_byte, mass_per_sig_op: consensus_params.mass_per_sig_op, storage_mass_parameter: consensus_params.storage_mass_parameter, - mass_combination_strategy: network_params.mass_combination_strategy(), + kip9_version: network_params.kip9_version(), } } @@ -243,44 +238,45 @@ impl MassCalculator { } } - pub fn calc_mass_for_transaction(&self, tx: &Transaction) -> u64 { - self.blank_transaction_mass() - + self.calc_mass_for_payload(tx.payload.len()) - + self.calc_mass_for_outputs(&tx.outputs) - + self.calc_mass_for_inputs(&tx.inputs) + pub fn calc_transaction_compute_mass(&self, tx: &Transaction) -> u64 { + let payload_len = tx.payload.len(); + self.blank_transaction_compute_mass() + + self.calc_compute_mass_for_payload(payload_len) + + self.calc_compute_mass_for_outputs(&tx.outputs) + + self.calc_compute_mass_for_inputs(&tx.inputs) } - pub fn blank_transaction_mass(&self) -> u64 { + pub(crate) fn blank_transaction_compute_mass(&self) -> u64 { blank_transaction_serialized_byte_size() * self.mass_per_tx_byte } - pub fn calc_mass_for_payload(&self, payload_byte_size: usize) -> u64 { + pub(crate) fn calc_compute_mass_for_payload(&self, payload_byte_size: usize) -> u64 { payload_byte_size as u64 * self.mass_per_tx_byte } - pub fn calc_mass_for_outputs(&self, outputs: &[TransactionOutput]) -> u64 { - outputs.iter().map(|output| self.calc_mass_for_output(output)).sum() + pub(crate) fn calc_compute_mass_for_outputs(&self, outputs: &[TransactionOutput]) -> u64 { + outputs.iter().map(|output| self.calc_compute_mass_for_output(output)).sum() } - pub fn calc_mass_for_inputs(&self, inputs: &[TransactionInput]) -> u64 { - inputs.iter().map(|input| self.calc_mass_for_input(input)).sum::() + pub(crate) fn calc_compute_mass_for_inputs(&self, inputs: &[TransactionInput]) -> u64 { + inputs.iter().map(|input| self.calc_compute_mass_for_input(input)).sum::() } - pub fn calc_mass_for_output(&self, output: &TransactionOutput) -> u64 { + pub(crate) fn calc_compute_mass_for_output(&self, output: &TransactionOutput) -> u64 { self.mass_per_script_pub_key_byte * (2 + output.script_public_key.script().len() as u64) + transaction_output_serialized_byte_size(output) * self.mass_per_tx_byte } - pub fn calc_mass_for_input(&self, input: &TransactionInput) -> u64 { + pub(crate) fn calc_compute_mass_for_input(&self, input: &TransactionInput) -> u64 { input.sig_op_count as u64 * self.mass_per_sig_op + transaction_input_serialized_byte_size(input) * self.mass_per_tx_byte } - pub fn calc_signature_mass(&self, minimum_signatures: u16) -> u64 { + pub(crate) fn calc_compute_mass_for_signature(&self, minimum_signatures: u16) -> u64 { let minimum_signatures = std::cmp::max(1, minimum_signatures); SIGNATURE_SIZE * self.mass_per_tx_byte * minimum_signatures as u64 } - pub fn calc_signature_mass_for_inputs(&self, number_of_inputs: usize, minimum_signatures: u16) -> u64 { + pub fn calc_signature_compute_mass_for_inputs(&self, number_of_inputs: usize, minimum_signatures: u16) -> u64 { let minimum_signatures = std::cmp::max(1, minimum_signatures); SIGNATURE_SIZE * self.mass_per_tx_byte * minimum_signatures as u64 * number_of_inputs as u64 } @@ -289,48 +285,66 @@ impl MassCalculator { calc_minimum_required_transaction_relay_fee(mass) } - pub fn calc_mass_for_signed_transaction(&self, tx: &Transaction, minimum_signatures: u16) -> u64 { - self.calc_mass_for_transaction(tx) + self.calc_signature_mass_for_inputs(tx.inputs.len(), minimum_signatures) + pub fn calc_compute_mass_for_signed_transaction(&self, tx: &Transaction, minimum_signatures: u16) -> u64 { + self.calc_transaction_compute_mass(tx) + self.calc_signature_compute_mass_for_inputs(tx.inputs.len(), minimum_signatures) } - pub fn calc_minium_transaction_relay_fee(&self, tx: &Transaction, minimum_signatures: u16) -> u64 { - let mass = self.calc_mass_for_transaction(tx) + self.calc_signature_mass_for_inputs(tx.inputs.len(), minimum_signatures); - calc_minimum_required_transaction_relay_fee(mass) + pub fn calc_transaction_storage_fee(&self, inputs: &[UtxoEntryReference], outputs: &[TransactionOutput]) -> u64 { + self.calc_fee_for_storage_mass(self.calc_storage_mass_for_transaction_parts(inputs, outputs).unwrap_or(u64::MAX)) } - pub fn calc_tx_storage_fee(&self, is_coinbase: bool, inputs: &[UtxoEntryReference], outputs: &[TransactionOutput]) -> u64 { - self.calc_fee_for_storage_mass(self.calc_storage_mass_for_transaction(is_coinbase, inputs, outputs).unwrap_or(u64::MAX)) + // provisional + pub fn calc_fee_for_storage_mass(&self, mass: u64) -> u64 { + mass } - pub fn calc_fee_for_storage_mass(&self, mass: u64) -> u64 { + // provisional + pub fn calc_fee_for_mass(&self, mass: u64) -> u64 { mass } pub fn combine_mass(&self, compute_mass: u64, storage_mass: u64) -> u64 { - match self.mass_combination_strategy { - MassCombinationStrategy::Add => compute_mass + storage_mass, - MassCombinationStrategy::Max => std::cmp::max(compute_mass, storage_mass), + match self.kip9_version { + Kip9Version::Alpha => compute_mass + storage_mass, + Kip9Version::Beta => std::cmp::max(compute_mass, storage_mass), } } - pub fn calc_storage_mass_for_transaction( + /// Calculates the overall mass of this transaction, combining both compute and storage masses. + pub fn calc_tx_overall_mass(&self, tx: &kcc::Transaction) -> Result> { + let cctx = Transaction::from(tx); + let mass = match self.kip9_version { + Kip9Version::Alpha => self + .calc_storage_mass_for_transaction(tx)? + .and_then(|mass| mass.checked_add(self.calc_transaction_compute_mass(&cctx))), + Kip9Version::Beta => { + self.calc_storage_mass_for_transaction(tx)?.map(|mass| mass.max(self.calc_transaction_compute_mass(&cctx))) + } + }; + + Ok(mass) + } + + pub fn calc_storage_mass_for_transaction(&self, tx: &kcc::Transaction) -> Result> { + let utxos = tx.utxo_entry_references()?; + let outputs = tx.outputs(); + Ok(self.calc_storage_mass_for_transaction_parts(&utxos, &outputs)) + } + + pub fn calc_storage_mass_for_transaction_parts( &self, - is_coinbase: bool, inputs: &[UtxoEntryReference], outputs: &[TransactionOutput], ) -> Option { - if is_coinbase { - return Some(0); - } /* The code below computes the following formula: - max( 0 , C·( |O|/H(O) - |I|/A(I) ) ) + max( 0 , C·( |O|/H(O) - |I|/A(I) ) ) where C is the mass storage parameter, O is the set of output values, I is the set of input values, H(S) := |S|/sum_{s in S} 1 / s is the harmonic mean over the set S and A(S) := sum_{s in S} / |S| is the arithmetic mean. - See the (to date unpublished) KIP-0009 for more details + See KIP-0009 for more details */ // Since we are doing integer division, we perform the multiplication with C over the inner @@ -338,15 +352,36 @@ impl MassCalculator { // // If sum of fractions overflowed (nearly impossible, requires 10^7 outputs for C = 10^12), // we return `None` indicating mass is incomputable + // + // Note: in theory this can be tighten by subtracting input mass in the process (possibly avoiding the overflow), + // however the overflow case is so unpractical with current mass limits so we avoid the hassle let harmonic_outs = outputs .iter() .map(|out| self.storage_mass_parameter / out.value) .try_fold(0u64, |total, current| total.checked_add(current))?; // C·|O|/H(O) + let outs_len = outputs.len() as u64; + let ins_len = inputs.len() as u64; + + /* + KIP-0009 relaxed formula for the cases |O| = 1 OR |O| <= |I| <= 2: + max( 0 , C·( |O|/H(O) - |I|/H(I) ) ) + + Note: in the case |I| = 1 both formulas are equal, yet the following code (harmonic_ins) is a bit more efficient. + Hence, we transform the condition to |O| = 1 OR |I| = 1 OR |O| = |I| = 2 which is equivalent (and faster). + */ + + if self.kip9_version == Kip9Version::Beta && (outs_len == 1 || ins_len == 1 || (outs_len == 2 && ins_len == 2)) { + let harmonic_ins = inputs + .iter() + .map(|entry| self.storage_mass_parameter / entry.amount()) + .fold(0u64, |total, current| total.saturating_add(current)); // C·|I|/H(I) + return Some(harmonic_outs.saturating_sub(harmonic_ins)); // max( 0 , C·( |O|/H(O) - |I|/H(I) ) ); + } + // Total supply is bounded, so a sum of existing UTXO entries cannot overflow (nor can it be zero) let sum_ins = inputs.iter().map(|entry| entry.amount()).sum::(); // |I|·A(I) - let ins_len = inputs.len() as u64; let mean_ins = sum_ins / ins_len; // Inner fraction must be with C and over the mean value, in order to maximize precision. diff --git a/wallet/core/src/utxo/context.rs b/wallet/core/src/utxo/context.rs index c65ea252b..e8d1ff39b 100644 --- a/wallet/core/src/utxo/context.rs +++ b/wallet/core/src/utxo/context.rs @@ -299,7 +299,7 @@ impl UtxoContext { context.mature.sorted_insert_binary_asc_by_key(utxo_entry.clone(), |entry| entry.amount_as_ref()); } else { let params = NetworkParams::from(self.processor().network_id()?); - match utxo_entry.maturity(¶ms, current_daa_score) { + match utxo_entry.maturity(params, current_daa_score) { Maturity::Stasis => { context.stasis.insert(utxo_entry.id().clone(), utxo_entry.clone()); self.processor() @@ -428,7 +428,7 @@ impl UtxoContext { for utxo_entry in utxo_entries.into_iter() { if let std::collections::hash_map::Entry::Vacant(e) = context.map.entry(utxo_entry.id()) { e.insert(utxo_entry.clone()); - match utxo_entry.maturity(¶ms, current_daa_score) { + match utxo_entry.maturity(params, current_daa_score) { Maturity::Stasis => { context.stasis.insert(utxo_entry.id().clone(), utxo_entry.clone()); self.processor() @@ -531,7 +531,7 @@ impl UtxoContext { let force_maturity_if_outgoing = outgoing_transaction.is_some(); let is_coinbase_stasis = - utxos.first().map(|utxo| matches!(utxo.maturity(¶ms, current_daa_score), Maturity::Stasis)).unwrap_or_default(); + utxos.first().map(|utxo| matches!(utxo.maturity(params, current_daa_score), Maturity::Stasis)).unwrap_or_default(); let is_batch = outgoing_transaction.as_ref().map_or_else(|| false, |tx| tx.is_batch()); if !is_batch { for utxo in utxos.iter() { diff --git a/wallet/core/src/utxo/processor.rs b/wallet/core/src/utxo/processor.rs index 28405887a..4195a00b5 100644 --- a/wallet/core/src/utxo/processor.rs +++ b/wallet/core/src/utxo/processor.rs @@ -181,10 +181,11 @@ impl UtxoProcessor { (*self.inner.network_id.lock().unwrap()).ok_or(Error::MissingNetworkId) } - // pub fn network_params(&self) -> Result<&'static NetworkParams> { - pub fn network_params(&self) -> Result { + pub fn network_params(&self) -> Result<&'static NetworkParams> { + // pub fn network_params(&self) -> Result { let network_id = (*self.inner.network_id.lock().unwrap()).ok_or(Error::MissingNetworkId)?; - Ok(network_id.into()) + Ok(NetworkParams::from(network_id)) + // Ok(network_id.into()) } pub fn pending(&self) -> &DashMap { @@ -275,7 +276,7 @@ impl UtxoProcessor { // scan and remove any pending entries that gained maturity let mut mature_entries = vec![]; let pending_entries = &self.inner.pending; - pending_entries.retain(|_, pending_entry| match pending_entry.maturity(¶ms, current_daa_score) { + pending_entries.retain(|_, pending_entry| match pending_entry.maturity(params, current_daa_score) { Maturity::Confirmed => { mature_entries.push(pending_entry.clone()); false @@ -288,7 +289,7 @@ impl UtxoProcessor { let mut revived_entries = vec![]; let stasis_entries = &self.inner.stasis; stasis_entries.retain(|_, stasis_entry| { - match stasis_entry.maturity(¶ms, current_daa_score) { + match stasis_entry.maturity(params, current_daa_score) { Maturity::Confirmed => { mature_entries.push(stasis_entry.clone()); false diff --git a/wallet/core/src/utxo/scan.rs b/wallet/core/src/utxo/scan.rs index fda8ea67c..f01257c96 100644 --- a/wallet/core/src/utxo/scan.rs +++ b/wallet/core/src/utxo/scan.rs @@ -107,7 +107,7 @@ impl Scan { } let balance: Balance = refs.iter().fold(Balance::default(), |mut balance, r| { - let entry_balance = r.balance(¶ms, self.current_daa_score); + let entry_balance = r.balance(params, self.current_daa_score); balance.mature += entry_balance.mature; balance.pending += entry_balance.pending; balance.mature_utxo_count += entry_balance.mature_utxo_count; @@ -151,7 +151,7 @@ impl Scan { let refs: Vec = resp.into_iter().map(UtxoEntryReference::from).collect(); let balance: Balance = refs.iter().fold(Balance::default(), |mut balance, r| { - let entry_balance = r.balance(¶ms, self.current_daa_score); + let entry_balance = r.balance(params, self.current_daa_score); balance.mature += entry_balance.mature; balance.pending += entry_balance.pending; balance.mature_utxo_count += entry_balance.mature_utxo_count; diff --git a/wallet/core/src/utxo/settings.rs b/wallet/core/src/utxo/settings.rs index caa8032eb..69e79a05d 100644 --- a/wallet/core/src/utxo/settings.rs +++ b/wallet/core/src/utxo/settings.rs @@ -4,128 +4,124 @@ //! use crate::imports::*; +use kaspa_consensus_core::mass::Kip9Version; #[derive(Debug)] -pub struct Inner { +pub struct NetworkParams { pub coinbase_transaction_maturity_period_daa: AtomicU64, pub coinbase_transaction_stasis_period_daa: u64, pub user_transaction_maturity_period_daa: AtomicU64, - pub mass_combination_strategy: MassCombinationStrategy, + pub kip9_version: Kip9Version, pub additional_compound_transaction_mass: u64, } -#[derive(Debug, Clone)] -pub struct NetworkParams { - inner: Arc, -} - impl NetworkParams { #[inline] pub fn coinbase_transaction_maturity_period_daa(&self) -> u64 { - self.inner.coinbase_transaction_maturity_period_daa.load(Ordering::Relaxed) + self.coinbase_transaction_maturity_period_daa.load(Ordering::Relaxed) } #[inline] pub fn coinbase_transaction_stasis_period_daa(&self) -> u64 { - self.inner.coinbase_transaction_stasis_period_daa + self.coinbase_transaction_stasis_period_daa } #[inline] pub fn user_transaction_maturity_period_daa(&self) -> u64 { - self.inner.user_transaction_maturity_period_daa.load(Ordering::Relaxed) + self.user_transaction_maturity_period_daa.load(Ordering::Relaxed) } #[inline] - pub fn mass_combination_strategy(&self) -> MassCombinationStrategy { - self.inner.mass_combination_strategy + pub fn kip9_version(&self) -> Kip9Version { + self.kip9_version } #[inline] pub fn additional_compound_transaction_mass(&self) -> u64 { - self.inner.additional_compound_transaction_mass + self.additional_compound_transaction_mass } pub fn set_coinbase_transaction_maturity_period_daa(&self, value: u64) { - self.inner.coinbase_transaction_maturity_period_daa.store(value, Ordering::Relaxed); + self.coinbase_transaction_maturity_period_daa.store(value, Ordering::Relaxed); } pub fn set_user_transaction_maturity_period_daa(&self, value: u64) { - self.inner.user_transaction_maturity_period_daa.store(value, Ordering::Relaxed); + self.user_transaction_maturity_period_daa.store(value, Ordering::Relaxed); } } -lazy_static::lazy_static! { - pub static ref MAINNET_NETWORK_PARAMS: NetworkParams = NetworkParams { - inner: Arc::new(Inner { - coinbase_transaction_maturity_period_daa: AtomicU64::new(100), - coinbase_transaction_stasis_period_daa: 50, - user_transaction_maturity_period_daa: AtomicU64::new(10), - mass_combination_strategy: MassCombinationStrategy::Max, - additional_compound_transaction_mass: 0, - }), - }; +static MAINNET_NETWORK_PARAMS: OnceLock = OnceLock::new(); + +pub fn mainnet_network_params() -> &'static NetworkParams { + MAINNET_NETWORK_PARAMS.get_or_init(|| NetworkParams { + coinbase_transaction_maturity_period_daa: AtomicU64::new(100), + coinbase_transaction_stasis_period_daa: 50, + user_transaction_maturity_period_daa: AtomicU64::new(10), + kip9_version: Kip9Version::Beta, + additional_compound_transaction_mass: 100, + }) } -lazy_static::lazy_static! { - pub static ref TESTNET10_NETWORK_PARAMS: NetworkParams = NetworkParams { - inner: Arc::new(Inner { - coinbase_transaction_maturity_period_daa: AtomicU64::new(100), - coinbase_transaction_stasis_period_daa: 50, - user_transaction_maturity_period_daa: AtomicU64::new(10), - mass_combination_strategy: MassCombinationStrategy::Max, - additional_compound_transaction_mass: 0, - }), - }; +static TESTNET10_NETWORK_PARAMS: OnceLock = OnceLock::new(); + +pub fn testnet10_network_params() -> &'static NetworkParams { + TESTNET10_NETWORK_PARAMS.get_or_init(|| NetworkParams { + coinbase_transaction_maturity_period_daa: AtomicU64::new(100), + coinbase_transaction_stasis_period_daa: 50, + user_transaction_maturity_period_daa: AtomicU64::new(10), + kip9_version: Kip9Version::Beta, + additional_compound_transaction_mass: 100, + }) } -lazy_static::lazy_static! { - pub static ref TESTNET11_NETWORK_PARAMS: NetworkParams = NetworkParams { - inner: Arc::new(Inner { - coinbase_transaction_maturity_period_daa: AtomicU64::new(1_000), - coinbase_transaction_stasis_period_daa: 500, - user_transaction_maturity_period_daa: AtomicU64::new(100), - mass_combination_strategy: MassCombinationStrategy::Max, - additional_compound_transaction_mass: 100, - }), - }; +static TESTNET11_NETWORK_PARAMS: OnceLock = OnceLock::new(); + +pub fn testnet11_network_params() -> &'static NetworkParams { + TESTNET11_NETWORK_PARAMS.get_or_init(|| NetworkParams { + coinbase_transaction_maturity_period_daa: AtomicU64::new(1_000), + coinbase_transaction_stasis_period_daa: 500, + user_transaction_maturity_period_daa: AtomicU64::new(100), + kip9_version: Kip9Version::Alpha, + additional_compound_transaction_mass: 100, + }) } -lazy_static::lazy_static! { - pub static ref SIMNET_NETWORK_PARAMS: NetworkParams = NetworkParams { - inner: Arc::new(Inner { - coinbase_transaction_maturity_period_daa: AtomicU64::new(100), - coinbase_transaction_stasis_period_daa: 50, - user_transaction_maturity_period_daa: AtomicU64::new(10), - mass_combination_strategy: MassCombinationStrategy::Max, - additional_compound_transaction_mass: 0, - }), - }; +static SIMNET_NETWORK_PARAMS: OnceLock = OnceLock::new(); + +pub fn simnet_network_params() -> &'static NetworkParams { + SIMNET_NETWORK_PARAMS.get_or_init(|| NetworkParams { + coinbase_transaction_maturity_period_daa: AtomicU64::new(100), + coinbase_transaction_stasis_period_daa: 50, + user_transaction_maturity_period_daa: AtomicU64::new(10), + kip9_version: Kip9Version::Alpha, + additional_compound_transaction_mass: 0, + }) } -lazy_static::lazy_static! { - pub static ref DEVNET_NETWORK_PARAMS: NetworkParams = NetworkParams { - inner: Arc::new(Inner { - coinbase_transaction_maturity_period_daa: AtomicU64::new(100), - coinbase_transaction_stasis_period_daa: 50, - user_transaction_maturity_period_daa: AtomicU64::new(10), - mass_combination_strategy: MassCombinationStrategy::Max, - additional_compound_transaction_mass: 0, - }), - }; +static DEVNET_NETWORK_PARAMS: OnceLock = OnceLock::new(); + +pub fn devnet_network_params() -> &'static NetworkParams { + DEVNET_NETWORK_PARAMS.get_or_init(|| NetworkParams { + coinbase_transaction_maturity_period_daa: AtomicU64::new(100), + coinbase_transaction_stasis_period_daa: 50, + user_transaction_maturity_period_daa: AtomicU64::new(10), + kip9_version: Kip9Version::Beta, + additional_compound_transaction_mass: 0, + }) } -impl From for NetworkParams { - fn from(value: NetworkId) -> Self { +impl NetworkParams { + pub fn from(value: NetworkId) -> &'static NetworkParams { match value.network_type { - NetworkType::Mainnet => MAINNET_NETWORK_PARAMS.clone(), + NetworkType::Mainnet => mainnet_network_params(), NetworkType::Testnet => match value.suffix { - Some(10) => TESTNET10_NETWORK_PARAMS.clone(), - Some(11) => TESTNET11_NETWORK_PARAMS.clone(), + Some(10) => testnet10_network_params(), + Some(11) => testnet11_network_params(), Some(x) => panic!("Testnet suffix {} is not supported", x), None => panic!("Testnet suffix not provided"), }, - NetworkType::Devnet => DEVNET_NETWORK_PARAMS.clone(), - NetworkType::Simnet => SIMNET_NETWORK_PARAMS.clone(), + NetworkType::Devnet => devnet_network_params(), + NetworkType::Simnet => simnet_network_params(), } } } diff --git a/wallet/core/src/wasm/signer.rs b/wallet/core/src/wasm/signer.rs index 106988a3c..9e0ce26b2 100644 --- a/wallet/core/src/wasm/signer.rs +++ b/wallet/core/src/wasm/signer.rs @@ -53,7 +53,7 @@ pub fn js_sign_transaction(tx: Transaction, signer: PrivateKeyArrayT, verify_sig pub fn sign_transaction(tx: Transaction, private_keys: &[[u8; 32]], verify_sig: bool) -> Result { let tx = sign(tx, private_keys)?; if verify_sig { - let (cctx, utxos) = tx.tx_and_utxos(); + let (cctx, utxos) = tx.tx_and_utxos()?; let populated_transaction = PopulatedTransaction::new(&cctx, utxos); verify(&populated_transaction)?; } @@ -76,7 +76,7 @@ pub fn create_input_signature( private_key: &PrivateKey, sighash_type: Option, ) -> Result { - let (cctx, _) = tx.tx_and_utxos(); + let (cctx, _) = tx.tx_and_utxos()?; let mutable_tx = SignableTransaction::new(cctx); let signature = diff --git a/wallet/core/src/wasm/tx/consensus.rs b/wallet/core/src/wasm/tx/consensus.rs deleted file mode 100644 index b1e722a61..000000000 --- a/wallet/core/src/wasm/tx/consensus.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::tx::consensus as core; -use kaspa_addresses::Address; -use kaspa_consensus_core::{config::params::Params, network::NetworkType}; -use wasm_bindgen::prelude::*; - -/// -/// `ConsensusParams` can be obtained using `getConsensusParametersByNetwork` or `getConsensusParametersByAddress`. -/// -/// @see {@link getConsensusParametersByNetwork} -/// @see {@link getConsensusParametersByAddress} -/// -/// @category Wallet SDK -/// -#[wasm_bindgen] -pub struct ConsensusParams { - params: Params, -} - -impl From for ConsensusParams { - fn from(params: Params) -> Self { - Self { params } - } -} - -impl From for Params { - fn from(cp: ConsensusParams) -> Self { - cp.params - } -} - -/// find Consensus parameters for given Address -/// @category Wallet SDK -#[wasm_bindgen(js_name = getConsensusParametersByAddress)] -pub fn get_consensus_params_by_address(address: &Address) -> ConsensusParams { - core::get_consensus_params_by_address(address).into() -} - -/// find Consensus parameters for given NetworkType -/// @category Wallet SDK -#[wasm_bindgen(js_name = getConsensusParametersByNetwork)] -pub fn get_consensus_params_by_network(network: NetworkType) -> ConsensusParams { - core::get_consensus_params_by_network(&network).into() -} diff --git a/wallet/core/src/wasm/tx/mass.rs b/wallet/core/src/wasm/tx/mass.rs index cc522fd8e..5dd09051f 100644 --- a/wallet/core/src/wasm/tx/mass.rs +++ b/wallet/core/src/wasm/tx/mass.rs @@ -1,151 +1,43 @@ use crate::imports::NetworkParams; use crate::result::Result; use crate::tx::mass; -use crate::wasm::tx::*; use kaspa_consensus_client::*; use kaspa_consensus_core::config::params::Params; -use kaspa_consensus_core::tx as cctx; -use std::sync::Arc; +use kaspa_consensus_core::network::{NetworkId, NetworkIdT}; use wasm_bindgen::prelude::*; use workflow_wasm::convert::*; +/// `calculateTransactionMass()` returns the mass of the passed transaction. +/// If the transaction is invalid, the function throws an error. +/// If the mass is larger than the transaction mass allowed by the network, the function +/// returns `undefined` which can be treated as a mass overflow condition. +/// /// @category Wallet SDK -#[wasm_bindgen] -pub struct MassCalculator { - mc: Arc, +/// +#[wasm_bindgen(js_name = calculateTransactionMass)] +pub fn calculate_transaction_mass(network_id: NetworkIdT, tx: &TransactionT) -> Result> { + let tx = Transaction::try_cast_from(tx)?; + let network_id = NetworkId::try_owned_from(network_id)?; + let consensus_params = Params::from(network_id); + let network_params = NetworkParams::from(network_id); + let mc = mass::MassCalculator::new(&consensus_params, network_params); + mc.calc_tx_overall_mass(tx.as_ref()) } -#[wasm_bindgen] -impl MassCalculator { - #[wasm_bindgen(constructor)] - pub fn new(cp: ConsensusParams) -> Self { - let consensus_params = Params::from(cp); - let network_params = NetworkParams::from(consensus_params.net); - Self { mc: Arc::new(mass::MassCalculator::new(&consensus_params, &network_params)) } - } - - #[wasm_bindgen(js_name=isDust)] - pub fn is_dust(&self, amount: u64) -> bool { - self.mc.is_dust(amount) - } - - /// `isTransactionOutputDust()` returns whether or not the passed transaction output - /// amount is considered dust or not based on the configured minimum transaction - /// relay fee. - /// - /// Dust is defined in terms of the minimum transaction relay fee. In particular, - /// if the cost to the network to spend coins is more than 1/3 of the minimum - /// transaction relay fee, it is considered dust. - /// - /// It is exposed by `MiningManager` for use by transaction generators and wallets. - #[wasm_bindgen(js_name=isTransactionOutputDust)] - pub fn is_transaction_output_dust(transaction_output: &JsValue) -> Result { - let transaction_output = TransactionOutput::try_from(transaction_output)?; - let transaction_output = cctx::TransactionOutput::from(&transaction_output); - Ok(mass::is_transaction_output_dust(&transaction_output)) - } - - /// `minimumRelayTransactionFee()` specifies the minimum transaction fee for a transaction to be accepted to - /// the mempool and relayed. It is specified in sompi per 1kg (or 1000 grams) of transaction mass. - /// - /// `pub(crate) const MINIMUM_RELAY_TRANSACTION_FEE: u64 = 1000;` - #[wasm_bindgen(js_name=minimumRelayTransactionFee)] - pub fn minimum_relay_transaction_fee() -> u32 { - mass::MINIMUM_RELAY_TRANSACTION_FEE as u32 - } - - /// `maximumStandardTransactionMass()` is the maximum mass allowed for transactions that - /// are considered standard and will therefore be relayed and considered for mining. - /// - /// `pub const MAXIMUM_STANDARD_TRANSACTION_MASS: u64 = 100_000;` - #[wasm_bindgen(js_name=maximumStandardTransactionMass)] - pub fn maximum_standard_transaction_mass() -> u32 { - mass::MAXIMUM_STANDARD_TRANSACTION_MASS as u32 - } - - /// minimum_required_transaction_relay_fee returns the minimum transaction fee required - /// for a transaction with the passed mass to be accepted into the mempool and relayed. - #[wasm_bindgen(js_name=minimumRequiredTransactionRelayFee)] - pub fn calc_minimum_required_transaction_relay_fee(mass: u32) -> u32 { - mass::calc_minimum_required_transaction_relay_fee(mass as u64) as u32 - } - - #[wasm_bindgen(js_name=calcMassForTransaction)] - pub fn calc_mass_for_transaction(&self, tx: &JsValue) -> Result { - let tx = Transaction::try_cast_from(tx)?; - let tx = cctx::Transaction::from(tx.as_ref()); - Ok(self.mc.calc_mass_for_transaction(&tx) as u32) - } - - #[wasm_bindgen(js_name=blankTransactionSerializedByteSize)] - pub fn blank_transaction_serialized_byte_size() -> u32 { - mass::blank_transaction_serialized_byte_size() as u32 - } - - #[wasm_bindgen(js_name=blankTransactionMass)] - pub fn blank_transaction_mass(&self) -> u32 { - self.mc.blank_transaction_mass() as u32 - } - - #[wasm_bindgen(js_name=calcMassForPayload)] - pub fn calc_mass_for_payload(&self, payload_byte_size: usize) -> u32 { - self.mc.calc_mass_for_payload(payload_byte_size) as u32 - } - - #[wasm_bindgen(js_name=calcMassForOutputs)] - pub fn calc_mass_for_outputs(&self, outputs: JsValue) -> Result { - let outputs = outputs - .dyn_into::()? - .iter() - .map(TransactionOutput::try_from) - .collect::, kaspa_consensus_client::error::Error>>()?; - let outputs = outputs.iter().map(|output| self.calc_mass_for_output(output)).collect::>>()?; - Ok(outputs.iter().sum()) - } - - #[wasm_bindgen(js_name=calcMassForInputs)] - pub fn calc_mass_for_inputs(&self, inputs: JsValue) -> Result { - let inputs = inputs - .dyn_into::()? - .iter() - .map(TransactionInput::try_owned_from) - .collect::, kaspa_consensus_client::error::Error>>()?; - let inputs = inputs.iter().map(|input| self.calc_mass_for_input(input)).collect::>>()?; - Ok(inputs.iter().sum()) - } - - #[wasm_bindgen(js_name=calcMassForOutput)] - pub fn calc_mass_for_output(&self, output: &TransactionOutput) -> Result { - // let output = TransactionOutput::try_from(output)?; - let output = cctx::TransactionOutput::from(output); - Ok(self.mc.calc_mass_for_output(&output) as u32) - } - - #[wasm_bindgen(js_name=calcMassForInput)] - pub fn calc_mass_for_input(&self, input: &TransactionInput) -> Result { - // let input = TransactionInput::try_from(input)?; - let input = cctx::TransactionInput::from(input); - Ok(self.mc.calc_mass_for_input(&input) as u32) - } - - #[wasm_bindgen(js_name=calcSignatureMass)] - pub fn calc_signature_mass(&self, minimum_signatures: u16) -> u32 { - self.mc.calc_signature_mass(minimum_signatures) as u32 - } - - #[wasm_bindgen(js_name=calcSignatureMassForInputs)] - pub fn calc_signature_mass_for_inputs(&self, number_of_inputs: usize, minimum_signatures: u16) -> u32 { - self.mc.calc_signature_mass_for_inputs(number_of_inputs, minimum_signatures) as u32 - } - - #[wasm_bindgen(js_name=calcMinimumTransactionRelayFeeFromMass)] - pub fn calc_minimum_transaction_relay_fee_from_mass(&self, mass: u64) -> u32 { - self.mc.calc_minimum_transaction_fee_from_mass(mass) as u32 - } - - #[wasm_bindgen(js_name=calcMiniumTxRelayFee)] - pub fn calc_minimum_transaction_relay_fee(&self, transaction: &Transaction, minimum_signatures: u16) -> Result { - let tx = cctx::Transaction::from(transaction); - Ok(self.mc.calc_minium_transaction_relay_fee(&tx, minimum_signatures) as u32) - } +/// `calculateTransactionFee()` returns minimum fees needed for the transaction to be +/// accepted by the network. If the transaction is invalid, the function throws an error. +/// If the mass of the transaction is larger than the maximum allowed by the network, the +/// function returns `undefined` which can be treated as a mass overflow condition. +/// +/// @category Wallet SDK +/// +#[wasm_bindgen(js_name = calculateTransactionFee)] +pub fn calculate_transaction_fee(network_id: NetworkIdT, tx: &TransactionT) -> Result> { + let tx = Transaction::try_cast_from(tx)?; + let network_id = NetworkId::try_owned_from(network_id)?; + let consensus_params = Params::from(network_id); + let network_params = NetworkParams::from(network_id); + let mc = mass::MassCalculator::new(&consensus_params, network_params); + let fee = mc.calc_tx_overall_mass(tx.as_ref())?.map(|mass| mc.calc_fee_for_mass(mass)); + Ok(fee) } diff --git a/wallet/core/src/wasm/tx/mod.rs b/wallet/core/src/wasm/tx/mod.rs index df826a997..742bb89ca 100644 --- a/wallet/core/src/wasm/tx/mod.rs +++ b/wallet/core/src/wasm/tx/mod.rs @@ -1,10 +1,8 @@ -pub mod consensus; pub mod fees; pub mod generator; pub mod mass; pub mod utils; -pub use self::consensus::*; pub use self::fees::*; pub use self::generator::*; pub use self::mass::*; diff --git a/wallet/core/src/wasm/tx/utils.rs b/wallet/core/src/wasm/tx/utils.rs index 0d911af76..352966a43 100644 --- a/wallet/core/src/wasm/tx/utils.rs +++ b/wallet/core/src/wasm/tx/utils.rs @@ -1,14 +1,11 @@ use crate::imports::*; use crate::result::Result; use crate::tx::{IPaymentOutputArray, PaymentOutputs}; -use crate::wasm::tx::consensus::get_consensus_params_by_address; use crate::wasm::tx::generator::*; -use crate::wasm::tx::mass::MassCalculator; -use kaspa_addresses::{Address, AddressT}; use kaspa_consensus_client::*; use kaspa_consensus_core::subnets::SUBNETWORK_ID_NATIVE; -//use kaspa_consensus_wasm::*; use kaspa_wallet_macros::declare_typescript_wasm_interface as declare; +use kaspa_wasm_core::types::BinaryT; use workflow_core::runtime::is_web; /// Create a basic transaction without any mass limit checks. @@ -17,16 +14,10 @@ use workflow_core::runtime::is_web; pub fn create_transaction_js( utxo_entry_source: IUtxoEntryArray, outputs: IPaymentOutputArray, - change_address: AddressT, priority_fee: BigInt, - payload: JsValue, - sig_op_count: JsValue, - minimum_signatures: JsValue, + payload: BinaryT, + sig_op_count: Option, ) -> crate::result::Result { - let change_address = Address::try_cast_from(change_address)?; - let params = get_consensus_params_by_address(change_address.as_ref()); - let mc = MassCalculator::new(params); - let utxo_entries = if let Some(utxo_entries) = utxo_entry_source.dyn_ref::() { utxo_entries.to_vec().iter().map(UtxoEntryReference::try_cast_from).collect::, _>>()? } else { @@ -35,14 +26,7 @@ pub fn create_transaction_js( let priority_fee: u64 = priority_fee.try_into().map_err(|err| Error::custom(format!("invalid fee value: {err}")))?; let payload = payload.try_as_vec_u8().ok().unwrap_or_default(); let outputs = PaymentOutputs::try_owned_from(outputs)?; - let sig_op_count = - if !sig_op_count.is_undefined() { sig_op_count.as_f64().expect("sigOpCount should be a number") as u8 } else { 1 }; - - let minimum_signatures = if !minimum_signatures.is_undefined() { - minimum_signatures.as_f64().expect("minimumSignatures should be a number") as u16 - } else { - 1 - }; + let sig_op_count = sig_op_count.unwrap_or(1); // --- @@ -64,12 +48,8 @@ pub fn create_transaction_js( return Err(format!("priority fee({priority_fee}) > amount({total_input_amount})").into()); } - // TODO - Calculate mass and fees - let outputs: Vec = outputs.into(); let transaction = Transaction::new(None, 0, inputs, outputs, 0, SUBNETWORK_ID_NATIVE, 0, payload)?; - let _fee = mc.calc_minimum_transaction_relay_fee(&transaction, minimum_signatures); - //let mtx = SignableTransaction::new(transaction, entries.into()); Ok(transaction) } diff --git a/wallet/keys/src/xprv.rs b/wallet/keys/src/xprv.rs index c9a6bf18d..1f478140e 100644 --- a/wallet/keys/src/xprv.rs +++ b/wallet/keys/src/xprv.rs @@ -1,4 +1,4 @@ -use kaspa_bip32::{ChainCode, KeyFingerprint, PrivateKey}; +use kaspa_bip32::{ChainCode, KeyFingerprint}; use crate::imports::*; @@ -73,6 +73,12 @@ impl XPrv { Ok(public_key.into()) } + #[wasm_bindgen(js_name = toPrivateKey)] + pub fn to_private_key(&self) -> Result { + let private_key = self.inner.private_key(); + Ok(private_key.into()) + } + // ~~~~ Getters ~~~~ #[wasm_bindgen(getter)] @@ -83,6 +89,7 @@ impl XPrv { #[wasm_bindgen(getter, js_name = "privateKey")] pub fn private_key_as_hex_string(&self) -> String { + use kaspa_bip32::PrivateKey; self.inner.private_key().to_bytes().to_vec().to_hex() } diff --git a/wasm/CHANGELOG.md b/wasm/CHANGELOG.md index 71b6375d2..d572dfd1d 100644 --- a/wasm/CHANGELOG.md +++ b/wasm/CHANGELOG.md @@ -2,6 +2,12 @@ Latest online documentation available at: https://kaspa.aspectron.org/docs/ ### Latest Release +- Replace `MassCalculator` with `calculateTransactionMass` and `calculateTransactionFee` functions. +- Change `createTransaction` function signature (remove requirement for change address). +- Make `ITransactionInput.signatureScript` optional (if not supplied, the signatureScript is assigned an empty vector). + +### Release 2024-07-17 + - Fix issues with deserializing manually-created objects matching `IUtxoEntry` interface. - Allow arguments expecting ScriptPublicKey to receive `{ version, script }` object or a hex string. - Fix `Transaction::serializeToObject()` return type (now returning `ISerializeTransaction` interface). diff --git a/wasm/examples/nodejs/javascript/general/derivation.js b/wasm/examples/nodejs/javascript/general/derivation.js index f92508c88..942f665f9 100644 --- a/wasm/examples/nodejs/javascript/general/derivation.js +++ b/wasm/examples/nodejs/javascript/general/derivation.js @@ -21,6 +21,7 @@ kaspa.initConsolePanicHook(); let xPrv = new XPrv(seed); // derive full path upto second address of receive wallet let pubkey1 = xPrv.derivePath("m/44'/111111'/0'/0/1").toXPub().toPublicKey(); + console.log("publickey", pubkey1.toString()) console.log("address", pubkey1.toAddress(NetworkType.Mainnet)); // create receive wallet @@ -28,17 +29,25 @@ kaspa.initConsolePanicHook(); // derive receive wallet for second address let pubkey2 = receiveWalletXPub.deriveChild(1, false).toPublicKey(); console.log("address", pubkey2.toAddress(NetworkType.Mainnet)); + if (pubkey1.toString() != pubkey2.toString()){ + throw new Error("pubkey2 dont match") + } // create change wallet let changeWalletXPub = xPrv.derivePath("m/44'/111111'/0'/1").toXPub(); // derive change wallet for first address let pubkey3 = changeWalletXPub.deriveChild(0, false).toPublicKey(); - console.log("address", pubkey2.toAddress(NetworkType.Mainnet)); + console.log("change address", pubkey3.toAddress(NetworkType.Mainnet)); + // --- - if (pubkey1.toString() != pubkey2.toString()){ - throw new Error("pubkeyes dont match") + //drive address via private key + let privateKey = xPrv.derivePath("m/44'/111111'/0'/0/1").toPrivateKey(); + console.log("address via private key", privateKey.toAddress(NetworkType.Mainnet)) + console.log("privatekey", privateKey.toString()); + let pubkey4 = privateKey.toPublicKey(); + if (pubkey1.toString() != pubkey4.toString()){ + throw new Error("pubkey4 dont match") } - // --- // xprv with ktrv prefix const ktrv = xPrv.intoString("ktrv");