From 0a455ebf3511c8c35eb0f14b1ca55e348f2da26a Mon Sep 17 00:00:00 2001 From: rooooooooob Date: Fri, 16 Aug 2024 21:51:12 -0700 Subject: [PATCH] handle ref inputs in tx builder properly --- chain/rust/src/builders/tx_builder.rs | 124 ++++++++++++++++----- chain/rust/src/builders/witness_builder.rs | 13 ++- chain/rust/src/fees.rs | 77 +++++-------- chain/rust/src/utils.rs | 9 +- chain/wasm/src/builders/tx_builder.rs | 1 + chain/wasm/src/fees.rs | 28 ++++- 6 files changed, 166 insertions(+), 86 deletions(-) diff --git a/chain/rust/src/builders/tx_builder.rs b/chain/rust/src/builders/tx_builder.rs index 096a16de..28c5ab1d 100644 --- a/chain/rust/src/builders/tx_builder.rs +++ b/chain/rust/src/builders/tx_builder.rs @@ -18,7 +18,6 @@ use crate::assets::MultiAsset; use crate::assets::{AssetArithmeticError, Mint}; use crate::auxdata::AuxiliaryData; use crate::builders::output_builder::TransactionOutputBuilder; -use crate::builders::tx_builder; use crate::certs::{Certificate, Credential}; use crate::crypto::hash::{calc_script_data_hash, hash_auxiliary_data, ScriptDataHashError}; use crate::crypto::{BootstrapWitness, Vkeywitness}; @@ -38,7 +37,7 @@ use cml_core::ordered_hash_map::OrderedHashMap; use cml_core::serialization::{CBORReadLen, Deserialize}; use cml_core::{ArithmeticError, DeserializeError, DeserializeFailure, Slot}; use cml_crypto::{Ed25519KeyHash, ScriptDataHash, ScriptHash, Serialize}; -use fraction::Zero; +use num::Zero; use rand::Rng; use std::collections::{BTreeSet, HashMap}; use std::convert::TryInto; @@ -189,8 +188,8 @@ pub enum TxBuilderError { "Multiasset values not supported by RandomImprove. Please use RandomImproveMultiAsset" )] RandomImproveCantContainMultiasset, - #[error("UTxO Balance Insufficient")] - UTxOBalanceInsufficient, + #[error("UTxO Balance Insufficient. Inputs: {0:?}, Outputs: {1:?}")] + UTxOBalanceInsufficient(Value, Value), #[error("NFTs too large for change output")] NFTTooLargeForChange, #[error("Collateral can only be payment keys (scripts not allowed)")] @@ -228,9 +227,17 @@ fn min_fee(tx_builder: &TransactionBuilder) -> Result { fn min_fee_with_exunits(tx_builder: &TransactionBuilder) -> Result { let full_tx = fake_full_tx(tx_builder, tx_builder.build_body()?)?; // we can't know the of scripts yet as they can't be calculated until we build the tx - + fn ref_script_orig_size_builder(utxo: &TransactionUnspentOutput) -> Option<(ScriptHash, u64)> { - utxo.output.script_ref().map(|script_ref| (script_ref.hash(), script_ref.raw_plutus_bytes().expect("TODO: handle this").len() as u64)) + utxo.output.script_ref().map(|script_ref| { + ( + script_ref.hash(), + script_ref + .raw_plutus_bytes() + .expect("TODO: handle this") + .len() as u64, + ) + }) } // let ref_script_orig_sizes: std::collections::BTreeMap = if let Some(ref_inputs) = &tx_builder.reference_inputs { // ref_inputs.iter().chain(tx_builder.inputs.iter()).filter_map(ref_script_orig_size_builder).collect() @@ -248,11 +255,15 @@ fn min_fee_with_exunits(tx_builder: &TransactionBuilder) -> Result = if let Some(ref_inputs) = &tx_builder.reference_inputs { - ref_inputs.iter().filter_map(ref_script_orig_size_builder).collect() - } else { - HashMap::default() - }; + let ref_script_orig_sizes: HashMap = + if let Some(ref_inputs) = &tx_builder.reference_inputs { + ref_inputs + .iter() + .filter_map(ref_script_orig_size_builder) + .collect() + } else { + HashMap::default() + }; println!("\n\n\n ORIG SIZES:"); for (hash, size) in ref_script_orig_sizes.iter() { @@ -261,21 +272,26 @@ fn min_fee_with_exunits(tx_builder: &TransactionBuilder) -> Result Result<(), TxBuilderError> { + pub fn add_input(&mut self, mut result: InputBuilderResult) -> Result<(), TxBuilderError> { + if let Some(reference_inputs) = &self.reference_inputs { + result.required_wits.remove_ref_scripts(reference_inputs); + } if let Some(script_ref) = result.utxo_info.script_ref() { self.witness_builders .witness_set_builder @@ -772,16 +803,27 @@ impl TransactionBuilder { .for_each(|signer| self.add_required_signer(*signer)); match &script_witness.script { - PlutusScriptWitness::Ref(ref_script) => { + PlutusScriptWitness::Ref(ref_script_hash) => { + // it could also be a reference script - check those too + // TODO: do we want to change how we store ref scripts to cache the hash to avoid re-hashing (slow on wasm) every time an input is added? if !self .witness_builders .witness_set_builder .required_wits .script_refs - .contains(ref_script) + .contains(ref_script_hash) + && !self.reference_inputs.iter().any(|ref_inputs| { + ref_inputs.iter().any(|ref_input| { + ref_input + .output + .script_ref() + .iter() + .any(|ref_script| ref_script.hash() == *ref_script_hash) + }) + }) { Err(TxBuilderError::RefScriptNotFound( - *ref_script, + *ref_script_hash, self.witness_builders .witness_set_builder .required_wits @@ -822,6 +864,7 @@ impl TransactionBuilder { .ok_or_else(|| ArithmeticError::IntegerOverflow.into()) } + /// Add a reference input. Must be called BEFORE adding anything (inputs, certs, etc) that refer to this reference input. pub fn add_reference_input(&mut self, utxo: TransactionUnspentOutput) { let reference_inputs = match self.reference_inputs.as_mut() { None => { @@ -905,7 +948,10 @@ impl TransactionBuilder { self.validity_start_interval = Some(validity_start_interval) } - pub fn add_cert(&mut self, result: CertificateBuilderResult) { + pub fn add_cert(&mut self, mut result: CertificateBuilderResult) { + if let Some(reference_inputs) = &self.reference_inputs { + result.required_wits.remove_ref_scripts(reference_inputs); + } self.witness_builders.redeemer_set_builder.add_cert(&result); if self.certs.is_none() { self.certs = Some(Vec::new()); @@ -930,6 +976,9 @@ impl TransactionBuilder { } pub fn add_proposal(&mut self, mut result: ProposalBuilderResult) { + if let Some(reference_inputs) = &self.reference_inputs { + result.required_wits.remove_ref_scripts(reference_inputs); + } self.witness_builders .redeemer_set_builder .add_proposal(&result); @@ -958,7 +1007,10 @@ impl TransactionBuilder { .add_required_wits(result.required_wits); } - pub fn add_vote(&mut self, result: VoteBuilderResult) { + pub fn add_vote(&mut self, mut result: VoteBuilderResult) { + if let Some(reference_inputs) = &self.reference_inputs { + result.required_wits.remove_ref_scripts(reference_inputs); + } self.witness_builders.redeemer_set_builder.add_vote(&result); if let Some(votes) = self.votes.as_mut() { votes.extend(result.votes.take()); @@ -987,7 +1039,10 @@ impl TransactionBuilder { self.withdrawals.clone() } - pub fn add_withdrawal(&mut self, result: WithdrawalBuilderResult) { + pub fn add_withdrawal(&mut self, mut result: WithdrawalBuilderResult) { + if let Some(reference_inputs) = &self.reference_inputs { + result.required_wits.remove_ref_scripts(reference_inputs); + } self.witness_builders .redeemer_set_builder .add_reward(&result); @@ -1035,7 +1090,10 @@ impl TransactionBuilder { } } - pub fn add_mint(&mut self, result: MintBuilderResult) -> Result<(), TxBuilderError> { + pub fn add_mint(&mut self, mut result: MintBuilderResult) -> Result<(), TxBuilderError> { + if let Some(reference_inputs) = &self.reference_inputs { + result.required_wits.remove_ref_scripts(reference_inputs); + } self.witness_builders.redeemer_set_builder.add_mint(&result); self.witness_builders .witness_set_builder @@ -1099,10 +1157,13 @@ impl TransactionBuilder { } } - pub fn add_collateral(&mut self, result: InputBuilderResult) -> Result<(), TxBuilderError> { + pub fn add_collateral(&mut self, mut result: InputBuilderResult) -> Result<(), TxBuilderError> { if result.aggregate_witness.is_some() { return Err(TxBuilderError::CollateralMustBePayment); } + if let Some(reference_inputs) = &self.reference_inputs { + result.required_wits.remove_ref_scripts(reference_inputs); + } let new_input = TransactionUnspentOutput { input: result.input, output: result.utxo_info, @@ -1690,7 +1751,10 @@ pub fn add_change_if_needed( builder.set_fee(input_total.checked_sub(&output_total)?.coin); Ok(false) } - Some(Ordering::Less) => Err(TxBuilderError::UTxOBalanceInsufficient), + Some(Ordering::Less) => Err(TxBuilderError::UTxOBalanceInsufficient( + input_total.clone(), + output_total.clone(), + )), Some(Ordering::Greater) => { let change_estimator = input_total.checked_sub(&output_total)?; if change_estimator.has_multiassets() { diff --git a/chain/rust/src/builders/witness_builder.rs b/chain/rust/src/builders/witness_builder.rs index 299f9955..6064f269 100644 --- a/chain/rust/src/builders/witness_builder.rs +++ b/chain/rust/src/builders/witness_builder.rs @@ -16,7 +16,10 @@ use cml_crypto::{ DatumHash, Ed25519KeyHash, Ed25519Signature, PublicKey, RawBytesEncoding, ScriptHash, }; -use super::redeemer_builder::{MissingExunitError, RedeemerBuilderError, RedeemerWitnessKey}; +use super::{ + redeemer_builder::{MissingExunitError, RedeemerBuilderError, RedeemerWitnessKey}, + tx_builder::TransactionUnspentOutput, +}; #[derive(Debug, thiserror::Error)] pub enum WitnessBuilderError { @@ -175,6 +178,14 @@ impl RequiredWitnessSet { self.redeemers.extend(requirements.redeemers); } + pub fn remove_ref_scripts(&mut self, ref_inputs: &[TransactionUnspentOutput]) { + ref_inputs.iter().for_each(|utxo| { + utxo.output.script_ref().inspect(|script_ref| { + self.scripts.remove(&script_ref.hash()); + }); + }) + } + pub(crate) fn len(&self) -> usize { self.vkeys.len() + self.bootstraps.len() diff --git a/chain/rust/src/fees.rs b/chain/rust/src/fees.rs index 571c1dab..75e10861 100644 --- a/chain/rust/src/fees.rs +++ b/chain/rust/src/fees.rs @@ -1,11 +1,10 @@ -use crate::{builders::tx_builder::TransactionUnspentOutput, plutus::utils::compute_total_ex_units}; +use crate::plutus::utils::compute_total_ex_units; use crate::plutus::ExUnitPrices; use crate::transaction::Transaction; use crate::Coin; use cml_core::{serialization::Serialize, ArithmeticError}; -use cml_crypto::ScriptHash; -use fraction::{Fraction, ToPrimitive}; -use std::collections::BTreeMap; +use num::{rational::BigRational, CheckedAdd, CheckedMul}; +use std::convert::TryFrom; /// Careful: although the linear fee is the same for Byron & Shelley /// The value of the parameters and how fees are computed is not the same @@ -43,71 +42,51 @@ pub fn min_script_fee( ) -> Result { if let Some(redeemers) = &tx.witness_set.redeemers { let total_ex_units = compute_total_ex_units(&redeemers.clone().to_flat_format())?; - let script_fee = ((Fraction::new(total_ex_units.mem, 1u64) - * Fraction::new( - ex_unit_prices.mem_price.numerator, - ex_unit_prices.mem_price.denominator, + let script_fee = ((BigRational::new(total_ex_units.mem.into(), 1u64.into()) + * BigRational::new( + ex_unit_prices.mem_price.numerator.into(), + ex_unit_prices.mem_price.denominator.into(), )) - + (Fraction::new(total_ex_units.steps, 1u64) - * Fraction::new( - ex_unit_prices.step_price.numerator, - ex_unit_prices.step_price.denominator, + + (BigRational::new(total_ex_units.steps.into(), 1u64.into()) + * BigRational::new( + ex_unit_prices.step_price.numerator.into(), + ex_unit_prices.step_price.denominator.into(), ))) .ceil() - .to_u64() - .unwrap(); - Ok(script_fee) + .to_integer(); + u64::try_from(script_fee).map_err(|_| ArithmeticError::IntegerOverflow) } else { Ok(0) } } - - -// pub fn total_ref_scripts_size(ref_inputs: &[TransactionUnspentOutput]) -> Result { -// ref_inputs -// .iter() -// .try_fold(0, |acc, utxo| -// utxo -// .output -// .script_ref() -// .map(|script| script.to_cbor_bytes().len() as u64) -// .unwrap_or(0u64) -// .checked_add(acc) -// .ok_or(ArithmeticError::IntegerOverflow)) -// } - /** * Calculates the cost of all ref scripts * * `total_ref_script_size` - Total size (original, not hashes) of all ref scripts. Duplicate scripts are counted as many times as they occur */ pub fn min_ref_script_fee( - tx: &Transaction, linear_fee: &LinearFee, total_ref_script_size: u64, ) -> Result { // based on: // https://github.com/IntersectMBO/cardano-ledger/blob/7e65f0365eef647b9415e3fe9b3c35561761a3d5/eras/conway/impl/src/Cardano/Ledger/Conway/Tx.hs#L84 // https://github.com/IntersectMBO/cardano-ledger/blob/a34f878c56763d138d2203d8ba84b3af64d94fce/eras/conway/impl/src/Cardano/Ledger/Conway/UTxO.hs#L152 - use num::{rational::Ratio, CheckedAdd, CheckedMul}; - if let Some(ref_inputs) = &tx.body.reference_inputs { - let multiplier = Ratio::new_raw(12u64, 10u64); + if total_ref_script_size > 0 { + let multiplier = BigRational::new(12u64.into(), 10u64.into()); let size_increment = 25_600u64; // 25KiB - let mut fee: Ratio = Ratio::from(0u64); - let mut fee_tier: Ratio = linear_fee.ref_script_cost_per_byte.into(); + let mut fee: BigRational = BigRational::from_integer(0.into()); + let mut fee_tier: BigRational = + BigRational::from_integer(linear_fee.ref_script_cost_per_byte.into()); let mut ref_scripts_size_left = total_ref_script_size; - // .iter() - // .try_fold(0u64, |acc, input| { - // let original_script_size = ref_script_sizes.get(input.) - // acc.checked_add(input.to_cbor_bytes().len() as u64).ok_or(ArithmeticError::IntegerOverflow) - // })?; loop { - fee = Ratio::from_integer(std::cmp::min(size_increment, ref_scripts_size_left)) - .checked_mul(&fee_tier) - .and_then(|x| x.checked_add(&fee)) - .ok_or(ArithmeticError::IntegerOverflow)?; + fee = BigRational::from_integer( + std::cmp::min(size_increment, ref_scripts_size_left).into(), + ) + .checked_mul(&fee_tier) + .and_then(|x| x.checked_add(&fee)) + .ok_or(ArithmeticError::IntegerOverflow)?; if ref_scripts_size_left <= size_increment { break; } @@ -116,8 +95,7 @@ pub fn min_ref_script_fee( .checked_mul(&multiplier) .ok_or(ArithmeticError::IntegerOverflow)?; } - - Ok(fee.to_integer()) + u64::try_from(fee.ceil().to_integer()).map_err(|_e| ArithmeticError::IntegerOverflow) } else { Ok(0) } @@ -142,8 +120,9 @@ pub fn min_fee( // TODO: the fee should be 0 if all inputs are genesis redeem addresses let base_fee = min_no_script_fee(tx, linear_fee)?; let script_fee = min_script_fee(tx, ex_unit_prices)?; - let ref_scripts_fee = min_ref_script_fee(tx, linear_fee, total_ref_script_size)?; - base_fee.checked_add(script_fee) + let ref_scripts_fee = min_ref_script_fee(linear_fee, total_ref_script_size)?; + base_fee + .checked_add(script_fee) .and_then(|x| x.checked_add(ref_scripts_fee)) .ok_or(ArithmeticError::IntegerOverflow) } diff --git a/chain/rust/src/utils.rs b/chain/rust/src/utils.rs index e55199c2..07e9d9c1 100644 --- a/chain/rust/src/utils.rs +++ b/chain/rust/src/utils.rs @@ -6,8 +6,11 @@ use cml_core::{ }; use cml_crypto::{Ed25519KeyHash, RawBytesEncoding, ScriptHash}; use derivative::Derivative; -use std::{convert::TryFrom, io::{BufRead, Seek, Write}}; use std::iter::IntoIterator; +use std::{ + convert::TryFrom, + io::{BufRead, Seek, Write}, +}; use crate::{ crypto::hash::{hash_script, ScriptHashNamespace}, @@ -27,7 +30,7 @@ impl Script { pub fn raw_plutus_bytes(&self) -> Result<&[u8], ScriptConversionError> { match self { - Self::Native{ .. } => Err(ScriptConversionError::NativeScriptNotPlutus), + Self::Native { .. } => Err(ScriptConversionError::NativeScriptNotPlutus), Self::PlutusV1 { script, .. } => Ok(script.to_raw_bytes()), Self::PlutusV2 { script, .. } => Ok(script.to_raw_bytes()), Self::PlutusV3 { script, .. } => Ok(script.to_raw_bytes()), @@ -142,7 +145,7 @@ impl TryFrom