diff --git a/package-lock.json b/package-lock.json index 56735897..11d70945 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cardano-serialization-lib", - "version": "9.1.4", + "version": "10.0.0-beta.8", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index e37f0856..4c1dd14a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cardano-serialization-lib", - "version": "9.1.4", + "version": "10.0.0-beta.8", "description": "(De)serialization functions for the Cardano blockchain along with related utility functions", "scripts": { "rust:build-nodejs": "(rimraf ./rust/pkg && cd rust; wasm-pack build --target=nodejs; wasm-pack pack) && npm run js:flowgen", diff --git a/rust/Cargo.lock b/rust/Cargo.lock index ccdbb8d3..ab208d1f 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -50,7 +50,7 @@ checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" [[package]] name = "cardano-serialization-lib" -version = "9.1.4" +version = "10.0.0-beta.8" dependencies = [ "bech32", "cbor_event", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 8c73457b..eaed2c5e 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cardano-serialization-lib" -version = "9.1.4" +version = "10.0.0-beta.8" edition = "2018" authors = ["EMURGO"] license = "MIT" diff --git a/rust/pkg/cardano_serialization_lib.js.flow b/rust/pkg/cardano_serialization_lib.js.flow index 9b9bc0b7..1c3b4825 100644 --- a/rust/pkg/cardano_serialization_lib.js.flow +++ b/rust/pkg/cardano_serialization_lib.js.flow @@ -5197,31 +5197,45 @@ declare export class TransactionBuilder { ): void; /** - * Set explicit Mint object to this builder - * it will replace any previously existing mint + * Set explicit Mint object and the required witnesses to this builder + * it will replace any previously existing mint and mint scripts + * NOTE! Error will be returned in case a mint policy does not have a matching script * @param {Mint} mint + * @param {NativeScripts} mint_scripts */ - set_mint(mint: Mint): void; + set_mint(mint: Mint, mint_scripts: NativeScripts): void; + + /** + * Returns a copy of the current mint state in the builder + * @returns {Mint | void} + */ + get_mint(): Mint | void; + + /** + * Returns a copy of the current mint witness scripts in the builder + * @returns {NativeScripts | void} + */ + get_mint_scripts(): NativeScripts | void; /** * Add a mint entry to this builder using a PolicyID and MintAssets object * It will be securely added to existing or new Mint in this builder * It will replace any existing mint assets with the same PolicyID - * @param {ScriptHash} policy_id + * @param {NativeScript} policy_script * @param {MintAssets} mint_assets */ - set_mint_asset(policy_id: ScriptHash, mint_assets: MintAssets): void; + set_mint_asset(policy_script: NativeScript, mint_assets: MintAssets): void; /** * Add a mint entry to this builder using a PolicyID, AssetName, and Int object for amount * It will be securely added to existing or new Mint in this builder * It will replace any previous existing amount same PolicyID and AssetName - * @param {ScriptHash} policy_id + * @param {NativeScript} policy_script * @param {AssetName} asset_name * @param {Int} amount */ add_mint_asset( - policy_id: ScriptHash, + policy_script: NativeScript, asset_name: AssetName, amount: Int ): void; @@ -5231,14 +5245,14 @@ declare export class TransactionBuilder { * Using a PolicyID, AssetName, Int for amount, Address, and Coin (BigNum) objects * The asset will be securely added to existing or new Mint in this builder * A new output will be added with the specified Address, the Coin value, and the minted asset - * @param {ScriptHash} policy_id + * @param {NativeScript} policy_script * @param {AssetName} asset_name * @param {Int} amount * @param {Address} address * @param {BigNum} output_coin */ add_mint_asset_and_output( - policy_id: ScriptHash, + policy_script: NativeScript, asset_name: AssetName, amount: Int, address: Address, @@ -5251,13 +5265,13 @@ declare export class TransactionBuilder { * The asset will be securely added to existing or new Mint in this builder * A new output will be added with the specified Address and the minted asset * The output will be set to contain the minimum required amount of Coin - * @param {ScriptHash} policy_id + * @param {NativeScript} policy_script * @param {AssetName} asset_name * @param {Int} amount * @param {Address} address */ add_mint_asset_and_output_min_required_coin( - policy_id: ScriptHash, + policy_script: NativeScript, asset_name: AssetName, amount: Int, address: Address @@ -5320,14 +5334,14 @@ declare export class TransactionBuilder { /** * Returns object the body of the new transaction * Auxiliary data itself is not included - * You can use `get_auxiliary_date` or `build_tx` + * You can use `get_auxiliary_data` or `build_tx` * @returns {TransactionBody} */ build(): TransactionBody; /** * Returns full Transaction object with the body and the auxiliary data - * NOTE: witness_set is set to just empty set + * NOTE: witness_set will contain all mint_scripts if any been added or set * NOTE: is_valid set to true * @returns {Transaction} */ diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 819c9154..db5948ba 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -34,6 +34,7 @@ use cbor_event::{ se::{Serialize, Serializer}, }; +pub mod traits; pub mod address; pub mod chain_core; pub mod chain_crypto; @@ -58,6 +59,8 @@ use plutus::*; use metadata::*; use utils::*; use std::cmp::Ordering; +use std::collections::BTreeSet; +use crate::traits::NoneOrEmpty; type DeltaCoin = Int; @@ -221,6 +224,7 @@ impl Certificates { } pub type RequiredSigners = Ed25519KeyHashes; +pub type RequiredSignersSet = BTreeSet; #[wasm_bindgen] #[derive(Clone)] @@ -1744,6 +1748,21 @@ impl NativeScripts { } } +impl From> for NativeScripts { + fn from(scripts: Vec) -> Self { + scripts.iter().fold(NativeScripts::new(), |mut scripts, s| { + scripts.add(s); + scripts + }) + } +} + +impl NoneOrEmpty for NativeScripts { + fn is_none_or_empty(&self) -> bool { + self.0.is_empty() + } +} + #[wasm_bindgen] #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] pub struct Update { @@ -2782,6 +2801,39 @@ impl NetworkId { } } +impl From<&NativeScript> for RequiredSignersSet { + fn from(script: &NativeScript) -> Self { + match &script.0 { + NativeScriptEnum::ScriptPubkey(spk) => { + let mut set = BTreeSet::new(); + set.insert(spk.addr_keyhash()); + set + }, + NativeScriptEnum::ScriptAll(all) => { + RequiredSignersSet::from(&all.native_scripts) + }, + NativeScriptEnum::ScriptAny(any) => { + RequiredSignersSet::from(&any.native_scripts) + }, + NativeScriptEnum::ScriptNOfK(ofk) => { + RequiredSignersSet::from(&ofk.native_scripts) + }, + _ => BTreeSet::new(), + } + } +} + +impl From<&NativeScripts> for RequiredSignersSet { + fn from(scripts: &NativeScripts) -> Self { + scripts.0.iter().fold(BTreeSet::new(), |mut set, s| { + RequiredSignersSet::from(s).iter().for_each(|pk| { + set.insert(pk.clone()); + }); + set + }) + } +} + #[cfg(test)] mod tests { use super::*; @@ -2963,4 +3015,77 @@ mod tests { assert_eq!(p_ass.get(&name1).unwrap(), amount1); assert_eq!(n_ass.get(&name1).unwrap(), amount1); } + + fn keyhash(x: u8) -> Ed25519KeyHash { + Ed25519KeyHash::from_bytes(vec![x, 180, 186, 93, 223, 42, 243, 7, 81, 98, 86, 125, 97, 69, 110, 52, 130, 243, 244, 98, 246, 13, 33, 212, 128, 168, 136, 40]).unwrap() + } + + fn pkscript(pk: &Ed25519KeyHash) -> NativeScript { + NativeScript::new_script_pubkey(&ScriptPubkey::new(pk)) + } + + fn scripts_vec(scripts: Vec<&NativeScript>) -> NativeScripts { + NativeScripts(scripts.iter().map(|s| { (*s).clone() }).collect()) + } + + #[test] + fn native_scripts_get_pubkeys() { + let keyhash1 = keyhash(1); + let keyhash2 = keyhash(2); + let keyhash3 = keyhash(3); + + let pks1 = RequiredSignersSet::from(&pkscript(&keyhash1)); + assert_eq!(pks1.len(), 1); + assert!(pks1.contains(&keyhash1)); + + let pks2 = RequiredSignersSet::from( + &NativeScript::new_timelock_start( + &TimelockStart::new(123), + ), + ); + assert_eq!(pks2.len(), 0); + + let pks3 = RequiredSignersSet::from( + &NativeScript::new_script_all( + &ScriptAll::new(&scripts_vec(vec![ + &pkscript(&keyhash1), + &pkscript(&keyhash2), + ])) + ), + ); + assert_eq!(pks3.len(), 2); + assert!(pks3.contains(&keyhash1)); + assert!(pks3.contains(&keyhash2)); + + let pks4 = RequiredSignersSet::from( + &NativeScript::new_script_any( + &ScriptAny::new(&scripts_vec(vec![ + &NativeScript::new_script_n_of_k(&ScriptNOfK::new( + 1, + &scripts_vec(vec![ + &NativeScript::new_timelock_start(&TimelockStart::new(132)), + &pkscript(&keyhash3), + ]), + )), + &NativeScript::new_script_all(&ScriptAll::new( + &scripts_vec(vec![ + &NativeScript::new_timelock_expiry(&TimelockExpiry::new(132)), + &pkscript(&keyhash1), + ]), + )), + &NativeScript::new_script_any(&ScriptAny::new( + &scripts_vec(vec![ + &pkscript(&keyhash1), + &pkscript(&keyhash2), + &pkscript(&keyhash3), + ]), + )), + ])) + ), + ); + assert_eq!(pks4.len(), 3); + assert!(pks4.contains(&keyhash1)); + assert!(pks4.contains(&keyhash2)); + assert!(pks4.contains(&keyhash3)); + } } diff --git a/rust/src/traits.rs b/rust/src/traits.rs new file mode 100644 index 00000000..8a1b2815 --- /dev/null +++ b/rust/src/traits.rs @@ -0,0 +1,15 @@ +pub trait NoneOrEmpty { + fn is_none_or_empty(&self) -> bool; +} + +impl NoneOrEmpty for &T { + fn is_none_or_empty(&self) -> bool { + (*self).is_none_or_empty() + } +} + +impl NoneOrEmpty for Option { + fn is_none_or_empty(&self) -> bool { + self.is_none() || self.as_ref().unwrap().is_none_or_empty() + } +} diff --git a/rust/src/tx_builder.rs b/rust/src/tx_builder.rs index 37117fd8..aeabf213 100644 --- a/rust/src/tx_builder.rs +++ b/rust/src/tx_builder.rs @@ -1,7 +1,7 @@ use super::*; use super::fees; use super::utils; -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::{BTreeMap, BTreeSet, HashSet}; // comes from witsVKeyNeeded in the Ledger spec fn witness_keys_for_cert(cert_enum: &Certificate, keys: &mut BTreeSet) { @@ -46,12 +46,6 @@ fn fake_private_key() -> Bip32PrivateKey { ).unwrap() } -fn fake_key_hash() -> Ed25519KeyHash { - Ed25519KeyHash::from( - [142, 239, 181, 120, 142, 135, 19, 200, 68, 223, 211, 43, 46, 145, 222, 30, 48, 159, 239, 255, 213, 85, 248, 39, 204, 158, 225, 100] - ) -} - fn fake_raw_key_sig() -> Ed25519Signature { Ed25519Signature::from_bytes( vec![36, 248, 153, 211, 155, 23, 253, 93, 102, 193, 146, 196, 181, 13, 52, 62, 66, 247, 35, 91, 48, 80, 76, 138, 231, 97, 159, 147, 200, 40, 220, 109, 206, 69, 104, 221, 105, 23, 124, 85, 24, 40, 73, 45, 119, 122, 103, 39, 253, 102, 194, 251, 204, 189, 168, 194, 174, 237, 146, 3, 44, 153, 121, 10] @@ -64,26 +58,36 @@ fn fake_raw_key_public() -> PublicKey { ).unwrap() } +fn count_needed_vkeys(tx_builder: &TransactionBuilder) -> usize { + let input_hashes = &tx_builder.input_types.vkeys; + match &tx_builder.mint_scripts { + None => input_hashes.len(), + Some(scripts) => { + // Union all input keys with minting keys + input_hashes.union(&RequiredSignersSet::from(scripts)).count() + } + } +} // tx_body must be the result of building from tx_builder // constructs the rest of the Transaction using fake witness data of the correct length // for use in calculating the size of the final Transaction fn fake_full_tx(tx_builder: &TransactionBuilder, body: TransactionBody) -> Result { let fake_key_root = fake_private_key(); - let fake_key_hash = fake_key_hash(); let raw_key_public = fake_raw_key_public(); let fake_sig = fake_raw_key_sig(); // recall: this includes keys for input, certs and withdrawals - let vkeys = match tx_builder.input_types.vkeys.len() { + let vkeys = match count_needed_vkeys(tx_builder) { 0 => None, x => { + let fake_vkey_witness = Vkeywitness::new( + &Vkey::new(&raw_key_public), + &fake_sig + ); let mut result = Vkeywitnesses::new(); for _i in 0..x { - result.add(&Vkeywitness::new( - &Vkey::new(&raw_key_public), - &fake_sig - )); + result.add(&fake_vkey_witness.clone()); } Some(result) }, @@ -110,21 +114,20 @@ fn fake_full_tx(tx_builder: &TransactionBuilder, body: TransactionBody) -> Resul Some(result) }, }; - let full_script_keys = match &tx_builder.mint { + let full_script_keys = match &tx_builder.mint_scripts { None => script_keys, - Some(mint) => { + Some(witness_scripts) => { let mut ns = script_keys - .map(|sk| { sk.clone() }) + .map(|x| { x.clone() }) .unwrap_or(NativeScripts::new()); - let spk = ScriptPubkey::new(&fake_key_hash); - mint.keys().0.iter().for_each(|_p| { - ns.add(&NativeScript::new_script_pubkey(&spk)); + witness_scripts.0.iter().for_each(|s| { + ns.add(s); }); Some(ns) } }; let witness_set = TransactionWitnessSet { - vkeys: vkeys, + vkeys, native_scripts: full_script_keys, bootstraps: bootstrap_keys, // TODO: plutus support? @@ -140,7 +143,37 @@ fn fake_full_tx(tx_builder: &TransactionBuilder, body: TransactionBody) -> Resul }) } +fn assert_required_mint_scripts(mint: &Mint, maybe_mint_scripts: Option<&NativeScripts>) -> Result<(), JsError> { + if maybe_mint_scripts.is_none_or_empty() { + return Err(JsError::from_str( + "Mint is present in the builder, but witness scripts are not provided!", + )); + } + let mint_scripts = maybe_mint_scripts.unwrap(); + let witness_hashes: HashSet = mint_scripts.0.iter().map(|script| { + script.hash(ScriptHashNamespace::NativeScript) + }).collect(); + for mint_hash in mint.keys().0.iter() { + if !witness_hashes.contains(mint_hash) { + return Err(JsError::from_str( + &format!( + "No witness script is found for mint policy '{:?}'! Script is required!", + hex::encode(mint_hash.to_bytes()), + )) + ); + } + } + Ok(()) +} + fn min_fee(tx_builder: &TransactionBuilder) -> Result { + // Commented out for performance, `min_fee` is a critical function + // This was mostly added here as a paranoid step anyways + // If someone is using `set_mint` and `add_mint*` API function, everything is expected to be intact + // TODO: figure out if assert is needed here and a better way to do it maybe only once if mint doesn't change + // if let Some(mint) = tx_builder.mint.as_ref() { + // assert_required_mint_scripts(mint, tx_builder.mint_scripts.as_ref())?; + // } let full_tx = fake_full_tx(tx_builder, tx_builder.build()?)?; fees::min_fee(&full_tx, &tx_builder.config.fee_algo) } @@ -274,7 +307,7 @@ pub struct TransactionBuilder { validity_start_interval: Option, input_types: MockWitnessSet, mint: Option, - inputs_auto_added: bool, + mint_scripts: Option, } #[wasm_bindgen] @@ -650,31 +683,65 @@ impl TransactionBuilder { Ok(()) } - /// Set explicit Mint object to this builder - /// it will replace any previously existing mint - pub fn set_mint(&mut self, mint: &Mint) { + /// Set explicit Mint object and the required witnesses to this builder + /// it will replace any previously existing mint and mint scripts + /// NOTE! Error will be returned in case a mint policy does not have a matching script + pub fn set_mint(&mut self, mint: &Mint, mint_scripts: &NativeScripts) -> Result<(), JsError> { + assert_required_mint_scripts(mint, Some(mint_scripts))?; self.mint = Some(mint.clone()); + self.mint_scripts = Some(mint_scripts.clone()); + Ok(()) + } + + /// Returns a copy of the current mint state in the builder + pub fn get_mint(&self) -> Option { + self.mint.clone() + } + + /// Returns a copy of the current mint witness scripts in the builder + pub fn get_mint_scripts(&self) -> Option { + self.mint_scripts.clone() + } + + fn _set_mint_asset(&mut self, policy_id: &PolicyID, policy_script: &NativeScript, mint_assets: &MintAssets) { + let mut mint = self.mint.as_ref().cloned().unwrap_or(Mint::new()); + let is_new_policy = mint.insert(&policy_id, mint_assets).is_none(); + let mint_scripts = { + let mut witness_scripts = self.mint_scripts.as_ref().cloned() + .unwrap_or(NativeScripts::new()); + if is_new_policy { + // If policy has not been encountered before - insert the script into witnesses + witness_scripts.add(&policy_script.clone()); + } + witness_scripts + }; + self.mint = Some(mint); + self.mint_scripts = Some(mint_scripts.clone()); } /// Add a mint entry to this builder using a PolicyID and MintAssets object /// It will be securely added to existing or new Mint in this builder /// It will replace any existing mint assets with the same PolicyID - pub fn set_mint_asset(&mut self, policy_id: &PolicyID, mint_assets: &MintAssets) { - let mut mint = self.mint.as_ref().cloned().unwrap_or(Mint::new()); - mint.insert(policy_id, mint_assets); - self.set_mint(&mint); + pub fn set_mint_asset(&mut self, policy_script: &NativeScript, mint_assets: &MintAssets) { + let policy_id: PolicyID = policy_script.hash(ScriptHashNamespace::NativeScript); + self._set_mint_asset(&policy_id, policy_script, mint_assets); } - /// Add a mint entry to this builder using a PolicyID, AssetName, and Int object for amount - /// It will be securely added to existing or new Mint in this builder - /// It will replace any previous existing amount same PolicyID and AssetName - pub fn add_mint_asset(&mut self, policy_id: &PolicyID, asset_name: &AssetName, amount: Int) { + fn _add_mint_asset(&mut self, policy_id: &PolicyID, policy_script: &NativeScript, asset_name: &AssetName, amount: Int) { let mut asset = self.mint.as_ref() - .map(|m| { m.get(policy_id).as_ref().cloned() }) + .map(|m| { m.get(&policy_id).as_ref().cloned() }) .unwrap_or(None) .unwrap_or(MintAssets::new()); asset.insert(asset_name, amount); - self.set_mint_asset(policy_id, &asset); + self._set_mint_asset(&policy_id, policy_script, &asset); + } + + /// Add a mint entry to this builder using a PolicyID, AssetName, and Int object for amount + /// It will be securely added to existing or new Mint in this builder + /// It will replace any previous existing amount same PolicyID and AssetName + pub fn add_mint_asset(&mut self, policy_script: &NativeScript, asset_name: &AssetName, amount: Int) { + let policy_id: PolicyID = policy_script.hash(ScriptHashNamespace::NativeScript); + self._add_mint_asset(&policy_id, policy_script, asset_name, amount); } /// Add a mint entry together with an output to this builder @@ -683,7 +750,7 @@ impl TransactionBuilder { /// A new output will be added with the specified Address, the Coin value, and the minted asset pub fn add_mint_asset_and_output( &mut self, - policy_id: &PolicyID, + policy_script: &NativeScript, asset_name: &AssetName, amount: Int, address: &Address, @@ -692,9 +759,10 @@ impl TransactionBuilder { if !amount.is_positive() { return Err(JsError::from_str("Output value must be positive!")); } - self.add_mint_asset(policy_id, asset_name, amount.clone()); + let policy_id: PolicyID = policy_script.hash(ScriptHashNamespace::NativeScript); + self._add_mint_asset(&policy_id, policy_script, asset_name, amount.clone()); let multiasset = Mint::new_from_entry( - policy_id, + &policy_id, &MintAssets::new_from_entry(asset_name, amount.clone()) ).as_positive_multiasset(); self.add_output_coin_and_asset(address, output_coin, &multiasset) @@ -707,7 +775,7 @@ impl TransactionBuilder { /// The output will be set to contain the minimum required amount of Coin pub fn add_mint_asset_and_output_min_required_coin( &mut self, - policy_id: &PolicyID, + policy_script: &NativeScript, asset_name: &AssetName, amount: Int, address: &Address, @@ -715,9 +783,10 @@ impl TransactionBuilder { if !amount.is_positive() { return Err(JsError::from_str("Output value must be positive!")); } - self.add_mint_asset(policy_id, asset_name, amount.clone()); + let policy_id: PolicyID = policy_script.hash(ScriptHashNamespace::NativeScript); + self._add_mint_asset(&policy_id, policy_script, asset_name, amount.clone()); let multiasset = Mint::new_from_entry( - policy_id, + &policy_id, &MintAssets::new_from_entry(asset_name, amount.clone()) ).as_positive_multiasset(); self.add_output_asset_and_min_required_coin(address, &multiasset) @@ -740,7 +809,7 @@ impl TransactionBuilder { }, validity_start_interval: None, mint: None, - inputs_auto_added: false, + mint_scripts: None, } } @@ -1063,7 +1132,7 @@ impl TransactionBuilder { /// Returns object the body of the new transaction /// Auxiliary data itself is not included - /// You can use `get_auxiliary_date` or `build_tx` + /// You can use `get_auxiliary_data` or `build_tx` pub fn build(&self) -> Result { let (body, full_tx_size) = self.build_and_size()?; if full_tx_size > self.config.max_tx_size as usize { @@ -1077,13 +1146,25 @@ impl TransactionBuilder { } } + // This function should be producing the total witness-set + // that is created by the tx-builder itself, + // before the transaction is getting signed by the actual wallet. + // E.g. scripts or something else that has been used during the tx preparation + fn get_witness_set(&self) -> TransactionWitnessSet { + let mut wit = TransactionWitnessSet::new(); + if let Some(scripts) = self.mint_scripts.as_ref() { + wit.set_native_scripts(scripts); + } + wit + } + /// Returns full Transaction object with the body and the auxiliary data - /// NOTE: witness_set is set to just empty set + /// NOTE: witness_set will contain all mint_scripts if any been added or set /// NOTE: is_valid set to true pub fn build_tx(&self) -> Result { Ok(Transaction { body: self.build()?, - witness_set: TransactionWitnessSet::new(), + witness_set: self.get_witness_set(), is_valid: true, auxiliary_data: self.auxiliary_data.clone(), }) @@ -1119,6 +1200,12 @@ mod tests { Bip32PrivateKey::from_bip39_entropy(&entropy, &[]) } + fn fake_key_hash(x: u8) -> Ed25519KeyHash { + Ed25519KeyHash::from_bytes( + vec![x, 239, 181, 120, 142, 135, 19, 200, 68, 223, 211, 43, 46, 145, 222, 30, 48, 159, 239, 255, 213, 85, 248, 39, 204, 158, 225, 100] + ).unwrap() + } + fn harden(index: u32) -> u32 { index | 0x80_00_00_00 } @@ -1670,12 +1757,12 @@ mod tests { ) .to_address(); - let policy_id = PolicyID::from([0u8; 28]); + let (min_script, policy_id) = mint_script_and_policy(0); let name = AssetName::new(vec![0u8, 1, 2, 3]).unwrap(); let amount = to_bignum(1234); // Adding mint of the asset - which should work as an input - tx_builder.add_mint_asset(&policy_id, &name, Int::new(&amount)); + tx_builder.add_mint_asset(&min_script, &name, Int::new(&amount)); let mut ass = Assets::new(); ass.insert(&name, &amount); @@ -1753,14 +1840,14 @@ mod tests { ) .to_address(); - let policy_id = PolicyID::from([0u8; 28]); + let (min_script, policy_id) = mint_script_and_policy(0); let name = AssetName::new(vec![0u8, 1, 2, 3]).unwrap(); let amount_minted = to_bignum(1000); let amount_sent = to_bignum(500); // Adding mint of the asset - which should work as an input - tx_builder.add_mint_asset(&policy_id, &name, Int::new(&amount_minted)); + tx_builder.add_mint_asset(&min_script, &name, Int::new(&amount_minted)); let mut ass = Assets::new(); ass.insert(&name, &amount_sent); @@ -3149,72 +3236,117 @@ mod tests { assert_eq!(result_asset.get(&create_asset_name()).unwrap(), Int::new_i32(1234)); } + fn mint_script_and_policy_and_hash(x: u8) -> (NativeScript, PolicyID, Ed25519KeyHash) { + let hash = fake_key_hash(x); + let mint_script = NativeScript::new_script_pubkey( + &ScriptPubkey::new(&hash) + ); + let policy_id = mint_script.hash(ScriptHashNamespace::NativeScript); + (mint_script, policy_id, hash) + } + + fn mint_script_and_policy(x: u8) -> (NativeScript, PolicyID) { + let (m, p, _) = mint_script_and_policy_and_hash(x); + (m, p) + } + #[test] fn set_mint_asset_with_empty_mint() { let mut tx_builder = create_default_tx_builder(); - let policy_id = PolicyID::from([0u8; 28]); - tx_builder.set_mint_asset(&policy_id, &create_mint_asset()); + let (mint_script, policy_id) = mint_script_and_policy(0); + tx_builder.set_mint_asset(&mint_script, &create_mint_asset()); assert!(tx_builder.mint.is_some()); + assert!(tx_builder.mint_scripts.is_some()); let mint = tx_builder.mint.unwrap(); + let mint_scripts = tx_builder.mint_scripts.unwrap(); assert_eq!(mint.len(), 1); assert_mint_asset(&mint, &policy_id); + + assert_eq!(mint_scripts.len(), 1); + assert_eq!(mint_scripts.get(0), mint_script); } #[test] fn set_mint_asset_with_existing_mint() { let mut tx_builder = create_default_tx_builder(); - let policy_id1 = PolicyID::from([0u8; 28]); - tx_builder.set_mint(&create_mint_with_one_asset(&policy_id1)); + let (mint_script1, policy_id1) = mint_script_and_policy(0); + let (mint_script2, policy_id2) = mint_script_and_policy(1); + + tx_builder.set_mint( + &create_mint_with_one_asset(&policy_id1), + &NativeScripts::from(vec![mint_script1.clone()]), + ).unwrap(); - let policy_id2 = PolicyID::from([1u8; 28]); - tx_builder.set_mint_asset(&policy_id2, &create_mint_asset()); + tx_builder.set_mint_asset(&mint_script2, &create_mint_asset()); assert!(tx_builder.mint.is_some()); + assert!(tx_builder.mint_scripts.is_some()); let mint = tx_builder.mint.unwrap(); + let mint_scripts = tx_builder.mint_scripts.unwrap(); assert_eq!(mint.len(), 2); assert_mint_asset(&mint, &policy_id1); assert_mint_asset(&mint, &policy_id2); + + // Only second script is present in the scripts + assert_eq!(mint_scripts.len(), 2); + assert_eq!(mint_scripts.get(0), mint_script1); + assert_eq!(mint_scripts.get(1), mint_script2); } #[test] fn add_mint_asset_with_empty_mint() { let mut tx_builder = create_default_tx_builder(); - let policy_id = PolicyID::from([0u8; 28]); - tx_builder.add_mint_asset(&policy_id, &create_asset_name(), Int::new_i32(1234)); + let (mint_script, policy_id) = mint_script_and_policy(0); + + tx_builder.add_mint_asset(&mint_script, &create_asset_name(), Int::new_i32(1234)); assert!(tx_builder.mint.is_some()); + assert!(tx_builder.mint_scripts.is_some()); let mint = tx_builder.mint.unwrap(); + let mint_scripts = tx_builder.mint_scripts.unwrap(); assert_eq!(mint.len(), 1); assert_mint_asset(&mint, &policy_id); + + assert_eq!(mint_scripts.len(), 1); + assert_eq!(mint_scripts.get(0), mint_script); } #[test] fn add_mint_asset_with_existing_mint() { let mut tx_builder = create_default_tx_builder(); - let policy_id1 = PolicyID::from([0u8; 28]); - tx_builder.set_mint(&create_mint_with_one_asset(&policy_id1)); + let (mint_script1, policy_id1) = mint_script_and_policy(0); + let (mint_script2, policy_id2) = mint_script_and_policy(1); - let policy_id2 = PolicyID::from([1u8; 28]); - tx_builder.add_mint_asset(&policy_id2, &create_asset_name(), Int::new_i32(1234)); + tx_builder.set_mint( + &create_mint_with_one_asset(&policy_id1), + &NativeScripts::from(vec![mint_script1.clone()]), + ).unwrap(); + tx_builder.add_mint_asset(&mint_script2, &create_asset_name(), Int::new_i32(1234)); assert!(tx_builder.mint.is_some()); + assert!(tx_builder.mint_scripts.is_some()); let mint = tx_builder.mint.unwrap(); + let mint_scripts = tx_builder.mint_scripts.unwrap(); assert_eq!(mint.len(), 2); assert_mint_asset(&mint, &policy_id1); assert_mint_asset(&mint, &policy_id2); + + assert_eq!(mint_scripts.len(), 2); + assert_eq!(mint_scripts.get(0), mint_script1); + assert_eq!(mint_scripts.get(1), mint_script2); } #[test] @@ -3294,8 +3426,9 @@ mod tests { fn add_mint_asset_and_output() { let mut tx_builder = create_default_tx_builder(); - let policy_id0 = PolicyID::from([0u8; 28]); - let policy_id1 = PolicyID::from([1u8; 28]); + let (mint_script0, policy_id0) = mint_script_and_policy(0); + let (mint_script1, policy_id1) = mint_script_and_policy(1); + let name = create_asset_name(); let amount = Int::new_i32(1234); @@ -3303,10 +3436,10 @@ mod tests { let coin = to_bignum(100); // Add unrelated mint first to check it is NOT added to output later - tx_builder.add_mint_asset(&policy_id0, &name, amount.clone()); + tx_builder.add_mint_asset(&mint_script0, &name, amount.clone()); tx_builder.add_mint_asset_and_output( - &policy_id1, + &mint_script1, &name, amount.clone(), &address, @@ -3314,14 +3447,20 @@ mod tests { ).unwrap(); assert!(tx_builder.mint.is_some()); + assert!(tx_builder.mint_scripts.is_some()); let mint = tx_builder.mint.as_ref().unwrap(); + let mint_scripts = tx_builder.mint_scripts.as_ref().unwrap(); // Mint contains two entries assert_eq!(mint.len(), 2); assert_mint_asset(mint, &policy_id0); assert_mint_asset(mint, &policy_id1); + assert_eq!(mint_scripts.len(), 2); + assert_eq!(mint_scripts.get(0), mint_script0); + assert_eq!(mint_scripts.get(1), mint_script1); + // One new output is created assert_eq!(tx_builder.outputs.len(), 1); let out = tx_builder.outputs.get(0); @@ -3345,32 +3484,40 @@ mod tests { fn add_mint_asset_and_min_required_coin() { let mut tx_builder = create_reallistic_tx_builder(); - let policy_id0 = PolicyID::from([0u8; 28]); - let policy_id1 = PolicyID::from([1u8; 28]); + let (mint_script0, policy_id0) = mint_script_and_policy(0); + let (mint_script1, policy_id1) = mint_script_and_policy(1); + let name = create_asset_name(); let amount = Int::new_i32(1234); let address = byron_address(); // Add unrelated mint first to check it is NOT added to output later - tx_builder.add_mint_asset(&policy_id0, &name, amount.clone()); + tx_builder.add_mint_asset(&mint_script0, &name, amount.clone()); tx_builder.add_mint_asset_and_output_min_required_coin( - &policy_id1, + &mint_script1, &name, amount.clone(), &address, ).unwrap(); assert!(tx_builder.mint.is_some()); + assert!(tx_builder.mint_scripts.is_some()); let mint = tx_builder.mint.as_ref().unwrap(); + let mint_scripts = tx_builder.mint_scripts.as_ref().unwrap(); // Mint contains two entries assert_eq!(mint.len(), 2); assert_mint_asset(mint, &policy_id0); assert_mint_asset(mint, &policy_id1); + + assert_eq!(mint_scripts.len(), 2); + assert_eq!(mint_scripts.get(0), mint_script0); + assert_eq!(mint_scripts.get(1), mint_script1); + // One new output is created assert_eq!(tx_builder.outputs.len(), 1); let out = tx_builder.outputs.get(0); @@ -3390,66 +3537,151 @@ mod tests { assert_eq!(asset.get(&name).unwrap(), to_bignum(1234)); } - #[test] fn add_mint_includes_witnesses_into_fee_estimation() { + let mut tx_builder = create_reallistic_tx_builder(); - let original_tx_fee = tx_builder.min_fee().unwrap(); - assert_eq!(original_tx_fee, to_bignum(156217)); + let hash0 = fake_key_hash(0); + + let (mint_script1, _, hash1) = mint_script_and_policy_and_hash(1); + let (mint_script2, _, _) = mint_script_and_policy_and_hash(2); + let (mint_script3, _, _) = mint_script_and_policy_and_hash(3); - let policy_id1 = PolicyID::from([0u8; 28]); - let policy_id2 = PolicyID::from([1u8; 28]); - let policy_id3 = PolicyID::from([2u8; 28]); let name1 = AssetName::new(vec![0u8, 1, 2, 3]).unwrap(); let name2 = AssetName::new(vec![1u8, 1, 2, 3]).unwrap(); let name3 = AssetName::new(vec![2u8, 1, 2, 3]).unwrap(); let name4 = AssetName::new(vec![3u8, 1, 2, 3]).unwrap(); let amount = Int::new_i32(1234); - let mut mint = Mint::new(); - mint.insert( - &policy_id1, - &MintAssets::new_from_entry(&name1, amount.clone()), + // One input from unrelated address + tx_builder.add_key_input( + &hash0, + &TransactionInput::new(&genesis_id(), 0), + &Value::new(&to_bignum(10_000_000)), ); - mint.insert( - &policy_id2, - &MintAssets::new_from_entry(&name2, amount.clone()), + + // One input from same address as mint + tx_builder.add_key_input( + &hash1, + &TransactionInput::new(&genesis_id(), 0), + &Value::new(&to_bignum(10_000_000)), ); - // Third policy with two asset names - let mut mass = MintAssets::new_from_entry(&name3, amount.clone()); - mass.insert(&name4, amount.clone()); - mint.insert(&policy_id3, &mass); + // Original tx fee now assumes two VKey signatures for two inputs + let original_tx_fee = tx_builder.min_fee().unwrap(); + assert_eq!(original_tx_fee, to_bignum(168361)); + + // Add minting four assets from three different policies + tx_builder.add_mint_asset(&mint_script1, &name1, amount.clone()); + tx_builder.add_mint_asset(&mint_script2, &name2, amount.clone()); + tx_builder.add_mint_asset(&mint_script3, &name3, amount.clone()); + tx_builder.add_mint_asset(&mint_script3, &name4, amount.clone()); + + let mint = tx_builder.get_mint().unwrap(); let mint_len = mint.to_bytes().len(); + + let mint_scripts = tx_builder.get_witness_set(); + let mint_scripts_len = mint_scripts.to_bytes().len() + - TransactionWitnessSet::new().to_bytes().len(); + let fee_coefficient = tx_builder.config.fee_algo.coefficient(); let raw_mint_fee = fee_coefficient .checked_mul(&to_bignum(mint_len as u64)) .unwrap(); - assert_eq!(raw_mint_fee, to_bignum(5544)); + let raw_mint_script_fee = fee_coefficient + .checked_mul(&to_bignum(mint_scripts_len as u64)) + .unwrap(); - tx_builder.set_mint(&mint); + assert_eq!(raw_mint_fee, to_bignum(5544)); + assert_eq!(raw_mint_script_fee, to_bignum(4312)); let new_tx_fee = tx_builder.min_fee().unwrap(); - let fee_diff_from_adding_mint = - new_tx_fee.checked_sub(&original_tx_fee) + let fee_diff_from_adding_mint = new_tx_fee + .checked_sub(&original_tx_fee) .unwrap(); - let witness_fee_increase = - fee_diff_from_adding_mint.checked_sub(&raw_mint_fee) - .unwrap(); + let witness_fee_increase = fee_diff_from_adding_mint + .checked_sub(&raw_mint_fee).unwrap() + .checked_sub(&raw_mint_script_fee).unwrap(); - assert_eq!(witness_fee_increase, to_bignum(4356)); + assert_eq!(witness_fee_increase, to_bignum(8932)); let fee_increase_bytes = from_bignum(&witness_fee_increase) .checked_div(from_bignum(&fee_coefficient)) .unwrap(); - // Three policy IDs of 32 bytes each + 3 byte overhead - assert_eq!(fee_increase_bytes, 99); + // Two vkey witnesses 96 bytes each (32 byte pubkey + 64 byte signature) + // Plus 11 bytes overhead for CBOR wrappers + // This is happening because we have three different minting policies + // but the same key-hash from one of them is already also used in inputs + // so no suplicate witness signature is require for that one + assert_eq!(fee_increase_bytes, 203); + } + + #[test] + fn fee_estimation_fails_on_missing_mint_scripts() { + let mut tx_builder = create_reallistic_tx_builder(); + + // No error estimating fee without mint + assert!(tx_builder.min_fee().is_ok()); + + let (mint_script1, policy_id1) = mint_script_and_policy(0); + let (mint_script2, _) = mint_script_and_policy(1); + + let name1 = AssetName::new(vec![0u8, 1, 2, 3]).unwrap(); + let amount = Int::new_i32(1234); + + let mut mint = Mint::new(); + mint.insert( + &policy_id1, + &MintAssets::new_from_entry(&name1, amount.clone()), + ); + + tx_builder.set_mint( + &mint, + &NativeScripts::from(vec![mint_script1]), + ).unwrap(); + + let est1 = tx_builder.min_fee(); + assert!(est1.is_ok()); + + tx_builder.add_mint_asset(&mint_script2, &name1, amount.clone()); + + let est2 = tx_builder.min_fee(); + assert!(est2.is_ok()); + + // Native script assertion has been commented out in `.min_fee` + // Until implemented in a more performant manner + // TODO: these test parts might be returned back when it's done + + // // Remove one mint script + // tx_builder.mint_scripts = + // Some(NativeScripts::from(vec![tx_builder.mint_scripts.unwrap().get(1)])); + // + // // Now two different policies are minted but only one witness script is present + // let est3 = tx_builder.min_fee(); + // assert!(est3.is_err()); + // assert!(est3.err().unwrap().to_string().contains(&format!("{:?}", hex::encode(policy_id1.to_bytes())))); + // + // // Remove all mint scripts + // tx_builder.mint_scripts = Some(NativeScripts::new()); + // + // // Mint exists but no witness scripts at all present + // let est4 = tx_builder.min_fee(); + // assert!(est4.is_err()); + // assert!(est4.err().unwrap().to_string().contains("witness scripts are not provided")); + // + // // Remove all mint scripts + // tx_builder.mint_scripts = None; + // + // // Mint exists but no witness scripts at all present + // let est5 = tx_builder.min_fee(); + // assert!(est5.is_err()); + // assert!(est5.err().unwrap().to_string().contains("witness scripts are not provided")); } #[test] @@ -3463,8 +3695,9 @@ mod tests { .derive(0) .to_public(); - let policy_id1 = &PolicyID::from([0u8; 28]); - let policy_id2 = &PolicyID::from([1u8; 28]); + let (mint_script1, policy_id1) = mint_script_and_policy(0); + let (mint_script2, policy_id2) = mint_script_and_policy(1); + let name = AssetName::new(vec![0u8, 1, 2, 3]).unwrap(); let ma_input1 = 100; @@ -3475,12 +3708,12 @@ mod tests { .iter() .map(|input| { let mut multiasset = MultiAsset::new(); - multiasset.insert(policy_id1, &{ + multiasset.insert(&policy_id1, &{ let mut assets = Assets::new(); assets.insert(&name, &to_bignum(*input)); assets }); - multiasset.insert(policy_id2, &{ + multiasset.insert(&policy_id2, &{ let mut assets = Assets::new(); assets.insert(&name, &to_bignum(*input)); assets @@ -3507,18 +3740,18 @@ mod tests { assert_eq!(total_input_before_mint.coin, to_bignum(300)); let ma1 = total_input_before_mint.multiasset.unwrap(); - assert_eq!(ma1.get(policy_id1).unwrap().get(&name).unwrap(), to_bignum(360)); - assert_eq!(ma1.get(policy_id2).unwrap().get(&name).unwrap(), to_bignum(360)); + assert_eq!(ma1.get(&policy_id1).unwrap().get(&name).unwrap(), to_bignum(360)); + assert_eq!(ma1.get(&policy_id2).unwrap().get(&name).unwrap(), to_bignum(360)); - tx_builder.add_mint_asset(policy_id1, &name, Int::new_i32(40)); - tx_builder.add_mint_asset(policy_id2, &name, Int::new_i32(-40)); + tx_builder.add_mint_asset(&mint_script1, &name, Int::new_i32(40)); + tx_builder.add_mint_asset(&mint_script2, &name, Int::new_i32(-40)); let total_input_after_mint = tx_builder.get_total_input().unwrap(); assert_eq!(total_input_after_mint.coin, to_bignum(300)); let ma2 = total_input_after_mint.multiasset.unwrap(); - assert_eq!(ma2.get(policy_id1).unwrap().get(&name).unwrap(), to_bignum(400)); - assert_eq!(ma2.get(policy_id2).unwrap().get(&name).unwrap(), to_bignum(320)); + assert_eq!(ma2.get(&policy_id1).unwrap().get(&name).unwrap(), to_bignum(400)); + assert_eq!(ma2.get(&policy_id2).unwrap().get(&name).unwrap(), to_bignum(320)); } }