Skip to content

Commit

Permalink
Utxo chains in inputs + input selection tests (#1164)
Browse files Browse the repository at this point in the history
* Utxo chains in inputs + input selection tests

* Add change file

* Add more input selection tests

* clippy

* Add tests with foundries

* Clippy
  • Loading branch information
Thoralf-M authored Aug 1, 2022
1 parent a582bfa commit 8e8715c
Show file tree
Hide file tree
Showing 17 changed files with 756 additions and 22 deletions.
6 changes: 6 additions & 0 deletions .changes/utxo chain inputs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

---
"nodejs-binding": patch
---

Improve handling for utxo chains in input selection.
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ categories = [ "cryptography::cryptocurrencies" ]
[dependencies]
async-trait = { version = "0.1.56", default-features = false }
bee-api-types = { version = "1.0.0-beta.3", default-features = false }
bee-block = { version = "1.0.0-beta.3", default-features = false, features = [ "serde", "dto", "std" ] }
bee-block = { version = "1.0.0-beta.5", default-features = false, features = [ "serde", "dto", "std" ] }
bee-pow = { version = "1.0.0-alpha.1", default-features = false }
derive_builder = { version = "0.11.2", default-features = false, features = [ "std" ]}
futures = { version = "0.3.21", default-features = false, features = [ "thread-pool" ] }
Expand Down Expand Up @@ -56,6 +56,7 @@ bee-ternary = { version = "1.0.0-alpha.1", default-features = false }

[dev-dependencies]
dotenv = { version = "0.15.0", default-features = false }
bee-block = { version = "1.0.0-beta.5", default-features = false, features = [ "rand", "std" ] }

[features]
default = [ "tls" ]
Expand Down
4 changes: 2 additions & 2 deletions bindings/java/iota-client-java/native/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/api/block_builder/input_selection/automatic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ async fn get_inputs_for_sender_and_issuer(
}

if !found_output {
return Err(Error::MissingInputWithEd25519UnlockCondition);
return Err(Error::MissingInputWithEd25519Address);
}
}
}
Expand Down
154 changes: 153 additions & 1 deletion src/api/block_builder/input_selection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ use bee_block::{
input::INPUT_COUNT_MAX,
output::{
unlock_condition::{AddressUnlockCondition, StorageDepositReturnUnlockCondition},
BasicOutputBuilder, NativeTokens, Output, Rent, RentStructure, UnlockCondition, OUTPUT_COUNT_MAX,
AliasOutputBuilder, BasicOutputBuilder, FoundryOutputBuilder, NativeTokens, NftOutputBuilder, Output, Rent,
RentStructure, UnlockCondition, OUTPUT_COUNT_MAX,
},
};
use packable::{bounded::TryIntoBoundedU16Error, PackableExt};
Expand Down Expand Up @@ -109,6 +110,86 @@ pub fn try_select_inputs(
// 1. get alias, foundry or nft inputs (because amount and native tokens of these outputs will also be available for
// the outputs)
for input_signing_data in utxo_chain_outputs {
// Only add outputs if also exisiting on the output side or if burning is allowed
let minimum_required_storage_deposit = input_signing_data.output.rent_cost(rent_structure);
let output_id = input_signing_data.output_id()?;

match &input_signing_data.output {
Output::Nft(nft_input) => {
// or if an output contains an nft output with the same nft id
let is_required = outputs.iter().any(|output| {
if let Output::Nft(nft_output) = output {
nft_input.nft_id().or_from_output_id(output_id) == *nft_output.nft_id()
} else {
false
}
});
if !is_required && !allow_burning {
// Don't add if it doesn't give us any amount or native tokens
if input_signing_data.output.amount() == minimum_required_storage_deposit
&& nft_input.native_tokens().is_empty()
{
continue;
}
// else add output to outputs with minimum_required_storage_deposit
let new_output = NftOutputBuilder::from(nft_input)
.with_nft_id(nft_input.nft_id().or_from_output_id(output_id))
.with_amount(minimum_required_storage_deposit)?
.finish_output()?;
outputs.push(new_output);
}
}
Output::Alias(alias_input) => {
// Don't add if output has not the same AliasId, so we don't burn it
if !outputs.iter().any(|output| {
if let Output::Alias(alias_output) = output {
alias_input.alias_id().or_from_output_id(output_id) == *alias_output.alias_id()
} else {
false
}
}) && !allow_burning
{
// Don't add if it doesn't give us any amount or native tokens
if input_signing_data.output.amount() == minimum_required_storage_deposit
&& alias_input.native_tokens().is_empty()
{
continue;
}
// else add output to outputs with minimum_required_storage_deposit
let new_output = AliasOutputBuilder::from(alias_input)
.with_alias_id(alias_input.alias_id().or_from_output_id(output_id))
.with_state_index(alias_input.state_index() + 1)
.with_amount(minimum_required_storage_deposit)?
.finish_output()?;
outputs.push(new_output);
}
}
Output::Foundry(foundry_input) => {
// Don't add if output has not the same FoundryId, so we don't burn it
if !outputs.iter().any(|output| {
if let Output::Foundry(foundry_output) = output {
foundry_input.id() == foundry_output.id()
} else {
false
}
}) && !allow_burning
{
// Don't add if it doesn't give us any amount or native tokens
if input_signing_data.output.amount() == minimum_required_storage_deposit
&& foundry_input.native_tokens().is_empty()
{
continue;
}
// else add output to outputs with minimum_required_storage_deposit
let new_output = FoundryOutputBuilder::from(foundry_input)
.with_amount(minimum_required_storage_deposit)?
.finish_output()?;
outputs.push(new_output);
}
}
_ => {}
}

let output = &input_signing_data.output;
selected_input_amount += output.amount();

Expand All @@ -122,6 +203,77 @@ pub fn try_select_inputs(
}

selected_inputs.push(input_signing_data.clone());

// Updated required value with possible new input
let input_outputs = inputs.iter().map(|i| &i.output);
required = get_accumulated_output_amounts(&input_outputs.clone(), outputs.iter())?;
}

// Validate that we have the required inputs for alias and nft outputs
for output in &outputs {
match output {
Output::Alias(alias_output) => {
// New created output requires no specific input
let alias_id = alias_output.alias_id();
if alias_id.is_null() {
continue;
}

if !selected_inputs.iter().any(|data| {
if let Output::Alias(input_alias_output) = &data.output {
input_alias_output
.alias_id()
.or_from_output_id(data.output_id().expect("Invalid output id"))
== *alias_id
} else {
false
}
}) {
return Err(crate::Error::MissingInput(format!(
"Missing alias input for {alias_id}"
)));
}
}
Output::Foundry(foundry_output) => {
let required_alias = foundry_output.alias_address().alias_id();
if !selected_inputs.iter().any(|data| {
if let Output::Alias(input_alias_output) = &data.output {
input_alias_output
.alias_id()
.or_from_output_id(data.output_id().expect("Invalid output id"))
== *required_alias
} else {
false
}
}) {
return Err(crate::Error::MissingInput(format!(
"Missing alias input {required_alias} for foundry {}",
foundry_output.id()
)));
}
}
Output::Nft(nft_output) => {
// New created output requires no specific input
let nft_id = nft_output.nft_id();
if nft_id.is_null() {
continue;
}

if !selected_inputs.iter().any(|data| {
if let Output::Nft(input_nft_output) = &data.output {
input_nft_output
.nft_id()
.or_from_output_id(data.output_id().expect("Invalid output id"))
== *nft_id
} else {
false
}
}) {
return Err(crate::Error::MissingInput(format!("Missing nft input for {nft_id}")));
}
}
_ => {}
}
}

// 2. get basic inputs for the required native tokens (because the amount of these outputs will also be available in
Expand Down
20 changes: 19 additions & 1 deletion src/api/block_builder/input_selection/remainder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,32 @@ pub(crate) fn get_remainder_address<'a>(
inputs: impl Iterator<Item = &'a InputSigningData>,
) -> Result<(Address, Option<Chain>)> {
for input in inputs {
// todo: check expiration with time, for now we just ignore outputs with an expiration unlock condition here
if input
.output
.unlock_conditions()
.and_then(UnlockConditions::expiration)
.is_some()
{
continue;
}
if let Some(address_unlock_condition) = input.output.unlock_conditions().and_then(UnlockConditions::address) {
if address_unlock_condition.address().is_ed25519() {
return Ok((*address_unlock_condition.address(), input.chain.clone()));
}
}
if let Some(governor_address_unlock_condition) = input
.output
.unlock_conditions()
.and_then(UnlockConditions::governor_address)
{
if governor_address_unlock_condition.address().is_ed25519() {
return Ok((*governor_address_unlock_condition.address(), input.chain.clone()));
}
}
}

Err(Error::MissingInputWithEd25519UnlockCondition)
Err(Error::MissingInputWithEd25519Address)
}

// Get additional required storage deposit amount for the remainder output
Expand Down
2 changes: 1 addition & 1 deletion src/api/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use crate::{
pub struct PreparedTransactionData {
/// Transaction essence
pub essence: TransactionEssence,
/// Required address information for signing
/// Required input information for signing. Inputs need to be ordered by address type
#[serde(rename = "inputsData")]
pub inputs_data: Vec<InputSigningData>,
/// Optional remainder output information
Expand Down
9 changes: 6 additions & 3 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ pub enum Error {
"The wallet account has enough funds, but splitted on too many outputs: {0}, max. is 128, consolidate them"
)]
ConsolidationRequired(usize),
/// Missing input for utxo chain
#[error("Missing input: {0}")]
MissingInput(String),
/// Missing required parameters
#[error("Must provide required parameter: {0}")]
MissingParameter(&'static str),
Expand Down Expand Up @@ -194,9 +197,9 @@ pub enum Error {
/// Specifically used for `TryInfo` implementations for `SecretManager`.
#[error("cannot unwrap a SecretManager: type mismatch!")]
SecretManagerMismatch,
/// No input with matching ed25519 unlock condition provided
#[error("No input with matching ed25519 unlock condition provided")]
MissingInputWithEd25519UnlockCondition,
/// No input with matching ed25519 address provided
#[error("No input with matching ed25519 address provided")]
MissingInputWithEd25519Address,
/// Ledger transport error
#[cfg(feature = "ledger_nano")]
#[error("ledger transport error")]
Expand Down
11 changes: 7 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@
//! High-level functions are accessible via the [`Client`][client::Client].
//!
//! ## Sending a block without a payload
//! ```compile_fail
//! ```no_run
//! # use iota_client::{Client, Result};
//! # #[tokio::main]
//! # async fn main() -> Result<()> {
//! let client = Client::builder()
//! .with_node("http://localhost:14265")?
//! .finish()
//! .await?;
//! .finish()?;
//!
//! let block = iota
//! let block = client
//! .block()
//! .finish()
//! .await?;
//!
//! println!("Block sent {}", block.id());
//! # Ok(())}
//! ```
#![deny(unused_extern_crates)]
Expand Down
9 changes: 4 additions & 5 deletions src/secret/ledger_nano.rs
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ fn merge_unlocks(
let mut merged_unlocks = Vec::new();
let mut block_indexes = HashMap::<Address, usize>::new();

// Assuming inputs_data is ordered by address type
for (current_block_index, input) in prepared_transaction_data.inputs_data.iter().enumerate() {
// Get the address that is required to unlock the input
let (_, input_address) = Address::try_from_bech32(&input.bech32_address)?;
Expand All @@ -404,18 +405,16 @@ fn merge_unlocks(
// address already at this point, because the reference index needs to be lower
// than the current block index
if !input_address.is_ed25519() {
return Err(crate::Error::MissingInputWithEd25519UnlockCondition);
return Err(crate::Error::MissingInputWithEd25519Address);
}

let unlock = unlocks
.next()
.ok_or(crate::Error::MissingInputWithEd25519UnlockCondition)?;
let unlock = unlocks.next().ok_or(crate::Error::MissingInputWithEd25519Address)?;

if let Unlock::Signature(signature_unlock) = &unlock {
let Signature::Ed25519(ed25519_signature) = signature_unlock.signature();
let ed25519_address = match input_address {
Address::Ed25519(ed25519_address) => ed25519_address,
_ => return Err(crate::Error::MissingInputWithEd25519UnlockCondition),
_ => return Err(crate::Error::MissingInputWithEd25519Address),
};
ed25519_signature.is_valid(&hashed_essence, &ed25519_address)?;
}
Expand Down
3 changes: 2 additions & 1 deletion src/secret/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ impl SecretManager {
let mut blocks = Vec::new();
let mut block_indexes = HashMap::<Address, usize>::new();

// Assuming inputs_data is ordered by address type
for (current_block_index, input) in prepared_transaction_data.inputs_data.iter().enumerate() {
// Get the address that is required to unlock the input
let (_, input_address) = Address::try_from_bech32(&input.bech32_address)?;
Expand All @@ -313,7 +314,7 @@ impl SecretManager {
// address already at this point, because the reference index needs to be lower
// than the current block index
if !input_address.is_ed25519() {
return Err(crate::Error::MissingInputWithEd25519UnlockCondition);
return Err(crate::Error::MissingInputWithEd25519Address);
}

let block = self
Expand Down
Loading

0 comments on commit 8e8715c

Please sign in to comment.