-
Notifications
You must be signed in to change notification settings - Fork 36
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
Changes from 2 commits
06007ee
0a455eb
07e640f
02dddbf
f225545
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,71 @@ 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: 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()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably we want to remove all these print statements? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} | ||
|
@@ -402,6 +463,7 @@ pub struct TransactionBuilder { | |
utxos: Vec<InputBuilderResult>, | ||
collateral_return: Option<TransactionOutput>, | ||
reference_inputs: Option<Vec<TransactionUnspentOutput>>, | ||
//reference_scripts_used: Vec<Script>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we need this? |
||
} | ||
|
||
impl TransactionBuilder { | ||
|
@@ -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() | ||
|
@@ -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() | ||
|
@@ -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(()) | ||
|
@@ -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); | ||
|
@@ -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 | ||
|
@@ -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? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO? Okay to merge with this? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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 => { | ||
|
@@ -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()); | ||
|
@@ -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); | ||
|
@@ -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()); | ||
|
@@ -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); | ||
|
@@ -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 | ||
|
@@ -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, | ||
|
@@ -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() { | ||
|
@@ -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 { | ||
|
@@ -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) | ||
|
@@ -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) | ||
|
@@ -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() | ||
|
There was a problem hiding this comment.
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)