Skip to content

Commit

Permalink
[WIP] Ref script size fee calculation + Ref input support (tx builder) (
Browse files Browse the repository at this point in the history
#349)

* Ref script size fee calculation

Fee calculation has changed to take into account ref script sizes via
the new protocol parameter `min_fee_ref_script_cost_per_byte`.

* handle ref inputs in tx builder properly

* cleaning up

* add plutus v3 in some areas
  • Loading branch information
rooooooooob authored Aug 26, 2024
1 parent 3b6899c commit 54cec5b
Show file tree
Hide file tree
Showing 8 changed files with 289 additions and 50 deletions.
125 changes: 103 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,43 @@ fn min_fee(tx_builder: &TransactionBuilder) -> Result<Coin, TxBuilderError> {
fn min_fee_with_exunits(tx_builder: &TransactionBuilder) -> Result<Coin, TxBuilderError> {
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<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()
};

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)
}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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(())
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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());
Expand All @@ -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);
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
39 changes: 37 additions & 2 deletions chain/rust/src/builders/witness_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,21 @@ 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,
};
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 +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()
Expand Down Expand Up @@ -320,6 +334,22 @@ impl TransactionWitnessSetBuilder {
)
}

pub fn get_plutus_v3_script(&self) -> Vec<PlutusV3Script> {
self.scripts
.iter()
.filter(|entry| !self.required_wits.script_refs.contains(entry.0))
.fold(
Vec::<PlutusV3Script>::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);
Expand Down Expand Up @@ -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() {
Expand All @@ -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());
}
Expand Down
Loading

0 comments on commit 54cec5b

Please sign in to comment.