Skip to content

Commit

Permalink
feat: add expiration transaction policy (#1583)
Browse files Browse the repository at this point in the history
closes: #1577

# Release notes

Added the `expiration` transaction policy. Now the user can limit until
which block height the transaction is valid.

# Breaking Changes

- added `expiration: Option<u64>` field to `TxPolicies` struct
- `Transaction` trait method `maturity` now returns `Option<u64>`
- `Transaction` trait method `with_maturity` is removed.
  • Loading branch information
hal3e authored Feb 4, 2025
1 parent bf12588 commit f2a7f27
Show file tree
Hide file tree
Showing 11 changed files with 301 additions and 52 deletions.
5 changes: 3 additions & 2 deletions docs/src/calling-contracts/tx-policies.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ Where:
1. **Tip** - amount to pay the block producer to prioritize the transaction.
2. **Witness Limit** - The maximum amount of witness data allowed for the transaction.
3. **Maturity** - Block until which the transaction cannot be included.
4. **Max Fee** - The maximum fee payable by this transaction.
5. **Script Gas Limit** - The maximum amount of gas the transaction may consume for executing its script code.
4. **Expiration** - Block after which the transaction cannot be included.
5. **Max Fee** - The maximum fee payable by this transaction.
6. **Script Gas Limit** - The maximum amount of gas the transaction may consume for executing its script code.

When the **Script Gas Limit** is not set, the Rust SDK will estimate the consumed gas in the background and set it as the limit.

Expand Down
2 changes: 1 addition & 1 deletion docs/src/custom-transactions/transaction-builders.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ We need to do one more thing before we stop thinking about transaction inputs. E

> **Note** It is recommended to add signers before calling `adjust_for_fee()` as the estimation will include the size of the witnesses.
We can also define transaction policies. For example, we can limit the gas price by doing the following:
We can also define transaction policies. For example, we can set the maturity and expiration with:

```rust,ignore
{{#include ../../../examples/cookbook/src/lib.rs:custom_tx_policies}}
Expand Down
49 changes: 34 additions & 15 deletions e2e/tests/contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ async fn mult_call_has_same_estimated_and_used_gas() -> Result<()> {
}

#[tokio::test]
async fn contract_method_call_respects_maturity() -> Result<()> {
async fn contract_method_call_respects_maturity_and_expiration() -> Result<()> {
setup_program_test!(
Wallets("wallet"),
Abigen(Contract(
Expand All @@ -399,23 +399,42 @@ async fn contract_method_call_respects_maturity() -> Result<()> {
random_salt = false,
),
);
let provider = wallet.try_provider()?;

let call_w_maturity = |maturity| {
contract_instance
.methods()
.calling_this_will_produce_a_block()
.with_tx_policies(TxPolicies::default().with_maturity(maturity))
};
let maturity = 10;
let expiration = 20;
let call_handler = contract_instance
.methods()
.calling_this_will_produce_a_block()
.with_tx_policies(
TxPolicies::default()
.with_maturity(maturity)
.with_expiration(expiration),
);

call_w_maturity(1).call().await.expect(
"should have passed since we're calling with a maturity \
that is less or equal to the current block height",
);
{
let err = call_handler
.clone()
.call()
.await
.expect_err("maturity not reached");

call_w_maturity(3).call().await.expect_err(
"should have failed since we're calling with a maturity \
that is greater than the current block height",
);
assert!(err.to_string().contains("TransactionMaturity"));
}
{
provider.produce_blocks(15, None).await?;
call_handler
.clone()
.call()
.await
.expect("should succeed. Block height between `maturity` and `expiration`");
}
{
provider.produce_blocks(15, None).await?;
let err = call_handler.call().await.expect_err("expiration reached");

assert!(err.to_string().contains("TransactionExpiration"));
}

Ok(())
}
Expand Down
77 changes: 77 additions & 0 deletions e2e/tests/predicates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1245,3 +1245,80 @@ async fn predicate_configurables_in_blobs() -> Result<()> {

Ok(())
}

#[tokio::test]
async fn predicate_transfer_respects_maturity_and_expiration() -> Result<()> {
abigen!(Predicate(
name = "MyPredicate",
abi = "e2e/sway/predicates/basic_predicate/out/release/basic_predicate-abi.json"
));

let predicate_data = MyPredicateEncoder::default().encode_data(4097, 4097)?;

let mut predicate: Predicate =
Predicate::load_from("sway/predicates/basic_predicate/out/release/basic_predicate.bin")?
.with_data(predicate_data);

let num_coins = 4;
let num_messages = 8;
let amount = 16;
let (provider, predicate_balance, receiver, receiver_balance, asset_id, _) =
setup_predicate_test(predicate.address(), num_coins, num_messages, amount).await?;

predicate.set_provider(provider.clone());

let maturity = 10;
let expiration = 20;
let tx_policies = TxPolicies::default()
.with_maturity(maturity)
.with_expiration(expiration);
let amount_to_send = 10;

// TODO: https://github.com/FuelLabs/fuels-rs/issues/1394
let expected_fee = 1;

{
let err = predicate
.transfer(receiver.address(), amount_to_send, asset_id, tx_policies)
.await
.expect_err("maturity not reached");

assert!(err.to_string().contains("TransactionMaturity"));
}
{
provider.produce_blocks(15, None).await?;
predicate
.transfer(receiver.address(), amount_to_send, asset_id, tx_policies)
.await
.expect("should succeed. Block height between `maturity` and `expiration`");
}
{
provider.produce_blocks(15, None).await?;
let err = predicate
.transfer(receiver.address(), amount_to_send, asset_id, tx_policies)
.await
.expect_err("expiration reached");

assert!(err.to_string().contains("TransactionExpiration"));
}

// The predicate has spent the funds
assert_address_balance(
predicate.address(),
&provider,
asset_id,
predicate_balance - amount_to_send - expected_fee,
)
.await;

// Funds were transferred
assert_address_balance(
receiver.address(),
&provider,
asset_id,
receiver_balance + amount_to_send,
)
.await;

Ok(())
}
56 changes: 36 additions & 20 deletions e2e/tests/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,38 +283,51 @@ async fn can_retrieve_latest_block_time() -> Result<()> {
}

#[tokio::test]
async fn contract_deployment_respects_maturity() -> Result<()> {
async fn contract_deployment_respects_maturity_and_expiration() -> Result<()> {
abigen!(Contract(name="MyContract", abi="e2e/sway/contracts/transaction_block_height/out/release/transaction_block_height-abi.json"));

let wallets =
launch_custom_provider_and_get_wallets(WalletsConfig::default(), None, None).await?;
let wallet = &wallets[0];
let provider = wallet.try_provider()?;
let wallet = launch_provider_and_get_wallet().await?;
let provider = wallet.try_provider()?.clone();

let deploy_w_maturity = |maturity| {
let maturity = 10;
let expiration = 20;

let deploy_w_maturity_and_expiration = || {
Contract::load_from(
"sway/contracts/transaction_block_height/out/release/transaction_block_height.bin",
LoadConfiguration::default(),
)
.map(|loaded_contract| {
loaded_contract
.deploy_if_not_exists(wallet, TxPolicies::default().with_maturity(maturity))
loaded_contract.deploy(
&wallet,
TxPolicies::default()
.with_maturity(maturity)
.with_expiration(expiration),
)
})
};

let err = deploy_w_maturity(1)?.await.expect_err(
"should not deploy contract since block height `0` is less than the requested maturity `1`",
);
{
let err = deploy_w_maturity_and_expiration()?
.await
.expect_err("maturity not reached");

let Error::Provider(s) = err else {
panic!("expected `Validation`, got: `{err}`");
};
assert!(s.contains("TransactionMaturity"));
assert!(err.to_string().contains("TransactionMaturity"));
}
{
provider.produce_blocks(15, None).await?;
deploy_w_maturity_and_expiration()?
.await
.expect("should succeed. Block height between `maturity` and `expiration`");
}
{
provider.produce_blocks(15, None).await?;
let err = deploy_w_maturity_and_expiration()?
.await
.expect_err("expiration reached");

provider.produce_blocks(1, None).await?;
deploy_w_maturity(1)?
.await
.expect("Should deploy contract since maturity `1` is <= than the block height `1`");
assert!(err.to_string().contains("TransactionExpiration"));
}

Ok(())
}
Expand Down Expand Up @@ -1087,12 +1100,14 @@ async fn tx_respects_policies() -> Result<()> {
let tip = 22;
let witness_limit = 1000;
let maturity = 4;
let expiration = 128;
let max_fee = 10_000;
let script_gas_limit = 3000;
let tx_policies = TxPolicies::new(
Some(tip),
Some(witness_limit),
Some(maturity),
Some(expiration),
Some(max_fee),
Some(script_gas_limit),
);
Expand All @@ -1119,7 +1134,8 @@ async fn tx_respects_policies() -> Result<()> {
_ => panic!("expected script transaction"),
};

assert_eq!(script.maturity(), maturity as u32);
assert_eq!(script.maturity().unwrap(), maturity);
assert_eq!(script.expiration().unwrap(), expiration);
assert_eq!(script.tip().unwrap(), tip);
assert_eq!(script.witness_limit().unwrap(), witness_limit);
assert_eq!(script.max_fee().unwrap(), max_fee);
Expand Down
47 changes: 47 additions & 0 deletions e2e/tests/scripts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -682,3 +682,50 @@ async fn loader_can_be_presented_as_a_normal_script_with_shifted_configurables()

Ok(())
}

#[tokio::test]
async fn script_call_respects_maturity_and_expiration() -> Result<()> {
abigen!(Script(
name = "MyScript",
abi = "e2e/sway/scripts/basic_script/out/release/basic_script-abi.json"
));
let wallet = launch_provider_and_get_wallet().await.expect("");
let provider = wallet.try_provider()?.clone();
let bin_path = "sway/scripts/basic_script/out/release/basic_script.bin";

let script_instance = MyScript::new(wallet, bin_path);

let maturity = 10;
let expiration = 20;
let call_handler = script_instance.main(1, 2).with_tx_policies(
TxPolicies::default()
.with_maturity(maturity)
.with_expiration(expiration),
);

{
let err = call_handler
.clone()
.call()
.await
.expect_err("maturity not reached");

assert!(err.to_string().contains("TransactionMaturity"));
}
{
provider.produce_blocks(15, None).await?;
call_handler
.clone()
.call()
.await
.expect("should succeed. Block height between `maturity` and `expiration`");
}
{
provider.produce_blocks(15, None).await?;
let err = call_handler.call().await.expect_err("expiration reached");

assert!(err.to_string().contains("TransactionExpiration"));
}

Ok(())
}
Loading

0 comments on commit f2a7f27

Please sign in to comment.