diff --git a/consensus/client/src/utxo.rs b/consensus/client/src/utxo.rs index c910cc4..b301f51 100644 --- a/consensus/client/src/utxo.rs +++ b/consensus/client/src/utxo.rs @@ -445,7 +445,7 @@ impl UtxoEntryReference { let outpoint = TransactionOutpoint::simulated(); let script_public_key = spectre_txscript::pay_to_address_script(address); let block_daa_score = 0; - let is_coinbase = true; + let is_coinbase = false; let utxo_entry = UtxoEntry { address: Some(address.clone()), outpoint, amount, script_public_key, block_daa_score, is_coinbase }; diff --git a/rpc/core/src/wasm/convert.rs b/rpc/core/src/wasm/convert.rs index 01f480c..9e265a8 100644 --- a/rpc/core/src/wasm/convert.rs +++ b/rpc/core/src/wasm/convert.rs @@ -67,7 +67,7 @@ cfg_if::cfg_if! { subnetwork_id: inner.subnetwork_id.clone(), gas: inner.gas, payload: inner.payload.clone(), - mass: tx.get_mass(), + mass: inner.mass, verbose_data: None, } } diff --git a/rpc/core/src/wasm/message.rs b/rpc/core/src/wasm/message.rs index b4dc5ff..dd08acc 100644 --- a/rpc/core/src/wasm/message.rs +++ b/rpc/core/src/wasm/message.rs @@ -1454,8 +1454,8 @@ try_from! ( args: ISubmitTransactionRequest, SubmitTransactionRequest, { } else { let tx = Transaction::try_cast_from(&transaction)?; SubmitTransactionRequest { - transaction : tx.as_ref().into(), - allow_orphan, + transaction : tx.as_ref().into(), + allow_orphan, } }; Ok(request) diff --git a/wallet/core/src/tx/generator/generator.rs b/wallet/core/src/tx/generator/generator.rs index 8904757..24112d3 100644 --- a/wallet/core/src/tx/generator/generator.rs +++ b/wallet/core/src/tx/generator/generator.rs @@ -703,7 +703,6 @@ impl Generator { Ok((DataKind::NoOp, data)) } else if stage.number_of_transactions > 0 { data.aggregate_mass += self.inner.standard_change_output_compute_mass; - data.change_output_value = Some(data.aggregate_input_value - data.transaction_fees); Ok((DataKind::Edge, data)) } else if data.aggregate_input_value < data.transaction_fees { Err(Error::InsufficientFunds { additional_needed: data.transaction_fees - data.aggregate_input_value, origin: "relay" }) diff --git a/wallet/core/src/tx/generator/test.rs b/wallet/core/src/tx/generator/test.rs index 3c25824..f028ef7 100644 --- a/wallet/core/src/tx/generator/test.rs +++ b/wallet/core/src/tx/generator/test.rs @@ -16,7 +16,7 @@ use workflow_log::style; use super::*; -const DISPLAY_LOGS: bool = true; +const DISPLAY_LOGS: bool = false; const DISPLAY_EXPECTED: bool = true; #[derive(Clone, Copy, Debug)] @@ -173,7 +173,7 @@ fn validate(pt: &PendingTransaction) { let compute_mass = calc.calc_compute_mass_for_unsigned_consensus_transaction(&tx, pt.minimum_signatures()); let utxo_entries = pt.utxo_entries().values().cloned().collect::>(); - let storage_mass = calc.calc_storage_mass_for_transaction_parts(&utxo_entries, &tx.outputs).unwrap_or_default(); + let storage_mass = calc.calc_storage_mass_for_transaction_parts(&utxo_entries, &tx.outputs).unwrap_or(u64::MAX); let calculated_mass = calc.combine_mass(compute_mass, storage_mass) + additional_mass; assert_eq!(pt.inner.mass, calculated_mass, "pending transaction mass does not match calculated mass"); @@ -203,7 +203,7 @@ where let compute_mass = calc.calc_compute_mass_for_unsigned_consensus_transaction(&tx, pt.minimum_signatures()); let utxo_entries = pt.utxo_entries().values().cloned().collect::>(); - let storage_mass = calc.calc_storage_mass_for_transaction_parts(&utxo_entries, &tx.outputs).unwrap_or_default(); + let storage_mass = calc.calc_storage_mass_for_transaction_parts(&utxo_entries, &tx.outputs).unwrap_or(u64::MAX); if DISPLAY_LOGS && storage_mass != 0 { println!("calculated storage mass: {} calculated_compute_mass: {}", storage_mass, compute_mass,); } @@ -323,6 +323,21 @@ impl Harness { self.clone() } + pub fn accumulate(self: &Rc, count: usize) -> Rc { + for _n in 0..count { + if DISPLAY_LOGS { + println!( + "{}", + style(format!("accumulate gathering transaction: {} ({})", _n, self.accumulator.borrow().list.len())).magenta() + ); + } + let ptx = self.generator.generate_transaction().unwrap().unwrap(); + ptx.accumulate(&mut self.accumulator.borrow_mut()); + } + // println!("accumulated `{}` transactions", self.accumulator.borrow().list.len()); + self.clone() + } + pub fn validate(self: &Rc) -> Rc { while let Some(pt) = self.generator.generate_transaction().unwrap() { pt.accumulate(&mut self.accumulator.borrow_mut()).validate(); @@ -332,7 +347,16 @@ impl Harness { pub fn finalize(self: Rc) { let pt = self.generator.generate_transaction().unwrap(); - assert!(pt.is_none(), "expected no more transactions"); + if pt.is_some() { + let mut pending = self.generator.generate_transaction().unwrap(); + let mut count = 1; + while pending.is_some() { + count += 1; + pending = self.generator.generate_transaction().unwrap(); + } + + panic!("received extra `{}` unexpected transactions", count); + } let summary = self.generator.summary(); if DISPLAY_LOGS { println!("{:#?}", summary); @@ -652,7 +676,7 @@ fn test_generator_inputs_100_outputs_1_fees_exclude_insufficient_funds() -> Resu } #[test] -fn test_generator_inputs_903_outputs_2_fees_exclude() -> Result<()> { +fn test_generator_inputs_1k_outputs_2_fees_exclude() -> Result<()> { generator(test_network_id(), &[10.0; 1_000], &[], Fees::sender(Spectre(5.0)), [(output_address, Spectre(9_000.0))].as_slice()) .unwrap() .harness() @@ -684,3 +708,28 @@ fn test_generator_inputs_903_outputs_2_fees_exclude() -> Result<()> { Ok(()) } + +#[test] +fn test_generator_inputs_32k_outputs_2_fees_exclude() -> Result<()> { + let f = 130.0; + generator( + test_network_id(), + &[f; 32_747], + &[], + Fees::sender(Spectre(10_000.0)), + [(output_address, Spectre(f * 32_747.0 - 10_001.0))].as_slice(), + ) + .unwrap() + .harness() + .accumulate(379) + .finalize(); + Ok(()) +} + +#[test] +fn test_generator_inputs_250k_outputs_2_sweep() -> Result<()> { + let f = 130.0; + let generator = make_generator(test_network_id(), &[f; 250_000], &[], Fees::None, change_address, PaymentDestination::Change); + generator.unwrap().harness().accumulate(2875).finalize(); + Ok(()) +} diff --git a/wallet/core/src/wasm/tx/mass.rs b/wallet/core/src/wasm/tx/mass.rs index 9adad5d..5865152 100644 --- a/wallet/core/src/wasm/tx/mass.rs +++ b/wallet/core/src/wasm/tx/mass.rs @@ -1,17 +1,33 @@ use crate::imports::NetworkParams; use crate::result::Result; -use crate::tx::mass; +use crate::tx::{mass, MAXIMUM_STANDARD_TRANSACTION_MASS}; use spectre_consensus_client::*; use spectre_consensus_core::config::params::Params; use spectre_consensus_core::network::{NetworkId, NetworkIdT}; use wasm_bindgen::prelude::*; use workflow_wasm::convert::*; +/// `maximumStandardTransactionMass()` returns the maximum transaction +/// size allowed by the network. +/// +/// @category Wallet SDK +/// @see {@link calculateTransactionMass} +/// @see {@link updateTransactionMass} +/// @see {@link calculateTransactionFee} +#[wasm_bindgen(js_name = maximumStandardTransactionMass)] +pub fn maximum_standard_transaction_mass() -> u64 { + MAXIMUM_STANDARD_TRANSACTION_MASS +} + /// `calculateTransactionMass()` returns the mass of the passed transaction. /// If the transaction is invalid, or the mass can not be calculated /// the function throws an error. /// +/// The mass value must not exceed the maximum standard transaction mass +/// that can be obtained using `maximumStandardTransactionMass()`. +/// /// @category Wallet SDK +/// @see {@link maximumStandardTransactionMass} /// #[wasm_bindgen(js_name = calculateTransactionMass)] pub fn calculate_unsigned_transaction_mass(network_id: NetworkIdT, tx: &TransactionT, minimum_signatures: Option) -> Result { @@ -24,39 +40,59 @@ pub fn calculate_unsigned_transaction_mass(network_id: NetworkIdT, tx: &Transact } /// `updateTransactionMass()` updates the mass property of the passed transaction. -/// If the transaction is invalid, or the mass is larger than transaction mass allowed -/// by the network, the function throws an error. +/// If the transaction is invalid, the function throws an error. +/// +/// The function returns `true` if the mass is within the maximum standard transaction mass and +/// the transaction mass is updated. Otherwise, the function returns `false`. /// -/// This is the same as `calculateTransactionMass()` but modifies the supplied +/// This is similar to `calculateTransactionMass()` but modifies the supplied /// `Transaction` object. /// /// @category Wallet SDK +/// @see {@link maximumStandardTransactionMass} +/// @see {@link calculateTransactionMass} +/// @see {@link calculateTransactionFee} /// #[wasm_bindgen(js_name = updateTransactionMass)] -pub fn update_unsigned_transaction_mass(network_id: NetworkIdT, tx: &Transaction, minimum_signatures: Option) -> Result<()> { +pub fn update_unsigned_transaction_mass(network_id: NetworkIdT, tx: &Transaction, minimum_signatures: Option) -> Result { 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 mass = mc.calc_overall_mass_for_unsigned_client_transaction(tx, minimum_signatures.unwrap_or(1))?; - tx.set_mass(mass); - Ok(()) + if mass > MAXIMUM_STANDARD_TRANSACTION_MASS { + Ok(false) + } else { + tx.set_mass(mass); + Ok(true) + } } /// `calculateTransactionFee()` returns minimum fees needed for the transaction to be /// accepted by the network. If the transaction is invalid or the mass can not be calculated, -/// the function throws an error. +/// the function throws an error. If the mass exceeds the maximum standard transaction mass, +/// the function returns `undefined`. /// /// @category Wallet SDK +/// @see {@link maximumStandardTransactionMass} +/// @see {@link calculateTransactionMass} +/// @see {@link updateTransactionMass} /// #[wasm_bindgen(js_name = calculateTransactionFee)] -pub fn calculate_unsigned_transaction_fee(network_id: NetworkIdT, tx: &TransactionT, minimum_signatures: Option) -> Result { +pub fn calculate_unsigned_transaction_fee( + network_id: NetworkIdT, + tx: &TransactionT, + minimum_signatures: Option, +) -> 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 mass = mc.calc_overall_mass_for_unsigned_client_transaction(tx.as_ref(), minimum_signatures.unwrap_or(1))?; - let fee = mc.calc_fee_for_mass(mass); - Ok(fee) + if mass > MAXIMUM_STANDARD_TRANSACTION_MASS { + Ok(None) + } else { + Ok(Some(mc.calc_fee_for_mass(mass))) + } } diff --git a/wallet/keys/src/publickey.rs b/wallet/keys/src/publickey.rs index 6065530..15269f4 100644 --- a/wallet/keys/src/publickey.rs +++ b/wallet/keys/src/publickey.rs @@ -17,10 +17,12 @@ //! ``` //! -use spectre_consensus_core::network::NetworkType; - use crate::imports::*; +use spectre_consensus_core::network::NetworkType; +use ripemd::{Digest, Ripemd160}; +use sha2::Sha256; + /// Data structure that envelopes a PublicKey. /// Only supports Schnorr-based addresses. /// @category Wallet SDK @@ -69,6 +71,17 @@ impl PublicKey { pub fn to_x_only_public_key(&self) -> XOnlyPublicKey { self.xonly_public_key.into() } + + /// Compute a 4-byte key fingerprint for this public key as a hex string. + /// Default implementation uses `RIPEMD160(SHA256(public_key))`. + pub fn fingerprint(&self) -> Option { + if let Some(public_key) = self.public_key.as_ref() { + let digest = Ripemd160::digest(Sha256::digest(public_key.serialize().as_slice())); + Some(digest[..4].as_ref().to_hex().into()) + } else { + None + } + } } impl PublicKey {