Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Ref script size fee calculation + Ref input support (tx builder) #349

Merged
merged 5 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 132 additions & 22 deletions chain/rust/src/builders/tx_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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;
Expand Down Expand Up @@ -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)")]
Expand Down Expand Up @@ -227,10 +227,71 @@ fn min_fee(tx_builder: &TransactionBuilder) -> Result<Coin, TxBuilderError> {
fn min_fee_with_exunits(tx_builder: &TransactionBuilder) -> Result<Coin, TxBuilderError> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

min_fee might possibly need updating I think, or even rethinking these two because we have more components to the fee calculations now - but maybe not since can you even possibly incur this ref script fee if you don't have exunits involved? but it could be involved just give temporarily incorrect values (e.g. for seeing how much doing something changes fee mid-build before plutus contract is added)

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: std::collections::BTreeMap<ScriptHash, u64> = 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()
// } else {
// tx_builder.inputs.iter().filter_map(ref_script_orig_size_builder).collect()
// };

// let mut total_ref_script_size = 0;
// for output in tx_builder.outputs.iter() {
// if let Some(Credential::Script{ hash, .. }) = output.address().payment_cred() {
// if let Some(orig_size) = ref_script_orig_sizes.get(hash) {
// total_ref_script_size += *orig_size;
// println!("USING REF SCRIPT {} | SIZE {}", hash.to_hex(), orig_size);
// }
// }
// }

let ref_script_orig_sizes: HashMap<ScriptHash, u64> =
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() {
println!(" orig {} - {}", hash.to_hex(), size);
}

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() {
println!(" ------ searching: {}", hash.to_hex());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably we want to remove all these print statements?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Sorry, I forgot to set it back to draft. I just wanted to check mergability.

if let Some(orig_size) = ref_script_orig_sizes.get(hash) {
total_ref_script_size += *orig_size;
println!(
"\n* USING REF SCRIPT {} | SIZE {}\n",
hash.to_hex(),
orig_size
);
}
}
}

println!("total_ref_script_size = {total_ref_script_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)
}
Expand Down Expand Up @@ -402,6 +463,7 @@ pub struct TransactionBuilder {
utxos: Vec<InputBuilderResult>,
collateral_return: Option<TransactionOutput>,
reference_inputs: Option<Vec<TransactionUnspentOutput>>,
//reference_scripts_used: Vec<Script>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this?

}

impl TransactionBuilder {
Expand Down Expand Up @@ -455,7 +517,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()
Expand Down Expand Up @@ -524,7 +589,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()
Expand Down Expand Up @@ -579,7 +647,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(())
Expand Down Expand Up @@ -631,7 +702,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);
Expand Down Expand Up @@ -693,7 +767,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
Expand Down Expand Up @@ -726,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?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO? Okay to merge with this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this, yes. I don't think it's too bad of a performance hit for now (did not profile it - just going off past experience with hashing in the builder and we don't support asm.js in the new CML anyway so it's not as ridiculous a difference) and it's only when using ref scripts and we can always optimize later, using this comment as a reminder.

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
Expand Down Expand Up @@ -776,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 => {
Expand Down Expand Up @@ -859,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());
Expand All @@ -884,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);
Expand Down Expand Up @@ -912,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());
Expand Down Expand Up @@ -941,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);
Expand Down Expand Up @@ -989,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
Expand Down Expand Up @@ -1053,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,
Expand Down Expand Up @@ -1644,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() {
Expand Down Expand Up @@ -2027,7 +2137,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 {
Expand Down Expand Up @@ -3867,7 +3977,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)
Expand Down Expand Up @@ -3911,7 +4021,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)
Expand Down Expand Up @@ -4180,7 +4290,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()
Expand Down
13 changes: 12 additions & 1 deletion chain/rust/src/builders/witness_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down
Loading
Loading