diff --git a/chain/rust/src/builders/tx_builder.rs b/chain/rust/src/builders/tx_builder.rs index b6223a32..8c2cb01c 100644 --- a/chain/rust/src/builders/tx_builder.rs +++ b/chain/rust/src/builders/tx_builder.rs @@ -18,7 +18,7 @@ use crate::assets::MultiAsset; use crate::assets::{AssetArithmeticError, Mint}; use crate::auxdata::AuxiliaryData; use crate::builders::output_builder::TransactionOutputBuilder; -use crate::certs::Certificate; +use crate::certs::{Certificate, Credential}; use crate::crypto::hash::{calc_script_data_hash, hash_auxiliary_data, ScriptDataHashError}; use crate::crypto::{BootstrapWitness, Vkeywitness}; use crate::deposit::{internal_get_deposit, internal_get_implicit_input}; @@ -37,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; @@ -188,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)")] @@ -227,10 +227,43 @@ 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, + ) + }) + } + + 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() + }; + + let mut total_ref_script_size = 0; + for utxo in tx_builder.inputs.iter() { + if let Some(Credential::Script { hash, .. }) = utxo.output.address().payment_cred() { + if let Some(orig_size) = ref_script_orig_sizes.get(hash) { + total_ref_script_size += *orig_size; + } + } + } + crate::fees::min_fee( &full_tx, &tx_builder.config.fee_algo, &tx_builder.config.ex_unit_prices, + total_ref_script_size, ) .map_err(Into::into) } @@ -455,7 +488,10 @@ impl TransactionBuilder { // a specific output, so the improvement algorithm we do above does not apply here. while input_total.coin < output_total.coin { if available_indices.is_empty() { - return Err(TxBuilderError::UTxOBalanceInsufficient); + return Err(TxBuilderError::UTxOBalanceInsufficient( + input_total.clone(), + output_total.clone(), + )); } let i = *available_indices .iter() @@ -524,7 +560,10 @@ impl TransactionBuilder { // a specific output, so the improvement algorithm we do above does not apply here. while input_total.coin < output_total.coin { if available_indices.is_empty() { - return Err(TxBuilderError::UTxOBalanceInsufficient); + return Err(TxBuilderError::UTxOBalanceInsufficient( + input_total.clone(), + output_total.clone(), + )); } let i = *available_indices .iter() @@ -579,7 +618,10 @@ impl TransactionBuilder { if by(input_total).unwrap_or_else(u64::zero) < by(output_total).expect("do not call on asset types that aren't in the output") { - return Err(TxBuilderError::UTxOBalanceInsufficient); + return Err(TxBuilderError::UTxOBalanceInsufficient( + input_total.clone(), + output_total.clone(), + )); } Ok(()) @@ -631,7 +673,10 @@ impl TransactionBuilder { let needed = by(output.amount()).unwrap(); while added < needed { if relevant_indices.is_empty() { - return Err(TxBuilderError::UTxOBalanceInsufficient); + return Err(TxBuilderError::UTxOBalanceInsufficient( + input_total.clone(), + output_total.clone(), + )); } let random_index = rng.gen_range(0..relevant_indices.len()); let i = relevant_indices.swap_remove(random_index); @@ -693,7 +738,10 @@ impl TransactionBuilder { Ok(()) } - pub fn add_input(&mut self, result: InputBuilderResult) -> 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 @@ -726,16 +774,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 @@ -776,6 +835,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 => { @@ -859,7 +919,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()); @@ -884,6 +947,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); @@ -912,7 +978,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()); @@ -941,7 +1010,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); @@ -989,7 +1061,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 @@ -1053,10 +1128,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, @@ -1644,7 +1722,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() { @@ -2027,7 +2108,7 @@ mod tests { } fn create_linear_fee(coefficient: u64, constant: u64) -> LinearFee { - LinearFee::new(coefficient, constant) + LinearFee::new(coefficient, constant, 0) } fn create_default_linear_fee() -> LinearFee { @@ -3867,7 +3948,7 @@ mod tests { #[flaky_test::flaky_test] fn tx_builder_cip2_random_improve_when_using_all_available_inputs() { // we have a = 1 to test increasing fees when more inputs are added - let linear_fee = LinearFee::new(1, 0); + let linear_fee = LinearFee::new(1, 0, 0); let cfg = TransactionBuilderConfigBuilder::default() .fee_algo(linear_fee) .pool_deposit(0) @@ -3911,7 +3992,7 @@ mod tests { #[flaky_test::flaky_test] fn tx_builder_cip2_random_improve_adds_enough_for_fees() { // we have a = 1 to test increasing fees when more inputs are added - let linear_fee = LinearFee::new(1, 0); + let linear_fee = LinearFee::new(1, 0, 0); let cfg = TransactionBuilderConfigBuilder::default() .fee_algo(linear_fee) .pool_deposit(0) @@ -4180,7 +4261,7 @@ mod tests { #[test] fn add_change_splits_change_into_multiple_outputs_when_nfts_overflow_output_size() { - let linear_fee = LinearFee::new(0, 1); + let linear_fee = LinearFee::new(0, 1, 0); let max_value_size = 100; // super low max output size to test with fewer assets let mut tx_builder = TransactionBuilder::new( TransactionBuilderConfigBuilder::default() diff --git a/chain/rust/src/builders/witness_builder.rs b/chain/rust/src/builders/witness_builder.rs index 8553883c..269a3419 100644 --- a/chain/rust/src/builders/witness_builder.rs +++ b/chain/rust/src/builders/witness_builder.rs @@ -8,7 +8,10 @@ use crate::{ byron::ByronAddress, certs::Credential, crypto::{hash::hash_plutus_data, BootstrapWitness, Vkey, Vkeywitness}, - plutus::{LegacyRedeemer, PlutusData, PlutusScript, PlutusV1Script, PlutusV2Script, Redeemers}, + plutus::{ + LegacyRedeemer, PlutusData, PlutusScript, PlutusV1Script, PlutusV2Script, PlutusV3Script, + Redeemers, + }, transaction::TransactionWitnessSet, NativeScript, RequiredSigners, Script, }; @@ -16,7 +19,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 +181,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() @@ -320,6 +334,22 @@ impl TransactionWitnessSetBuilder { ) } + pub fn get_plutus_v3_script(&self) -> Vec { + self.scripts + .iter() + .filter(|entry| !self.required_wits.script_refs.contains(entry.0)) + .fold( + Vec::::new(), + |mut acc, script| match &script.1 { + &Script::PlutusV3 { script, .. } => { + acc.push(script.clone()); + acc + } + _ => acc, + }, + ) + } + pub fn add_plutus_datum(&mut self, plutus_datum: PlutusData) { self.plutus_data .insert(hash_plutus_data(&plutus_datum), plutus_datum); @@ -419,6 +449,7 @@ impl TransactionWitnessSetBuilder { let native_scripts = self.get_native_script(); let plutus_v1_scripts = self.get_plutus_v1_script(); let plutus_v2_scripts = self.get_plutus_v2_script(); + let plutus_v3_scripts = self.get_plutus_v3_script(); let plutus_datums = self.get_plutus_datum(); if !self.vkeys.is_empty() { @@ -442,6 +473,10 @@ impl TransactionWitnessSetBuilder { result.plutus_v2_scripts = Some(plutus_v2_scripts.into()); } + if !plutus_v3_scripts.is_empty() { + result.plutus_v3_scripts = Some(plutus_v3_scripts.into()); + } + if !self.plutus_data.is_empty() { result.plutus_datums = Some(plutus_datums.into()); } diff --git a/chain/rust/src/fees.rs b/chain/rust/src/fees.rs index 800b11f0..75e10861 100644 --- a/chain/rust/src/fees.rs +++ b/chain/rust/src/fees.rs @@ -3,27 +3,38 @@ use crate::plutus::ExUnitPrices; use crate::transaction::Transaction; use crate::Coin; use cml_core::{serialization::Serialize, ArithmeticError}; -use fraction::{Fraction, ToPrimitive}; +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 #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] pub struct LinearFee { - pub constant: Coin, + /// minfee_a pub coefficient: Coin, + /// minfee_b + pub constant: Coin, + /// min_fee_ref_script_cost_per_byte + pub ref_script_cost_per_byte: Coin, } impl LinearFee { - pub fn new(coefficient: Coin, constant: Coin) -> Self { + /** + * * `coefficient` - minfee_a from protocol params + * * `constant` - minfee_b from protocol params + * * `ref_script_cost_per_bytes` - min_fee_ref_script_cost_per_byte from protocol params. New in Conway + */ + pub fn new(coefficient: Coin, constant: Coin, ref_script_cost_per_byte: Coin) -> Self { Self { constant, coefficient, + ref_script_cost_per_byte, } } } /** - * Min fee for JUST the script + * Min fee for JUST the script, NOT including ref inputs */ pub fn min_script_fee( tx: &Transaction, @@ -31,20 +42,60 @@ 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) + } +} + +/** + * 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( + 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 + + if total_ref_script_size > 0 { + let multiplier = BigRational::new(12u64.into(), 10u64.into()); + let size_increment = 25_600u64; // 25KiB + 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; + + loop { + 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; + } + ref_scripts_size_left -= size_increment; + fee_tier = fee_tier + .checked_mul(&multiplier) + .ok_or(ArithmeticError::IntegerOverflow)?; + } + u64::try_from(fee.ceil().to_integer()).map_err(|_e| ArithmeticError::IntegerOverflow) } else { Ok(0) } @@ -64,9 +115,14 @@ pub fn min_fee( tx: &Transaction, linear_fee: &LinearFee, ex_unit_prices: &ExUnitPrices, + total_ref_script_size: u64, ) -> Result { // TODO: the fee should be 0 if all inputs are genesis redeem addresses - min_no_script_fee(tx, linear_fee)? - .checked_add(min_script_fee(tx, ex_unit_prices)?) + 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(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/genesis/byron/parse.rs b/chain/rust/src/genesis/byron/parse.rs index ddbedc88..00f22047 100644 --- a/chain/rust/src/genesis/byron/parse.rs +++ b/chain/rust/src/genesis/byron/parse.rs @@ -128,6 +128,7 @@ pub fn parse_genesis_data(json: R) -> Result Result<&[u8], ScriptConversionError> { + match self { + 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()), + } + } + // Returns which language the script is if it's a Plutus script // Returns None otherwise (i.e. NativeScript) pub fn language(&self) -> Option { @@ -128,6 +140,25 @@ impl From for Script { } } +#[derive(Debug, thiserror::Error)] +pub enum ScriptConversionError { + #[error("Cannot convert NativeScript to PlutusScript")] + NativeScriptNotPlutus, +} + +impl TryFrom