Skip to content

Commit

Permalink
Timelock Expiration SlotIndex (#1661)
Browse files Browse the repository at this point in the history
* Timelock/Expiration validation with a CommitmentInput

* Oh no

* Correct is_expired

* TODO

* meh

* TODO

* Nits

* Fix is_timelocked

* Fix is_expired

* First batch of changes

* Second batch of changes

* Fixes

* Simplified required_address

* Infallible required_address

* Some fixes

* Remove commented code

* Fix conflicts

* Fix rebase

* Address some todos

* Simplify delegation unlock

* Address more todos

* Fix some checks and cleanup

* Fix test

* Address review comments

* Store protocol parameters in file

* Fix params

* Order

* Use wrong tx failure reason

* Rename error

* Update sdk/src/client/error.rs

Co-authored-by: Thibault Martinez <[email protected]>

* Update merged part

* Update sdk/src/types/block/output/anchor.rs

Co-authored-by: Thibault Martinez <[email protected]>

* Address reviews

* Use flat_map()

* Review suggestion

* Remove superfluous Copy bounds

* Update sdk/src/client/secret/ledger_nano.rs

Co-authored-by: DaughterOfMars <[email protected]>

* Cleanup find_map

* Update sdk/src/wallet/operations/helpers/time.rs

Co-authored-by: /alex/ <[email protected]>

* Address review comments

* Rename to CommittableAgeRange

* Use is_some_and()

* Fix match

* Update sdk/src/types/block/protocol/mod.rs

Co-authored-by: /alex/ <[email protected]>

* Rename params

* Review suggestions

* Return Result from locked_address()

* Review suggestions

* Remove into

---------

Co-authored-by: Thibault Martinez <[email protected]>
Co-authored-by: DaughterOfMars <[email protected]>
Co-authored-by: /alex/ <[email protected]>
  • Loading branch information
4 people authored Dec 7, 2023
1 parent 9cd9121 commit d691cea
Show file tree
Hide file tree
Showing 49 changed files with 580 additions and 294 deletions.
3 changes: 2 additions & 1 deletion bindings/core/src/method/secret_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crypto::keys::bip44::Bip44;
use derivative::Derivative;
use iota_sdk::{
client::api::{GetAddressesOptions, PreparedTransactionDataDto},
types::block::UnsignedBlockDto,
types::block::{protocol::ProtocolParameters, UnsignedBlockDto},
utils::serde::bip44::Bip44Def,
};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -61,6 +61,7 @@ pub enum SecretManagerMethod {
SignTransaction {
/// Prepared transaction data
prepared_transaction_data: PreparedTransactionDataDto,
protocol_parameters: Box<ProtocolParameters>,
},
// Sign a block.
#[serde(rename_all = "camelCase")]
Expand Down
1 change: 1 addition & 0 deletions bindings/core/src/method/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,5 +126,6 @@ pub enum UtilsMethod {
transaction: TransactionDto,
inputs: Vec<InputSigningData>,
unlocks: Option<Vec<Unlock>>,
protocol_parameters: ProtocolParameters,
},
}
2 changes: 1 addition & 1 deletion bindings/core/src/method/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#[cfg(feature = "stronghold")]
use std::path::PathBuf;

use crypto::{keys::bip44::Bip44, signatures::ed25519::PublicKey};
use crypto::keys::bip44::Bip44;
use derivative::Derivative;
#[cfg(feature = "events")]
use iota_sdk::wallet::events::types::{WalletEvent, WalletEventType};
Expand Down
6 changes: 5 additions & 1 deletion bindings/core/src/method_handler/secret_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,13 @@ where
}
SecretManagerMethod::SignTransaction {
prepared_transaction_data,
protocol_parameters,
} => {
let transaction = &secret_manager
.sign_transaction(PreparedTransactionData::try_from_dto(prepared_transaction_data)?)
.sign_transaction(
PreparedTransactionData::try_from_dto(prepared_transaction_data)?,
&protocol_parameters,
)
.await
.map_err(iota_sdk::client::Error::from)?;
Response::SignedTransaction(transaction.into())
Expand Down
4 changes: 3 additions & 1 deletion bindings/core/src/method_handler/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,16 @@ pub(crate) fn call_utils_method_internal(method: UtilsMethod) -> Result<Response
transaction,
inputs,
unlocks,
protocol_parameters,
} => {
let transaction = Transaction::try_from_dto(transaction)?;
let inputs = inputs
.iter()
.map(|input| (input.output_id(), &input.output))
.collect::<Vec<(&OutputId, &Output)>>();

let context = SemanticValidationContext::new(&transaction, &inputs, unlocks.as_deref());
let context =
SemanticValidationContext::new(&transaction, &inputs, unlocks.as_deref(), protocol_parameters);

Response::TransactionFailureReason(context.validate()?)
}
Expand Down
10 changes: 7 additions & 3 deletions cli/src/wallet_cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,7 @@ async fn print_wallet_address(wallet: &Wallet) -> Result<(), Error> {
);

let slot_index = wallet.client().get_slot_index().await?;
let protocol_parameters = wallet.client().get_protocol_parameters().await?;

let mut output_ids = Vec::new();
let mut amount = 0;
Expand All @@ -986,11 +987,14 @@ async fn print_wallet_address(wallet: &Wallet) -> Result<(), Error> {
output_ids.push(output_id);

// Output might be associated with the address, but can't be unlocked by it, so we check that here.
let (required_address, _) = &output_data
let required_address = &output_data
.output
.required_and_unlocked_address(slot_index, &output_id)?;
.required_address(slot_index, protocol_parameters.committable_age_range())?;

if address.inner() == required_address {
if required_address
.as_ref()
.is_some_and(|required_address| required_address == address.inner())
{
if let Some(nt) = output_data.output.native_token() {
native_tokens.add_native_token(*nt)?;
}
Expand Down
27 changes: 17 additions & 10 deletions sdk/examples/wallet/offline_signing/1_prepare_transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,15 @@
//! ```

use iota_sdk::{
client::{
api::{PreparedTransactionData, PreparedTransactionDataDto},
constants::SHIMMER_COIN_TYPE,
secret::SecretManager,
},
client::{api::PreparedTransactionDataDto, constants::SHIMMER_COIN_TYPE, secret::SecretManager},
crypto::keys::bip44::Bip44,
wallet::{types::Bip44Address, ClientOptions, Result, SendParams, Wallet},
};

const ONLINE_WALLET_DB_PATH: &str = "./examples/wallet/offline_signing/example-online-walletdb";
const ADDRESS_FILE_PATH: &str = "./examples/wallet/offline_signing/example.address.json";
const PREPARED_TRANSACTION_FILE_PATH: &str = "./examples/wallet/offline_signing/example.prepared_transaction.json";
const PROTOCOL_PARAMETERS_FILE_PATH: &str = "./examples/wallet/offline_signing/example.protocol_parameters.json";
// Address to which we want to send the amount.
const RECV_ADDRESS: &str = "rms1qpszqzadsym6wpppd6z037dvlejmjuke7s24hm95s9fg9vpua7vluaw60xu";
// The amount to send.
Expand Down Expand Up @@ -60,7 +57,17 @@ async fn main() -> Result<()> {

println!("Prepared transaction sending {params:?}");

write_transaction_to_file(prepared_transaction).await?;
write_data_to_file(
PreparedTransactionDataDto::from(&prepared_transaction),
PREPARED_TRANSACTION_FILE_PATH,
)
.await?;

write_data_to_file(
wallet.client().get_protocol_parameters().await?,
PROTOCOL_PARAMETERS_FILE_PATH,
)
.await?;

Ok(())
}
Expand All @@ -75,12 +82,12 @@ async fn read_address_from_file() -> Result<Bip44Address> {
Ok(serde_json::from_str(&json)?)
}

async fn write_transaction_to_file(prepared_transaction: PreparedTransactionData) -> Result<()> {
async fn write_data_to_file(data: impl serde::Serialize, path: &str) -> Result<()> {
use tokio::io::AsyncWriteExt;

let json = serde_json::to_string_pretty(&PreparedTransactionDataDto::from(&prepared_transaction))?;
let mut file = tokio::io::BufWriter::new(tokio::fs::File::create(PREPARED_TRANSACTION_FILE_PATH).await?);
println!("example.prepared_transaction.json:\n{json}");
let json = serde_json::to_string_pretty(&data)?;
let mut file = tokio::io::BufWriter::new(tokio::fs::File::create(path).await?);
println!("{path}:\n{json}");
file.write_all(json.as_bytes()).await?;
file.flush().await?;
Ok(())
Expand Down
21 changes: 12 additions & 9 deletions sdk/examples/wallet/offline_signing/2_sign_transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
use iota_sdk::{
client::{
api::{
transaction::validate_signed_transaction_payload_length, PreparedTransactionData,
PreparedTransactionDataDto, SignedTransactionData, SignedTransactionDataDto,
transaction::validate_signed_transaction_payload_length, PreparedTransactionData, SignedTransactionData,
SignedTransactionDataDto,
},
secret::{stronghold::StrongholdSecretManager, SecretManage, SecretManager},
},
Expand All @@ -22,6 +22,7 @@ use iota_sdk::{

const STRONGHOLD_SNAPSHOT_PATH: &str = "./examples/wallet/offline_signing/example.stronghold";
const PREPARED_TRANSACTION_FILE_PATH: &str = "./examples/wallet/offline_signing/example.prepared_transaction.json";
const PROTOCOL_PARAMETERS_FILE_PATH: &str = "./examples/wallet/offline_signing/example.protocol_parameters.json";
const SIGNED_TRANSACTION_FILE_PATH: &str = "./examples/wallet/offline_signing/example.signed_transaction.json";

#[tokio::main]
Expand All @@ -39,11 +40,15 @@ async fn main() -> Result<()> {
.password(std::env::var("STRONGHOLD_PASSWORD").unwrap())
.build(STRONGHOLD_SNAPSHOT_PATH)?;

let prepared_transaction_data = read_prepared_transaction_from_file().await?;
let prepared_transaction_data = PreparedTransactionData::try_from_dto(serde_json::from_str(
&read_data_from_file(PREPARED_TRANSACTION_FILE_PATH).await?,
)?)?;

let protocol_parameters = serde_json::from_str(&read_data_from_file(PROTOCOL_PARAMETERS_FILE_PATH).await?)?;

// Signs prepared transaction offline.
let unlocks = SecretManager::Stronghold(secret_manager)
.transaction_unlocks(&prepared_transaction_data)
.transaction_unlocks(&prepared_transaction_data, &protocol_parameters)
.await?;

let signed_transaction = SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?;
Expand All @@ -62,16 +67,14 @@ async fn main() -> Result<()> {
Ok(())
}

async fn read_prepared_transaction_from_file() -> Result<PreparedTransactionData> {
async fn read_data_from_file(path: &str) -> Result<String> {
use tokio::io::AsyncReadExt;

let mut file = tokio::io::BufReader::new(tokio::fs::File::open(PREPARED_TRANSACTION_FILE_PATH).await?);
let mut file = tokio::io::BufReader::new(tokio::fs::File::open(path).await?);
let mut json = String::new();
file.read_to_string(&mut json).await?;

Ok(PreparedTransactionData::try_from_dto(serde_json::from_str::<
PreparedTransactionDataDto,
>(&json)?)?)
Ok(json)
}

async fn write_signed_transaction_to_file(signed_transaction_data: &SignedTransactionData) -> Result<()> {
Expand Down
100 changes: 59 additions & 41 deletions sdk/src/client/api/block_builder/input_selection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use crate::{
AccountOutput, ChainId, FoundryOutput, NativeTokensBuilder, NftOutput, Output, OutputId, OUTPUT_COUNT_RANGE,
},
payload::signed_transaction::TransactionCapabilities,
protocol::ProtocolParameters,
protocol::{CommittableAgeRange, ProtocolParameters},
slot::SlotIndex,
},
};
Expand Down Expand Up @@ -62,8 +62,9 @@ impl InputSelection {
fn required_account_nft_addresses(&self, input: &InputSigningData) -> Result<Option<Requirement>, Error> {
let required_address = input
.output
.required_and_unlocked_address(self.slot_index, input.output_id())?
.0;
.required_address(self.slot_index, self.protocol_parameters.committable_age_range())?
.expect("expiration unlockable outputs already filtered out");

let required_address = if let Address::Restricted(restricted) = &required_address {
restricted.address()
} else {
Expand Down Expand Up @@ -176,6 +177,7 @@ impl InputSelection {
remainder_address: None,
protocol_parameters,
// TODO may want to make this mandatory at some point
// Should be set from a commitment context input
slot_index: SlotIndex::from(0),
requirements: Vec::new(),
automatically_transitioned: HashSet::new(),
Expand Down Expand Up @@ -227,21 +229,27 @@ impl InputSelection {
// PANIC: safe to unwrap as non basic/account/foundry/nft outputs are already filtered out.
let unlock_conditions = input.output.unlock_conditions().unwrap();

if unlock_conditions.is_time_locked(self.slot_index) {
if unlock_conditions.is_timelocked(self.slot_index, self.protocol_parameters.min_committable_age()) {
return false;
}

let required_address = input
.output
// Account transition is irrelevant here as we keep accounts anyway.
.required_and_unlocked_address(self.slot_index, input.output_id())
.required_address(self.slot_index, self.protocol_parameters.committable_age_range())
// PANIC: safe to unwrap as non basic/account/foundry/nft outputs are already filtered out.
.unwrap()
.0;
let required_address = if let Address::Restricted(restricted) = &required_address {
restricted.address()
} else {
&required_address
.unwrap();

let required_address = match &required_address {
Some(address) => {
if let Address::Restricted(restricted) = address {
restricted.address()
} else {
address
}
}
// Time in which no address can unlock the output because of an expiration unlock condition
None => return false,
};

match required_address {
Expand All @@ -252,7 +260,7 @@ impl InputSelection {
.addresses
.contains(&Address::from(*implicit_account_creation.ed25519_address()))
}
_ => self.addresses.contains(&required_address),
_ => self.addresses.contains(required_address),
}
})
}
Expand All @@ -261,6 +269,7 @@ impl InputSelection {
pub(crate) fn sort_input_signing_data(
mut inputs: Vec<InputSigningData>,
slot_index: SlotIndex,
committable_age_range: CommittableAgeRange,
) -> Result<Vec<InputSigningData>, Error> {
// initially sort by output to make it deterministic
// TODO: rethink this, we only need it deterministic for tests, for the protocol it doesn't matter, also there
Expand All @@ -269,38 +278,42 @@ impl InputSelection {
// filter for ed25519 address first
let (mut sorted_inputs, account_nft_address_inputs): (Vec<InputSigningData>, Vec<InputSigningData>) =
inputs.into_iter().partition(|input_signing_data| {
let (input_address, _) = input_signing_data
let required_address = input_signing_data
.output
.required_and_unlocked_address(slot_index, input_signing_data.output_id())
// PANIC: safe to unwrap, because we filtered irrelevant outputs out before
.unwrap();
.required_address(slot_index, committable_age_range)
// PANIC: safe to unwrap as non basic/alias/foundry/nft outputs are already filtered out.
.unwrap()
.expect("expiration unlockable outputs already filtered out");

input_address.is_ed25519()
required_address.is_ed25519()
});

for input in account_nft_address_inputs {
let (input_address, _) = input
let required_address = input
.output
.required_and_unlocked_address(slot_index, input.output_id())?;
.required_address(slot_index, committable_age_range)?
.expect("expiration unlockable outputs already filtered out");

match sorted_inputs.iter().position(|input_signing_data| match input_address {
Address::Account(unlock_address) => {
if let Output::Account(account_output) = &input_signing_data.output {
*unlock_address.account_id()
== account_output.account_id_non_null(input_signing_data.output_id())
} else {
false
match sorted_inputs
.iter()
.position(|input_signing_data| match required_address {
Address::Account(unlock_address) => {
if let Output::Account(account_output) = &input_signing_data.output {
*unlock_address.account_id()
== account_output.account_id_non_null(input_signing_data.output_id())
} else {
false
}
}
}
Address::Nft(unlock_address) => {
if let Output::Nft(nft_output) = &input_signing_data.output {
*unlock_address.nft_id() == nft_output.nft_id_non_null(input_signing_data.output_id())
} else {
false
Address::Nft(unlock_address) => {
if let Output::Nft(nft_output) = &input_signing_data.output {
*unlock_address.nft_id() == nft_output.nft_id_non_null(input_signing_data.output_id())
} else {
false
}
}
}
_ => false,
}) {
_ => false,
}) {
Some(position) => {
// Insert after the output we need
sorted_inputs.insert(position + 1, input);
Expand All @@ -320,13 +333,14 @@ impl InputSelection {
if let Some(account_or_nft_address) = account_or_nft_address {
// Check for existing outputs for this address, and insert before
match sorted_inputs.iter().position(|input_signing_data| {
let (input_address, _) = input_signing_data
let required_address = input_signing_data
.output
.required_and_unlocked_address(slot_index, input.output_id())
// PANIC: safe to unwrap, because we filtered irrelevant outputs out before
.unwrap();
.required_address(slot_index, committable_age_range)
// PANIC: safe to unwrap as non basic/alias/foundry/nft outputs are already filtered
.unwrap()
.expect("expiration unlockable outputs already filtered out");

input_address == account_or_nft_address
required_address == account_or_nft_address
}) {
Some(position) => {
// Insert before the output with this address required for unlocking
Expand Down Expand Up @@ -396,7 +410,11 @@ impl InputSelection {
self.validate_transitions()?;

Ok(Selected {
inputs: Self::sort_input_signing_data(self.selected_inputs, self.slot_index)?,
inputs: Self::sort_input_signing_data(
self.selected_inputs,
self.slot_index,
self.protocol_parameters.committable_age_range(),
)?,
outputs: self.outputs,
remainder,
})
Expand Down
Loading

0 comments on commit d691cea

Please sign in to comment.