From 2a67e3b50cc1a14e96c1bbd05af566df3b1c3d03 Mon Sep 17 00:00:00 2001 From: Ahmed Sagdati <37515857+segfault-magnet@users.noreply.github.com> Date: Sun, 6 Oct 2024 03:09:43 +0200 Subject: [PATCH] feat: script and predicate blobs (#1520) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes: #1519 Scripts and predicates can be pre-uploaded with blobs and then run with loader code. --------- Co-authored-by: Oleksii Filonenko <12615679+Br1ght0ne@users.noreply.github.com> Co-authored-by: hal3e Co-authored-by: Rodrigo Araújo --- .github/workflows/ci.yml | 61 ++- Cargo.toml | 26 +- docs/src/SUMMARY.md | 1 + docs/src/preuploading-code.md | 42 ++ e2e/Forc.toml | 4 + e2e/sway/predicates/predicate_blobs/Forc.toml | 5 + .../predicates/predicate_blobs/src/main.sw | 9 + .../predicate_configurables/src/main.sw | 6 +- e2e/sway/scripts/empty/Forc.toml | 7 + e2e/sway/scripts/empty/src/main.sw | 3 + e2e/sway/scripts/script_blobs/Forc.toml | 5 + e2e/sway/scripts/script_blobs/src/main.sw | 36 ++ e2e/sway/scripts/script_proxy/Forc.toml | 5 + e2e/sway/scripts/script_proxy/src/main.sw | 24 + e2e/tests/configurables.rs | 3 + e2e/tests/contracts.rs | 6 + e2e/tests/logs.rs | 44 ++ e2e/tests/predicates.rs | 188 ++++++- e2e/tests/providers.rs | 1 + e2e/tests/scripts.rs | 163 +++++- examples/contracts/src/lib.rs | 6 +- examples/cookbook/src/lib.rs | 3 +- packages/fuels-accounts/src/account.rs | 2 +- packages/fuels-accounts/src/provider.rs | 38 +- .../src/provider/retryable_client.rs | 12 +- .../provider/supported_fuel_core_version.rs | 2 +- .../abigen/bindings/script.rs | 48 +- .../src/types/wrappers/transaction.rs | 66 +-- packages/fuels-programs/src/executable.rs | 463 ++++++++++++++++++ packages/fuels-programs/src/lib.rs | 1 + packages/fuels-test-helpers/src/lib.rs | 4 + packages/fuels-test-helpers/src/service.rs | 3 +- 32 files changed, 1174 insertions(+), 113 deletions(-) create mode 100644 docs/src/preuploading-code.md create mode 100644 e2e/sway/predicates/predicate_blobs/Forc.toml create mode 100644 e2e/sway/predicates/predicate_blobs/src/main.sw create mode 100644 e2e/sway/scripts/empty/Forc.toml create mode 100644 e2e/sway/scripts/empty/src/main.sw create mode 100644 e2e/sway/scripts/script_blobs/Forc.toml create mode 100644 e2e/sway/scripts/script_blobs/src/main.sw create mode 100644 e2e/sway/scripts/script_proxy/Forc.toml create mode 100644 e2e/sway/scripts/script_proxy/src/main.sw create mode 100644 packages/fuels-programs/src/executable.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64b93f6ab4..ea9ddc1d7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,8 +16,9 @@ env: CARGO_TERM_COLOR: always DASEL_VERSION: https://github.com/TomWright/dasel/releases/download/v2.3.6/dasel_linux_amd64 RUSTFLAGS: "-D warnings" - FUEL_CORE_VERSION: 0.36.0 - FUEL_CORE_PATCH_BRANCH: + FUEL_CORE_VERSION: 0.37.0 + FUEL_CORE_PATCH_BRANCH: "" + FUEL_CORE_PATCH_REVISION: "" RUST_VERSION: 1.79.0 FORC_VERSION: 0.64.0 FORC_PATCH_BRANCH: "" @@ -99,6 +100,42 @@ jobs: echo "Comparing minimum supported toolchain ($MIN_VERSION) with ci toolchain (RUST_VERSION)" test "$MIN_VERSION" == "$RUST_VERSION" + # Fetch Fuel Core and upload as artifact, useful when we build the core from a + # revision so that we can repeat flaky tests without rebuilding the core. + fetch-fuel-core: + runs-on: ubuntu-latest + steps: + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_VERSION }} + targets: wasm32-unknown-unknown + + # selecting a toolchain either by action or manual `rustup` calls should happen + # before the cache plugin, as it uses the current rustc version as its cache key + - uses: Swatinem/rust-cache@v2.7.3 + continue-on-error: true + with: + key: "fuel-core-build" + - name: Install Fuel Core + run: | + if [[ -n $FUEL_CORE_PATCH_BRANCH ]]; then + cargo install --locked fuel-core-bin --git https://github.com/FuelLabs/fuel-core --branch "$FUEL_CORE_PATCH_BRANCH" --root fuel-core-install + elif [[ -n $FUEL_CORE_PATCH_REVISION ]]; then + cargo install --locked fuel-core-bin --git https://github.com/FuelLabs/fuel-core --rev "$FUEL_CORE_PATCH_REVISION" --root fuel-core-install + + else + curl -sSLf https://github.com/FuelLabs/fuel-core/releases/download/v${{ env.FUEL_CORE_VERSION }}/fuel-core-${{ env.FUEL_CORE_VERSION }}-x86_64-unknown-linux-gnu.tar.gz -L -o fuel-core.tar.gz + tar -xvf fuel-core.tar.gz + chmod +x fuel-core-${{ env.FUEL_CORE_VERSION }}-x86_64-unknown-linux-gnu/fuel-core + mkdir -p fuel-core-install/bin + mv fuel-core-${{ env.FUEL_CORE_VERSION }}-x86_64-unknown-linux-gnu/fuel-core fuel-core-install/bin/fuel-core + fi + + - uses: actions/upload-artifact@v4 + with: + name: fuel-core + path: fuel-core-install/bin/fuel-core + # Ensure workspace is publishable publish-crates-check: runs-on: ubuntu-latest @@ -123,6 +160,7 @@ jobs: - verify-rust-version - get-workspace-members - publish-crates-check + - fetch-fuel-core runs-on: ubuntu-latest strategy: matrix: @@ -136,11 +174,11 @@ jobs: args: --all-targets download_sway_artifacts: sway-examples - cargo_command: nextest - args: run --all-targets --features "default fuel-core-lib coin-cache" --workspace --cargo-quiet + args: run --all-targets --features "default fuel-core-lib coin-cache" --workspace --cargo-quiet --no-fail-fast download_sway_artifacts: sway-examples install_fuel_core: true - cargo_command: nextest - args: run --all-targets --workspace --cargo-quiet + args: run --all-targets --workspace --cargo-quiet --no-fail-fast download_sway_artifacts: sway-examples install_fuel_core: true - cargo_command: test @@ -175,17 +213,16 @@ jobs: with: key: "${{ matrix.cargo_command }} ${{ matrix.args }} ${{ matrix.package }}" + - name: Download Fuel Core + if: ${{ matrix.install_fuel_core }} + uses: actions/download-artifact@v4 + with: + name: fuel-core - name: Install Fuel Core if: ${{ matrix.install_fuel_core }} run: | - if [[ -n $FUEL_CORE_PATCH_BRANCH ]]; then - cargo install --locked fuel-core-bin --git https://github.com/FuelLabs/fuel-core --branch "$FUEL_CORE_PATCH_BRANCH" - else - curl -sSLf https://github.com/FuelLabs/fuel-core/releases/download/v${{ env.FUEL_CORE_VERSION }}/fuel-core-${{ env.FUEL_CORE_VERSION }}-x86_64-unknown-linux-gnu.tar.gz -L -o fuel-core.tar.gz - tar -xvf fuel-core.tar.gz - chmod +x fuel-core-${{ env.FUEL_CORE_VERSION }}-x86_64-unknown-linux-gnu/fuel-core - mv fuel-core-${{ env.FUEL_CORE_VERSION }}-x86_64-unknown-linux-gnu/fuel-core /usr/local/bin/fuel-core - fi + chmod +x fuel-core + mv fuel-core /usr/local/bin/fuel-core - name: Download sway example artifacts if: ${{ matrix.download_sway_artifacts }} diff --git a/Cargo.toml b/Cargo.toml index 0997eb9856..679c2aea45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,23 +85,23 @@ octocrab = { version = "0.39", default-features = false } dotenv = { version = "0.15", default-features = false } # Dependencies from the `fuel-core` repository: -fuel-core = { version = "0.36.0", default-features = false, features = [ +fuel-core = { version = "0.37.0", default-features = false, features = [ "wasm-executor", ] } -fuel-core-chain-config = { version = "0.36.0", default-features = false } -fuel-core-client = { version = "0.36.0", default-features = false } -fuel-core-poa = { version = "0.36.0", default-features = false } -fuel-core-services = { version = "0.36.0", default-features = false } -fuel-core-types = { version = "0.36.0", default-features = false } +fuel-core-chain-config = { version = "0.37.0", default-features = false } +fuel-core-client = { version = "0.37.0", default-features = false } +fuel-core-poa = { version = "0.37.0", default-features = false } +fuel-core-services = { version = "0.37.0", default-features = false } +fuel-core-types = { version = "0.37.0", default-features = false } # Dependencies from the `fuel-vm` repository: -fuel-asm = { version = "0.57.0" } -fuel-crypto = { version = "0.57.0" } -fuel-merkle = { version = "0.57.0" } -fuel-storage = { version = "0.57.0" } -fuel-tx = { version = "0.57.0" } -fuel-types = { version = "0.57.0" } -fuel-vm = { version = "0.57.0" } +fuel-asm = { version = "0.58.0" } +fuel-crypto = { version = "0.58.0" } +fuel-merkle = { version = "0.58.0" } +fuel-storage = { version = "0.58.0" } +fuel-tx = { version = "0.58.0" } +fuel-types = { version = "0.58.0" } +fuel-vm = { version = "0.58.0" } # Workspace projects fuels = { version = "0.66.5", path = "./packages/fuels", default-features = false } diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index f7db09f308..252949af3e 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -44,6 +44,7 @@ - [Running scripts](./running-scripts.md) - [Predicates](./predicates/index.md) - [Signatures example](./predicates/send-spend-predicate.md) +- [Pre-uploading code](./preuploading-code.md) - [Custom transactions](./custom-transactions/index.md) - [Transaction builders](./custom-transactions/transaction-builders.md) - [Custom contract and script calls](./custom-transactions/custom-calls.md) diff --git a/docs/src/preuploading-code.md b/docs/src/preuploading-code.md new file mode 100644 index 0000000000..17879ff6b1 --- /dev/null +++ b/docs/src/preuploading-code.md @@ -0,0 +1,42 @@ +# Pre-uploading code + +If you have a script or predicate that is larger than normal or which you plan +on calling often, you can pre-upload its code as a blob to the network and run a +loader script/predicate instead. The loader can be configured with the +script/predicate configurables, so you can change how the script/predicate is +configured on each run without having large transactions due to the code +duplication. + +## Scripts + +A high level pre-upload: + +```rust,ignore +{{#include ../../e2e/tests/scripts.rs:preload_high_level}} +``` + +The upload of the blob is handled inside of the `convert_into_loader` method. If you +want more fine-grained control over it, you can create the script transaction +manually: + +```rust,ignore +{{#include ../../e2e/tests/scripts.rs:preload_low_level}} +``` + +## Predicates + +You can prepare a predicate for pre-uploading without doing network requests: + +```rust,ignore +{{#include ../../e2e/tests/predicates.rs:preparing_the_predicate}} +``` + +Once you want to execute the predicate, you must beforehand upload the blob +containing its code: + +```rust,ignore +{{#include ../../e2e/tests/predicates.rs:uploading_the_blob}} +``` + +By pre-uploading the predicate code, you allow for cheaper calls to the predicate +from subsequent callers. diff --git a/e2e/Forc.toml b/e2e/Forc.toml index 29fdb899ae..530e385130 100644 --- a/e2e/Forc.toml +++ b/e2e/Forc.toml @@ -38,19 +38,23 @@ members = [ 'sway/logs/script_needs_custom_decoder_logging', 'sway/logs/script_with_contract_logs', 'sway/predicates/basic_predicate', + 'sway/predicates/predicate_blobs', 'sway/predicates/predicate_configurables', 'sway/predicates/predicate_witnesses', 'sway/predicates/signatures', 'sway/predicates/swap', 'sway/scripts/arguments', 'sway/scripts/basic_script', + 'sway/scripts/empty', 'sway/scripts/require_from_contract', 'sway/scripts/reverting', 'sway/scripts/script_array', 'sway/scripts/script_asserts', + 'sway/scripts/script_blobs', 'sway/scripts/script_configurables', 'sway/scripts/script_enum', 'sway/scripts/script_needs_custom_decoder', + 'sway/scripts/script_proxy', 'sway/scripts/script_require', 'sway/scripts/script_struct', 'sway/scripts/transfer_script', diff --git a/e2e/sway/predicates/predicate_blobs/Forc.toml b/e2e/sway/predicates/predicate_blobs/Forc.toml new file mode 100644 index 0000000000..1a4d64ffff --- /dev/null +++ b/e2e/sway/predicates/predicate_blobs/Forc.toml @@ -0,0 +1,5 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "predicate_blobs" diff --git a/e2e/sway/predicates/predicate_blobs/src/main.sw b/e2e/sway/predicates/predicate_blobs/src/main.sw new file mode 100644 index 0000000000..5fd76487a9 --- /dev/null +++ b/e2e/sway/predicates/predicate_blobs/src/main.sw @@ -0,0 +1,9 @@ +predicate; + +configurable { + SECRET_NUMBER: u64 = 9000, +} + +fn main(arg1: u8, arg2: u8) -> bool { + arg1 == 1 && arg2 == 19 && SECRET_NUMBER == 10001 +} diff --git a/e2e/sway/predicates/predicate_configurables/src/main.sw b/e2e/sway/predicates/predicate_configurables/src/main.sw index 94a3af131c..b530da2cb8 100644 --- a/e2e/sway/predicates/predicate_configurables/src/main.sw +++ b/e2e/sway/predicates/predicate_configurables/src/main.sw @@ -31,6 +31,8 @@ struct StructWithGeneric { configurable { BOOL: bool = true, U8: u8 = 8, + TUPLE: (u8, bool) = (8, true), + ARRAY: [u32; 3] = [253, 254, 255], STRUCT: StructWithGeneric = StructWithGeneric { field_1: 8, field_2: 16, @@ -41,9 +43,11 @@ configurable { fn main( switch: bool, u_8: u8, + some_tuple: (u8, bool), + some_array: [u32; 3], some_struct: StructWithGeneric, some_enum: EnumWithGeneric, ) -> bool { - switch == BOOL && u_8 == U8 && some_struct == STRUCT && some_enum == ENUM + switch == BOOL && u_8 == U8 && some_tuple.0 == TUPLE.0 && some_tuple.1 == TUPLE.1 && some_array[0] == ARRAY[0] && some_array[1] == ARRAY[1] && some_array[2] == ARRAY[2] && some_struct == STRUCT && some_enum == ENUM } // ANCHOR_END: predicate_configurables diff --git a/e2e/sway/scripts/empty/Forc.toml b/e2e/sway/scripts/empty/Forc.toml new file mode 100644 index 0000000000..e2a6f5c440 --- /dev/null +++ b/e2e/sway/scripts/empty/Forc.toml @@ -0,0 +1,7 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "empty" + +[dependencies] diff --git a/e2e/sway/scripts/empty/src/main.sw b/e2e/sway/scripts/empty/src/main.sw new file mode 100644 index 0000000000..4db3e481aa --- /dev/null +++ b/e2e/sway/scripts/empty/src/main.sw @@ -0,0 +1,3 @@ +script; + +fn main() {} diff --git a/e2e/sway/scripts/script_blobs/Forc.toml b/e2e/sway/scripts/script_blobs/Forc.toml new file mode 100644 index 0000000000..52fef11e36 --- /dev/null +++ b/e2e/sway/scripts/script_blobs/Forc.toml @@ -0,0 +1,5 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "script_blobs" diff --git a/e2e/sway/scripts/script_blobs/src/main.sw b/e2e/sway/scripts/script_blobs/src/main.sw new file mode 100644 index 0000000000..ee13dab2d7 --- /dev/null +++ b/e2e/sway/scripts/script_blobs/src/main.sw @@ -0,0 +1,36 @@ +script; + +configurable { + SECRET_NUMBER: u64 = 9000, +} + +enum MyEnum { + A: u64, + B: u8, + C: (), +} + +struct MyStruct { + field_a: MyEnum, + field_b: b256, +} + +fn main(arg1: MyStruct) -> u64 { + assert_eq(SECRET_NUMBER, 10001); + + match arg1.field_a { + MyEnum::B(value) => { + assert_eq(value, 99); + } + _ => { + assert(false) + } + } + + assert_eq( + arg1.field_b, + 0x1111111111111111111111111111111111111111111111111111111111111111, + ); + + return SECRET_NUMBER; +} diff --git a/e2e/sway/scripts/script_proxy/Forc.toml b/e2e/sway/scripts/script_proxy/Forc.toml new file mode 100644 index 0000000000..2e29ae4200 --- /dev/null +++ b/e2e/sway/scripts/script_proxy/Forc.toml @@ -0,0 +1,5 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "script_proxy" diff --git a/e2e/sway/scripts/script_proxy/src/main.sw b/e2e/sway/scripts/script_proxy/src/main.sw new file mode 100644 index 0000000000..8040159414 --- /dev/null +++ b/e2e/sway/scripts/script_proxy/src/main.sw @@ -0,0 +1,24 @@ +script; + +abi Proxy { + #[storage(write)] + fn set_target_contract(id: ContractId); + + // methods of the `huge_contract` in our e2e sway contracts + #[storage(read)] + fn something() -> u64; + + #[storage(read)] + fn write_some_u64(some: u64); + + #[storage(read)] + fn read_some_u64() -> u64; +} + +fn main(proxy_contract_id: ContractId) -> bool { + let proxy_instance = abi(Proxy, proxy_contract_id.into()); + let _ = proxy_instance.something(); + proxy_instance.write_some_u64(10001); + let read_u_64 = proxy_instance.read_some_u64(); + return read_u_64 == 10001; +} diff --git a/e2e/tests/configurables.rs b/e2e/tests/configurables.rs index 8eda436fe0..6c65221f67 100644 --- a/e2e/tests/configurables.rs +++ b/e2e/tests/configurables.rs @@ -66,6 +66,9 @@ async fn script_default_configurables() -> Result<()> { ) ); + let mut script_instance = script_instance; + script_instance.convert_into_loader().await?; + let response = script_instance.main().call().await?; let expected_value = ( diff --git a/e2e/tests/contracts.rs b/e2e/tests/contracts.rs index 409082929a..a9e3f6d14b 100644 --- a/e2e/tests/contracts.rs +++ b/e2e/tests/contracts.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use fuel_tx::{ consensus_parameters::{ConsensusParametersV1, FeeParametersV1}, ConsensusParameters, FeeParameters, @@ -1476,6 +1478,7 @@ async fn test_contract_submit_and_response() -> Result<()> { let contract_methods = contract_instance.methods(); let submitted_tx = contract_methods.get(1, 2).submit().await?; + tokio::time::sleep(Duration::from_millis(500)).await; let value = submitted_tx.response().await?.value; assert_eq!(value, 3); @@ -1489,6 +1492,7 @@ async fn test_contract_submit_and_response() -> Result<()> { .add_call(call_handler_2); let handle = multi_call_handler.submit().await?; + tokio::time::sleep(Duration::from_millis(500)).await; let (val_1, val_2): (u64, u64) = handle.response().await?.value; assert_eq!(val_1, 7); @@ -1683,6 +1687,8 @@ async fn contract_custom_call_no_signatures_strategy() -> Result<()> { // ANCHOR_END: tb_no_signatures_strategy let tx_id = provider.send_transaction(tx).await?; + tokio::time::sleep(Duration::from_millis(500)).await; + let tx_status = provider.tx_status(&tx_id).await?; let response = call_handler.get_response_from(tx_status)?; diff --git a/e2e/tests/logs.rs b/e2e/tests/logs.rs index a22dfacf08..c5f5ba41d5 100644 --- a/e2e/tests/logs.rs +++ b/e2e/tests/logs.rs @@ -1086,6 +1086,50 @@ async fn test_script_require_from_contract() -> Result<()> { Ok(()) } +#[tokio::test] +async fn test_loader_script_require_from_loader_contract() -> Result<()> { + setup_program_test!( + Wallets("wallet"), + Abigen( + Contract( + name = "MyContract", + project = "e2e/sway/contracts/lib_contract", + ), + Script( + name = "LogScript", + project = "e2e/sway/scripts/require_from_contract" + ) + ), + LoadScript( + name = "script_instance", + script = "LogScript", + wallet = "wallet" + ) + ); + + let contract_binary = "sway/contracts/lib_contract/out/release/lib_contract.bin"; + let contract = Contract::load_from(contract_binary, LoadConfiguration::default())?; + let contract_id = contract + .convert_to_loader(100_000)? + .deploy(&wallet, TxPolicies::default()) + .await?; + let contract_instance = MyContract::new(contract_id, wallet); + + let mut script_instance = script_instance; + script_instance.convert_into_loader().await?; + + let error = script_instance + .main(contract_instance.id()) + .with_contracts(&[&contract_instance]) + .call() + .await + .expect_err("should return a revert error"); + + assert_revert_containing_msg("require from contract", error); + + Ok(()) +} + fn assert_assert_eq_containing_msg(left: T, right: T, error: Error) { let msg = format!("left: `\"{left:?}\"`\n right: `\"{right:?}\"`"); assert_revert_containing_msg(&msg, error) diff --git a/e2e/tests/predicates.rs b/e2e/tests/predicates.rs index 039c05ad8b..cb99b772c3 100644 --- a/e2e/tests/predicates.rs +++ b/e2e/tests/predicates.rs @@ -6,6 +6,7 @@ use fuels::{ traits::Tokenizable, }, prelude::*, + programs::executable::Executable, types::{coin::Coin, coin_type::CoinType, input::Input, message::Message, output::Output}, }; @@ -63,13 +64,14 @@ async fn setup_predicate_test( num_coins: u64, num_messages: u64, amount: u64, -) -> Result<(Provider, u64, WalletUnlocked, u64, AssetId)> { +) -> Result<(Provider, u64, WalletUnlocked, u64, AssetId, WalletUnlocked)> { let receiver_num_coins = 1; let receiver_amount = 1; let receiver_balance = receiver_num_coins * receiver_amount; let predicate_balance = (num_coins + num_messages) * amount; let mut receiver = WalletUnlocked::new_random(None); + let mut extra_wallet = WalletUnlocked::new_random(None); let (mut coins, messages, asset_id) = get_test_coins_and_messages(predicate_address, num_coins, num_messages, amount, 0); @@ -80,6 +82,12 @@ async fn setup_predicate_test( receiver_num_coins, receiver_amount, )); + coins.extend(setup_single_asset_coins( + extra_wallet.address(), + AssetId::zeroed(), + 10000, + u64::MAX, + )); coins.extend(setup_single_asset_coins( predicate_address, @@ -90,6 +98,7 @@ async fn setup_predicate_test( let provider = setup_test_provider(coins, messages, None, None).await?; receiver.set_provider(provider.clone()); + extra_wallet.set_provider(provider.clone()); Ok(( provider, @@ -97,6 +106,7 @@ async fn setup_predicate_test( receiver, receiver_balance, asset_id, + extra_wallet, )) } @@ -158,7 +168,7 @@ async fn spend_predicate_coins_messages_basic() -> Result<()> { let num_coins = 4; let num_messages = 8; let amount = 16; - let (provider, predicate_balance, receiver, receiver_balance, asset_id) = + 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()); @@ -211,7 +221,7 @@ async fn pay_with_predicate() -> Result<()> { let num_coins = 4; let num_messages = 8; let amount = 16; - let (provider, _predicate_balance, _receiver, _receiver_balance, _asset_id) = + 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()); @@ -280,7 +290,7 @@ async fn pay_with_predicate_vector_data() -> Result<()> { let num_coins = 4; let num_messages = 8; let amount = 16; - let (provider, _predicate_balance, _receiver, _receiver_balance, _asset_id) = + 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()); @@ -342,7 +352,7 @@ async fn predicate_contract_transfer() -> Result<()> { let num_coins = 4; let num_messages = 8; let amount = 300; - let (provider, _predicate_balance, _receiver, _receiver_balance, _asset_id) = + 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()); @@ -398,7 +408,7 @@ async fn predicate_transfer_to_base_layer() -> Result<()> { let num_coins = 4; let num_messages = 8; let amount = 300; - let (provider, _predicate_balance, _receiver, _receiver_balance, _asset_id) = + 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()); @@ -531,7 +541,7 @@ async fn contract_tx_and_call_params_with_predicate() -> Result<()> { let num_coins = 1; let num_messages = 1; let amount = 1000; - let (provider, _predicate_balance, _receiver, _receiver_balance, _asset_id) = + 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()); @@ -611,7 +621,7 @@ async fn diff_asset_predicate_payment() -> Result<()> { let num_coins = 1; let num_messages = 1; let amount = 1_000_000_000; - let (provider, _predicate_balance, _receiver, _receiver_balance, _asset_id) = + 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()); @@ -651,8 +661,14 @@ async fn predicate_default_configurables() -> Result<()> { }; let new_enum = EnumWithGeneric::VariantOne(true); - let predicate_data = - MyPredicateEncoder::default().encode_data(true, 8, new_struct, new_enum)?; + let predicate_data = MyPredicateEncoder::default().encode_data( + true, + 8, + (8, true), + [253, 254, 255], + new_struct, + new_enum, + )?; let mut predicate: Predicate = Predicate::load_from( "sway/predicates/predicate_configurables/out/release/predicate_configurables.bin", @@ -662,7 +678,7 @@ async fn predicate_default_configurables() -> Result<()> { let num_coins = 4; let num_messages = 8; let amount = 16; - let (provider, predicate_balance, receiver, receiver_balance, asset_id) = + 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()); @@ -701,6 +717,8 @@ async fn predicate_configurables() -> Result<()> { abi = "e2e/sway/predicates/predicate_configurables/out/release/predicate_configurables-abi.json" )); + let new_tuple = (16, false); + let new_array = [123, 124, 125]; let new_struct = StructWithGeneric { field_1: 32u8, field_2: 64, @@ -709,11 +727,13 @@ async fn predicate_configurables() -> Result<()> { let configurables = MyPredicateConfigurables::default() .with_U8(8)? + .with_TUPLE(new_tuple)? + .with_ARRAY(new_array)? .with_STRUCT(new_struct.clone())? .with_ENUM(new_enum.clone())?; - let predicate_data = - MyPredicateEncoder::default().encode_data(true, 8u8, new_struct, new_enum)?; + let predicate_data = MyPredicateEncoder::default() + .encode_data(true, 8u8, new_tuple, new_array, new_struct, new_enum)?; let mut predicate: Predicate = Predicate::load_from( "sway/predicates/predicate_configurables/out/release/predicate_configurables.bin", @@ -725,7 +745,7 @@ async fn predicate_configurables() -> Result<()> { let num_coins = 4; let num_messages = 8; let amount = 16; - let (provider, predicate_balance, receiver, receiver_balance, asset_id) = + 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()); @@ -874,7 +894,7 @@ async fn predicate_can_access_manually_added_witnesses() -> Result<()> { let num_coins = 4; let num_messages = 0; let amount = 16; - let (provider, predicate_balance, receiver, receiver_balance, asset_id) = + 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()); @@ -942,7 +962,7 @@ async fn tx_id_not_changed_after_adding_witnesses() -> Result<()> { let num_coins = 4; let num_messages = 0; let amount = 16; - let (provider, _predicate_balance, receiver, _receiver_balance, asset_id) = + 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()); @@ -1022,7 +1042,7 @@ async fn predicate_transfers_non_base_asset() -> Result<()> { let num_coins = 4; let num_message = 6; let amount = 20; - let (provider, _, receiver, _, _) = + let (provider, _, receiver, _, _, _) = setup_predicate_test(predicate.address(), num_coins, num_message, amount).await?; predicate.set_provider(provider); let other_asset_id = AssetId::from([1u8; 32]); @@ -1062,7 +1082,7 @@ async fn predicate_with_invalid_data_fails() -> Result<()> { let num_coins = 4; let num_message = 6; let amount = 20; - let (provider, _, receiver, _, _) = + let (provider, _, receiver, _, _, _) = setup_predicate_test(predicate.address(), num_coins, num_message, amount).await?; predicate.set_provider(provider); let other_asset_id = AssetId::from([1u8; 32]); @@ -1084,3 +1104,135 @@ async fn predicate_with_invalid_data_fails() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn predicate_blobs() -> Result<()> { + abigen!(Predicate( + name = "MyPredicate", + abi = "e2e/sway/predicates/predicate_blobs/out/release/predicate_blobs-abi.json" + )); + + // ANCHOR: preparing_the_predicate + let configurables = MyPredicateConfigurables::default().with_SECRET_NUMBER(10001)?; + + let predicate_data = MyPredicateEncoder::default().encode_data(1, 19)?; + + let executable = + Executable::load_from("sway/predicates/predicate_blobs/out/release/predicate_blobs.bin")?; + + let loader = executable + .convert_to_loader()? + .with_configurables(configurables); + + let mut predicate: Predicate = Predicate::from_code(loader.code()).with_data(predicate_data); + // ANCHOR_END: preparing_the_predicate + + let num_coins = 4; + let num_messages = 8; + let amount = 16; + let (provider, predicate_balance, receiver, receiver_balance, asset_id, extra_wallet) = + setup_predicate_test(predicate.address(), num_coins, num_messages, amount).await?; + + // we don't want to pay with the recipient wallet so that we don't affect the assertion we're + // gonna make later on + // ANCHOR: uploading_the_blob + loader.upload_blob(extra_wallet).await?; + + predicate.set_provider(provider.clone()); + + let expected_fee = 1; + predicate + .transfer( + receiver.address(), + predicate_balance - expected_fee, + asset_id, + TxPolicies::default(), + ) + .await?; + // ANCHOR_END: uploading_the_blob + + // The predicate has spent the funds + assert_address_balance(predicate.address(), &provider, asset_id, 0).await; + + // Funds were transferred + assert_address_balance( + receiver.address(), + &provider, + asset_id, + receiver_balance + predicate_balance - expected_fee, + ) + .await; + + Ok(()) +} + +#[tokio::test] +async fn predicate_configurables_in_blobs() -> Result<()> { + abigen!(Predicate( + name = "MyPredicate", + abi = "e2e/sway/predicates/predicate_configurables/out/release/predicate_configurables-abi.json" + )); + + let new_tuple = (16, false); + let new_array = [123, 124, 125]; + let new_struct = StructWithGeneric { + field_1: 32u8, + field_2: 64, + }; + let new_enum = EnumWithGeneric::VariantTwo; + + let configurables = MyPredicateConfigurables::default() + .with_U8(8)? + .with_TUPLE(new_tuple)? + .with_ARRAY(new_array)? + .with_STRUCT(new_struct.clone())? + .with_ENUM(new_enum.clone())?; + + let predicate_data = MyPredicateEncoder::default() + .encode_data(true, 8u8, new_tuple, new_array, new_struct, new_enum)?; + + let executable = Executable::load_from( + "sway/predicates/predicate_configurables/out/release/predicate_configurables.bin", + )?; + + let loader = executable + .convert_to_loader()? + .with_configurables(configurables); + + let mut predicate: Predicate = Predicate::from_code(loader.code()).with_data(predicate_data); + + let num_coins = 4; + let num_messages = 8; + let amount = 16; + let (provider, predicate_balance, receiver, receiver_balance, asset_id, extra_wallet) = + setup_predicate_test(predicate.address(), num_coins, num_messages, amount).await?; + + predicate.set_provider(provider.clone()); + + loader.upload_blob(extra_wallet).await?; + + // TODO: https://github.com/FuelLabs/fuels-rs/issues/1394 + let expected_fee = 1; + predicate + .transfer( + receiver.address(), + predicate_balance - expected_fee, + asset_id, + TxPolicies::default(), + ) + .await?; + + // The predicate has spent the funds + assert_address_balance(predicate.address(), &provider, asset_id, 0).await; + + // Funds were transferred + assert_address_balance( + receiver.address(), + &provider, + asset_id, + receiver_balance + predicate_balance - expected_fee, + ) + .await; + + Ok(()) +} diff --git a/e2e/tests/providers.rs b/e2e/tests/providers.rs index accba3e317..51294639d7 100644 --- a/e2e/tests/providers.rs +++ b/e2e/tests/providers.rs @@ -960,6 +960,7 @@ async fn can_produce_blocks_with_trig_never() -> Result<()> { provider.send_transaction(tx).await?; provider.produce_blocks(1, None).await?; + tokio::time::sleep(std::time::Duration::from_millis(500)).await; let status = provider.tx_status(&tx_id).await?; assert!(matches!(status, TxStatus::Success { .. })); diff --git a/e2e/tests/scripts.rs b/e2e/tests/scripts.rs index 2ab872ccb6..8ea5b4a8af 100644 --- a/e2e/tests/scripts.rs +++ b/e2e/tests/scripts.rs @@ -1,7 +1,13 @@ +use std::time::Duration; + use fuels::{ - core::codec::{DecoderConfig, EncoderConfig}, + core::{ + codec::{DecoderConfig, EncoderConfig}, + traits::Tokenizable, + }, prelude::*, - types::Identity, + programs::executable::Executable, + types::{Bits256, Identity}, }; #[tokio::test] @@ -275,6 +281,7 @@ async fn test_script_submit_and_response() -> Result<()> { // ANCHOR: submit_response_script let submitted_tx = script_instance.main(my_struct).submit().await?; + tokio::time::sleep(Duration::from_millis(500)).await; let value = submitted_tx.response().await?.value; // ANCHOR_END: submit_response_script @@ -311,6 +318,7 @@ async fn test_script_transaction_builder() -> Result<()> { let tx = tb.build(provider).await?; let tx_id = provider.send_transaction(tx).await?; + tokio::time::sleep(Duration::from_millis(500)).await; let tx_status = provider.tx_status(&tx_id).await?; let response = script_call_handler.get_response_from(tx_status)?; @@ -397,3 +405,154 @@ async fn simulations_can_be_made_without_coins() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn can_be_run_in_blobs_builder() -> Result<()> { + abigen!(Script( + abi = "e2e/sway/scripts/script_blobs/out/release/script_blobs-abi.json", + name = "MyScript" + )); + + let binary_path = "./sway/scripts/script_blobs/out/release/script_blobs.bin"; + let wallet = launch_provider_and_get_wallet().await?; + let provider = wallet.try_provider()?.clone(); + + // ANCHOR: preload_low_level + let regular = Executable::load_from(binary_path)?; + + let configurables = MyScriptConfigurables::default().with_SECRET_NUMBER(10001)?; + let loader = regular + .convert_to_loader()? + .with_configurables(configurables); + + // The Blob must be uploaded manually, otherwise the script code will revert. + loader.upload_blob(wallet.clone()).await?; + + let encoder = fuels::core::codec::ABIEncoder::default(); + let token = MyStruct { + field_a: MyEnum::B(99), + field_b: Bits256([17; 32]), + } + .into_token(); + let data = encoder.encode(&[token])?; + + let mut tb = ScriptTransactionBuilder::default() + .with_script(loader.code()) + .with_script_data(data); + + wallet.adjust_for_fee(&mut tb, 0).await?; + + wallet.add_witnesses(&mut tb)?; + + let tx = tb.build(&provider).await?; + + let response = provider.send_transaction_and_await_commit(tx).await?; + + response.check(None)?; + // ANCHOR_END: preload_low_level + + Ok(()) +} + +#[tokio::test] +async fn can_be_run_in_blobs_high_level() -> Result<()> { + setup_program_test!( + Abigen(Script( + project = "e2e/sway/scripts/script_blobs", + name = "MyScript" + )), + Wallets("wallet"), + LoadScript(name = "my_script", script = "MyScript", wallet = "wallet") + ); + + let configurables = MyScriptConfigurables::default().with_SECRET_NUMBER(10001)?; + let mut my_script = my_script.with_configurables(configurables); + + let arg = MyStruct { + field_a: MyEnum::B(99), + field_b: Bits256([17; 32]), + }; + let secret = my_script + .convert_into_loader() + .await? + .main(arg) + .call() + .await? + .value; + + assert_eq!(secret, 10001); + + Ok(()) +} + +#[tokio::test] +async fn no_data_section_blob_run() -> Result<()> { + setup_program_test!( + Abigen(Script( + project = "e2e/sway/scripts/empty", + name = "MyScript" + )), + Wallets("wallet"), + LoadScript(name = "my_script", script = "MyScript", wallet = "wallet") + ); + + let mut my_script = my_script; + + // ANCHOR: preload_high_level + my_script.convert_into_loader().await?.main().call().await?; + // ANCHOR_END: preload_high_level + + Ok(()) +} + +#[tokio::test] +async fn loader_script_calling_loader_proxy() -> Result<()> { + setup_program_test!( + Abigen( + Contract( + name = "MyContract", + project = "e2e/sway/contracts/huge_contract" + ), + Contract(name = "MyProxy", project = "e2e/sway/contracts/proxy"), + Script(name = "MyScript", project = "e2e/sway/scripts/script_proxy"), + ), + Wallets("wallet"), + LoadScript(name = "my_script", script = "MyScript", wallet = "wallet") + ); + + let contract_binary = "sway/contracts/huge_contract/out/release/huge_contract.bin"; + + let contract = Contract::load_from(contract_binary, LoadConfiguration::default())?; + + let contract_id = contract + .convert_to_loader(100)? + .deploy(&wallet, TxPolicies::default()) + .await?; + + let contract_binary = "sway/contracts/proxy/out/release/proxy.bin"; + + let proxy_id = Contract::load_from(contract_binary, LoadConfiguration::default())? + .convert_to_loader(100)? + .deploy(&wallet, TxPolicies::default()) + .await?; + + let proxy = MyProxy::new(proxy_id.clone(), wallet.clone()); + proxy + .methods() + .set_target_contract(contract_id.clone()) + .call() + .await?; + + let mut my_script = my_script; + let result = my_script + .convert_into_loader() + .await? + .main(proxy_id.clone()) + .with_contract_ids(&[contract_id, proxy_id]) + .call() + .await?; + + assert!(result.value); + + Ok(()) +} diff --git a/examples/contracts/src/lib.rs b/examples/contracts/src/lib.rs index 0d70b7d9f7..87c3330508 100644 --- a/examples/contracts/src/lib.rs +++ b/examples/contracts/src/lib.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod tests { - use std::collections::HashSet; + use std::{collections::HashSet, time::Duration}; use fuels::{ core::codec::{encode_fn_selector, DecoderConfig, EncoderConfig}, @@ -211,6 +211,7 @@ mod tests { .submit() .await?; + tokio::time::sleep(Duration::from_millis(500)).await; let value = response.response().await?.value; // ANCHOR_END: submit_response_contract @@ -585,6 +586,7 @@ mod tests { let multi_call_handler = multi_call_handler_tmp.clone(); // ANCHOR: submit_response_multicontract let submitted_tx = multi_call_handler.submit().await?; + tokio::time::sleep(Duration::from_millis(500)).await; let (counter, array): (u64, [u64; 2]) = submitted_tx.response().await?.value; // ANCHOR_END: submit_response_multicontract @@ -886,6 +888,8 @@ mod tests { let tx = tb.build(provider).await?; let tx_id = provider.send_transaction(tx).await?; + tokio::time::sleep(Duration::from_millis(500)).await; + let tx_status = provider.tx_status(&tx_id).await?; let response = call_handler.get_response_from(tx_status)?; diff --git a/examples/cookbook/src/lib.rs b/examples/cookbook/src/lib.rs index 8a153214c4..88c8a0baeb 100644 --- a/examples/cookbook/src/lib.rs +++ b/examples/cookbook/src/lib.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod tests { - use std::str::FromStr; + use std::{str::FromStr, time::Duration}; use fuels::{ accounts::{predicate::Predicate, wallet::WalletUnlocked, Account, ViewOnlyAccount}, @@ -318,6 +318,7 @@ mod tests { let tx_id = provider.send_transaction(tx).await?; // ANCHOR_END: custom_tx_build + tokio::time::sleep(Duration::from_millis(500)).await; // ANCHOR: custom_tx_verify let status = provider.tx_status(&tx_id).await?; assert!(matches!(status, TxStatus::Success { .. })); diff --git a/packages/fuels-accounts/src/account.rs b/packages/fuels-accounts/src/account.rs index 8fb7c31d62..7801e49730 100644 --- a/packages/fuels-accounts/src/account.rs +++ b/packages/fuels-accounts/src/account.rs @@ -417,7 +417,7 @@ mod tests { tb.add_signer(wallet.clone())?; // ANCHOR_END: sign_tb - let tx = tb.build(&MockDryRunner::default()).await?; // Resolve signatures and add corresponding witness indexes + let tx = tb.build(MockDryRunner::default()).await?; // Resolve signatures and add corresponding witness indexes // Extract the signature from the tx witnesses let bytes = <[u8; Signature::LEN]>::try_from(tx.witnesses().first().unwrap().as_ref())?; diff --git a/packages/fuels-accounts/src/provider.rs b/packages/fuels-accounts/src/provider.rs index 314c9d2b71..9babc5f304 100644 --- a/packages/fuels-accounts/src/provider.rs +++ b/packages/fuels-accounts/src/provider.rs @@ -17,10 +17,7 @@ use fuel_core_client::client::{ gas_price::{EstimateGasPrice, LatestGasPrice}, }, }; -use fuel_core_types::{ - blockchain::header::LATEST_STATE_TRANSITION_VERSION, - services::executor::TransactionExecutionResult, -}; +use fuel_core_types::services::executor::TransactionExecutionResult; use fuel_tx::{ AssetId, ConsensusParameters, Receipt, Transaction as FuelTransaction, TxId, UtxoId, }; @@ -40,6 +37,7 @@ use fuels_core::{ message_proof::MessageProof, node_info::NodeInfo, transaction::{Transaction, Transactions}, + transaction_builders::{Blob, BlobId}, transaction_response::TransactionResponse, tx_status::TxStatus, DryRun, DryRunner, @@ -150,6 +148,18 @@ impl Provider { self.client.url() } + pub async fn blob(&self, blob_id: BlobId) -> Result> { + Ok(self + .client + .blob(blob_id.into()) + .await? + .map(|blob| Blob::new(blob.bytecode))) + } + + pub async fn blob_exists(&self, blob_id: BlobId) -> Result { + Ok(self.client.blob_exists(blob_id.into()).await?) + } + /// Sends a transaction to the underlying Provider's client. pub async fn send_transaction_and_await_commit( &self, @@ -753,22 +763,10 @@ impl DryRunner for Provider { async fn maybe_estimate_predicates( &self, tx: &FuelTransaction, - latest_chain_executor_version: Option, + _latest_chain_executor_version: Option, ) -> Result> { - let latest_chain_executor_version = match latest_chain_executor_version { - Some(exec_version) => exec_version, - None => { - self.chain_info() - .await? - .latest_block - .header - .state_transition_bytecode_version - } - }; - - Ok( - (latest_chain_executor_version > LATEST_STATE_TRANSITION_VERSION) - .then_some(self.client.estimate_predicates(tx).await?), - ) + // We always delegate the estimation to the client because estimating locally is no longer + // possible due to the need of blob storage + Ok(Some(self.client.estimate_predicates(tx).await?)) } } diff --git a/packages/fuels-accounts/src/provider/retryable_client.rs b/packages/fuels-accounts/src/provider/retryable_client.rs index 7c65acd0a8..510b4d8b65 100644 --- a/packages/fuels-accounts/src/provider/retryable_client.rs +++ b/packages/fuels-accounts/src/provider/retryable_client.rs @@ -5,13 +5,13 @@ use fuel_core_client::client::{ types::{ gas_price::{EstimateGasPrice, LatestGasPrice}, primitives::{BlockId, TransactionId}, - Balance, Block, ChainInfo, Coin, CoinType, ContractBalance, Message, MessageProof, + Balance, Blob, Block, ChainInfo, Coin, CoinType, ContractBalance, Message, MessageProof, NodeInfo, TransactionResponse, TransactionStatus, }, FuelClient, }; use fuel_core_types::services::executor::TransactionExecutionStatus; -use fuel_tx::{Transaction, TxId, UtxoId}; +use fuel_tx::{BlobId, Transaction, TxId, UtxoId}; use fuel_types::{Address, AssetId, BlockHeight, ContractId, Nonce}; use fuels_core::types::errors::{error, Error, Result}; @@ -140,6 +140,14 @@ impl RetryableClient { self.wrap(|| self.client.node_info()).await } + pub async fn blob(&self, blob_id: BlobId) -> RequestResult> { + self.wrap(|| self.client.blob(blob_id)).await + } + + pub async fn blob_exists(&self, blob_id: BlobId) -> RequestResult { + self.wrap(|| self.client.blob_exists(blob_id)).await + } + pub async fn latest_gas_price(&self) -> RequestResult { self.wrap(|| self.client.latest_gas_price()).await } diff --git a/packages/fuels-accounts/src/provider/supported_fuel_core_version.rs b/packages/fuels-accounts/src/provider/supported_fuel_core_version.rs index e787920732..494def0da1 100644 --- a/packages/fuels-accounts/src/provider/supported_fuel_core_version.rs +++ b/packages/fuels-accounts/src/provider/supported_fuel_core_version.rs @@ -1 +1 @@ -pub const SUPPORTED_FUEL_CORE_VERSION: semver::Version = semver::Version::new(0, 36, 0); +pub const SUPPORTED_FUEL_CORE_VERSION: semver::Version = semver::Version::new(0, 37, 0); diff --git a/packages/fuels-code-gen/src/program_bindings/abigen/bindings/script.rs b/packages/fuels-code-gen/src/program_bindings/abigen/bindings/script.rs index d2c5c24474..21092e7232 100644 --- a/packages/fuels-code-gen/src/program_bindings/abigen/bindings/script.rs +++ b/packages/fuels-code-gen/src/program_bindings/abigen/bindings/script.rs @@ -42,7 +42,9 @@ pub(crate) fn script_bindings( #[derive(Debug,Clone)] pub struct #name{ account: A, - binary: ::std::vec::Vec, + unconfigured_binary: ::std::vec::Vec, + configurables: ::fuels::core::Configurables, + converted_into_loader: bool, log_decoder: ::fuels::core::codec::LogDecoder, encoder_config: ::fuels::core::codec::EncoderConfig, } @@ -54,7 +56,9 @@ pub(crate) fn script_bindings( .expect(&format!("could not read script binary {binary_filepath:?}")); Self { account, - binary, + unconfigured_binary: binary, + configurables: ::core::default::Default::default(), + converted_into_loader: false, log_decoder: ::fuels::core::codec::LogDecoder::new(#log_formatters_lookup), encoder_config: ::fuels::core::codec::EncoderConfig::default(), } @@ -63,20 +67,32 @@ pub(crate) fn script_bindings( pub fn with_account(self, account: U) -> #name { #name { account, - binary: self.binary, + unconfigured_binary: self.unconfigured_binary, log_decoder: self.log_decoder, encoder_config: self.encoder_config, + configurables: self.configurables, + converted_into_loader: self.converted_into_loader, } } pub fn with_configurables(mut self, configurables: impl Into<::fuels::core::Configurables>) -> Self { - let configurables: ::fuels::core::Configurables = configurables.into(); - configurables.update_constants_in(&mut self.binary); + self.configurables = configurables.into(); self } + pub fn code(&self) -> ::std::vec::Vec { + let regular = ::fuels::programs::executable::Executable::from_bytes(self.unconfigured_binary.clone()).with_configurables(self.configurables.clone()); + + if self.converted_into_loader { + let loader = regular.convert_to_loader().expect("cannot fail since we already converted to the loader successfully"); + loader.code() + } else { + regular.code() + } + } + pub fn with_encoder_config(mut self, encoder_config: ::fuels::core::codec::EncoderConfig) -> Self { @@ -89,6 +105,23 @@ pub(crate) fn script_bindings( self.log_decoder.clone() } + /// Will upload the script code as a blob to the network and change the script code + /// into a loader that will fetch the blob and load it into memory before executing the + /// code inside. Allows you to optimize fees by paying for most of the code once and + /// then just running a small loader. + pub async fn convert_into_loader(&mut self) -> ::fuels::types::errors::Result<&mut Self> { + if !self.converted_into_loader { + let regular = ::fuels::programs::executable::Executable::from_bytes(self.unconfigured_binary.clone()).with_configurables(self.configurables.clone()); + let loader = regular.convert_to_loader()?; + + loader.upload_blob(self.account.clone()).await?; + + self.converted_into_loader = true; + } + ::fuels::types::errors::Result::Ok(self) + + } + #main_function } @@ -111,8 +144,9 @@ fn expand_fn(fn_abi: &FullABIFunction) -> Result { let original_output_type = generator.output_type(); let body = quote! { let encoded_args = ::fuels::core::codec::ABIEncoder::new(self.encoder_config).encode(&#arg_tokens); + ::fuels::programs::calls::CallHandler::new_script_call( - self.binary.clone(), + self.code(), encoded_args, self.account.clone(), self.log_decoder.clone() @@ -191,7 +225,7 @@ mod tests { let encoded_args=::fuels::core::codec::ABIEncoder::new(self.encoder_config) .encode(&[::fuels::core::traits::Tokenizable::into_token(bimbam)]); ::fuels::programs::calls::CallHandler::new_script_call( - self.binary.clone(), + self.code(), encoded_args, self.account.clone(), self.log_decoder.clone() diff --git a/packages/fuels-core/src/types/wrappers/transaction.rs b/packages/fuels-core/src/types/wrappers/transaction.rs index add8048ee4..a4590982f8 100644 --- a/packages/fuels-core/src/types/wrappers/transaction.rs +++ b/packages/fuels-core/src/types/wrappers/transaction.rs @@ -19,9 +19,6 @@ use fuel_tx::{ TransactionFee, UniqueIdentifier, Upgrade, Upload, Witness, }; use fuel_types::{bytes::padded_len_usize, AssetId, ChainId}; -use fuel_vm::checked_transaction::{ - CheckPredicateParams, CheckPredicates, EstimatePredicates, IntoChecked, -}; use itertools::Itertools; use crate::{ @@ -179,7 +176,6 @@ impl TxPolicies { } use fuel_tx::field::{BytecodeWitnessIndex, Salt, StorageSlots}; -use fuel_vm::prelude::MemoryInstance; use crate::types::coin_type_id::CoinTypeId; @@ -380,16 +376,10 @@ macro_rules! impl_tx_wrapper { impl ValidatablePredicates for $wrapper { fn validate_predicates( self, - consensus_parameters: &ConsensusParameters, - block_height: u32, + _consensus_parameters: &ConsensusParameters, + _block_height: u32, ) -> Result<()> { - let checked = self - .tx - .into_checked(block_height.into(), consensus_parameters)?; - - let check_predicates_parameters: CheckPredicateParams = consensus_parameters.into(); - - checked.check_predicates(&check_predicates_parameters, MemoryInstance::new())?; + // Can no longer validate predicates locally due to the need for blob storage Ok(()) } @@ -564,10 +554,12 @@ impl EstimablePredicates for UploadTransaction { { tx.as_upload().expect("is upload").clone_into(&mut self.tx); } else { - self.tx.estimate_predicates( - &provider.consensus_parameters().into(), - MemoryInstance::new(), - )?; + // We no longer estimate locally since we don't have the blob storage. + // maybe_estimate_predicates should always return an estimation + return Err(error!( + Other, + "Should have been given an estimation from the node. This is a bug." + )); } Ok(()) @@ -590,10 +582,12 @@ impl EstimablePredicates for UpgradeTransaction { .expect("is upgrade") .clone_into(&mut self.tx); } else { - self.tx.estimate_predicates( - &provider.consensus_parameters().into(), - MemoryInstance::new(), - )?; + // We no longer estimate locally since we don't have the blob storage. + // maybe_estimate_predicates should always return an estimation + return Err(error!( + Other, + "Should have been given an estimation from the node. This is a bug." + )); } Ok(()) @@ -614,10 +608,12 @@ impl EstimablePredicates for CreateTransaction { { tx.as_create().expect("is create").clone_into(&mut self.tx); } else { - self.tx.estimate_predicates( - &provider.consensus_parameters().into(), - MemoryInstance::new(), - )?; + // We no longer estimate locally since we don't have the blob storage. + // maybe_estimate_predicates should always return an estimation + return Err(error!( + Other, + "Should have been given an estimation from the node. This is a bug." + )); } Ok(()) @@ -652,10 +648,12 @@ impl EstimablePredicates for ScriptTransaction { { tx.as_script().expect("is script").clone_into(&mut self.tx); } else { - self.tx.estimate_predicates( - &provider.consensus_parameters().into(), - MemoryInstance::new(), - )?; + // We no longer estimate locally since we don't have the blob storage. + // maybe_estimate_predicates should always return an estimation + return Err(error!( + Other, + "Should have been given an estimation from the node. This is a bug." + )); } Ok(()) @@ -676,10 +674,12 @@ impl EstimablePredicates for BlobTransaction { { tx.as_blob().expect("is blob").clone_into(&mut self.tx); } else { - self.tx.estimate_predicates( - &provider.consensus_parameters().into(), - MemoryInstance::new(), - )?; + // We no longer estimate locally since we don't have the blob storage. + // maybe_estimate_predicates should always return an estimation + return Err(error!( + Other, + "Should have been given an estimation from the node. This is a bug." + )); } Ok(()) diff --git a/packages/fuels-programs/src/executable.rs b/packages/fuels-programs/src/executable.rs new file mode 100644 index 0000000000..201ca70f52 --- /dev/null +++ b/packages/fuels-programs/src/executable.rs @@ -0,0 +1,463 @@ +use fuel_asm::{op, Instruction, RegId}; +use fuels_core::{ + constants::WORD_SIZE, + types::{ + errors::Result, + transaction_builders::{Blob, BlobId, BlobTransactionBuilder}, + }, + Configurables, +}; + +/// This struct represents a standard executable with its associated bytecode and configurables. +#[derive(Debug, Clone, PartialEq)] +pub struct Regular { + code: Vec, + configurables: Configurables, +} + +impl Regular { + pub fn new(code: Vec, configurables: Configurables) -> Self { + Self { + code, + configurables, + } + } +} + +/// Used to transform Script or Predicate code into a loader variant, where the code is uploaded as +/// a blob and the binary itself is substituted with code that will load the blob code and apply +/// the given configurables to the Script/Predicate. +pub struct Executable { + state: State, +} + +impl Executable { + pub fn from_bytes(code: Vec) -> Self { + Executable { + state: Regular::new(code, Default::default()), + } + } + + /// Loads an `Executable` from a file at the given path. + /// + /// # Parameters + /// + /// - `path`: The file path to load the executable from. + /// + /// # Returns + /// + /// A `Result` containing the `Executable` or an error if loading fails. + pub fn load_from(path: &str) -> Result> { + let code = std::fs::read(path)?; + + Ok(Executable { + state: Regular::new(code, Default::default()), + }) + } + + pub fn with_configurables(self, configurables: impl Into) -> Self { + Executable { + state: Regular { + configurables: configurables.into(), + ..self.state + }, + } + } + + /// Returns the code of the executable with configurables applied. + /// + /// # Returns + /// + /// The bytecode of the executable with configurables updated. + pub fn code(&self) -> Vec { + let mut code = self.state.code.clone(); + self.state.configurables.update_constants_in(&mut code); + code + } + + /// Converts the `Executable` into an `Executable`. + /// + /// # Returns + /// + /// A `Result` containing the `Executable` or an error if loader code cannot be + /// generated for the given binary. + pub fn convert_to_loader(self) -> Result> { + validate_loader_can_be_made_from_code( + self.state.code.clone(), + self.state.configurables.clone(), + )?; + + Ok(Executable { + state: Loader { + code: self.state.code, + configurables: self.state.configurables, + }, + }) + } +} + +pub struct Loader { + code: Vec, + configurables: Configurables, +} + +impl Executable { + pub fn with_configurables(self, configurables: impl Into) -> Self { + Executable { + state: Loader { + configurables: configurables.into(), + ..self.state + }, + } + } + + /// Returns the code of the loader executable with configurables applied. + pub fn code(&self) -> Vec { + let mut code = self.state.code.clone(); + + self.state.configurables.update_constants_in(&mut code); + + let blob_id = self.blob().id(); + + transform_into_configurable_loader(code, &blob_id) + .expect("checked before turning into a Executable") + } + + /// A Blob containing the original executable code minus the data section. + pub fn blob(&self) -> Blob { + let data_section_offset = extract_data_offset(&self.state.code) + .expect("checked before turning into a Executable"); + + let code_without_data_section = self.state.code[..data_section_offset].to_vec(); + + Blob::new(code_without_data_section) + } + + /// Uploads a blob containing the original executable code minus the data section. + pub async fn upload_blob(&self, account: impl fuels_accounts::Account) -> Result<()> { + let blob = self.blob(); + let provider = account.try_provider()?; + + if provider.blob_exists(blob.id()).await? { + return Ok(()); + } + + let mut tb = BlobTransactionBuilder::default().with_blob(self.blob()); + + account.adjust_for_fee(&mut tb, 0).await?; + + account.add_witnesses(&mut tb)?; + + let tx = tb.build(provider).await?; + + provider + .send_transaction_and_await_commit(tx) + .await? + .check(None)?; + + Ok(()) + } +} + +fn extract_data_offset(binary: &[u8]) -> Result { + if binary.len() < 16 { + return Err(fuels_core::error!( + Other, + "given binary is too short to contain a data offset, len: {}", + binary.len() + )); + } + + let data_offset: [u8; 8] = binary[8..16].try_into().expect("checked above"); + + Ok(u64::from_be_bytes(data_offset) as usize) +} + +fn transform_into_configurable_loader(binary: Vec, blob_id: &BlobId) -> Result> { + // The final code is going to have this structure (if the data section is non-empty): + // 1. loader instructions + // 2. blob id + // 3. length_of_data_section + // 4. the data_section (updated with configurables as needed) + const BLOB_ID_SIZE: u16 = 32; + const REG_ADDRESS_OF_DATA_AFTER_CODE: u8 = 0x10; + const REG_START_OF_LOADED_CODE: u8 = 0x11; + const REG_GENERAL_USE: u8 = 0x12; + let get_instructions = |num_of_instructions| { + // There are 3 main steps: + // 1. Load the blob content into memory + // 2. Load the data section right after the blob + // 3. Jump to the beginning of the memory where the blob was loaded + [ + // 1. Load the blob content into memory + // Find the start of the hardcoded blob ID, which is located after the loader code ends. + op::move_(REG_ADDRESS_OF_DATA_AFTER_CODE, RegId::PC), + // hold the address of the blob ID. + op::addi( + REG_ADDRESS_OF_DATA_AFTER_CODE, + REG_ADDRESS_OF_DATA_AFTER_CODE, + num_of_instructions * Instruction::SIZE as u16, + ), + // The code is going to be loaded from the current value of SP onwards, save + // the location into REG_START_OF_LOADED_CODE so we can jump into it at the end. + op::move_(REG_START_OF_LOADED_CODE, RegId::SP), + // REG_GENERAL_USE to hold the size of the blob. + op::bsiz(REG_GENERAL_USE, REG_ADDRESS_OF_DATA_AFTER_CODE), + // Push the blob contents onto the stack. + op::ldc(REG_ADDRESS_OF_DATA_AFTER_CODE, 0, REG_GENERAL_USE, 1), + // Move on to the data section length + op::addi( + REG_ADDRESS_OF_DATA_AFTER_CODE, + REG_ADDRESS_OF_DATA_AFTER_CODE, + BLOB_ID_SIZE, + ), + // load the size of the data section into REG_GENERAL_USE + op::lw(REG_GENERAL_USE, REG_ADDRESS_OF_DATA_AFTER_CODE, 0), + // after we have read the length of the data section, we move the pointer to the actual + // data by skipping WORD_SIZE B. + op::addi( + REG_ADDRESS_OF_DATA_AFTER_CODE, + REG_ADDRESS_OF_DATA_AFTER_CODE, + WORD_SIZE as u16, + ), + // load the data section of the executable + op::ldc(REG_ADDRESS_OF_DATA_AFTER_CODE, 0, REG_GENERAL_USE, 2), + // Jump into the memory where the contract is loaded. + // What follows is called _jmp_mem by the sway compiler. + // Subtract the address contained in IS because jmp will add it back. + op::sub( + REG_START_OF_LOADED_CODE, + REG_START_OF_LOADED_CODE, + RegId::IS, + ), + // jmp will multiply by 4, so we need to divide to cancel that out. + op::divi(REG_START_OF_LOADED_CODE, REG_START_OF_LOADED_CODE, 4), + // Jump to the start of the contract we loaded. + op::jmp(REG_START_OF_LOADED_CODE), + ] + }; + + let get_instructions_no_data_section = |num_of_instructions| { + // There are 2 main steps: + // 1. Load the blob content into memory + // 2. Jump to the beginning of the memory where the blob was loaded + [ + // 1. Load the blob content into memory + // Find the start of the hardcoded blob ID, which is located after the loader code ends. + op::move_(REG_ADDRESS_OF_DATA_AFTER_CODE, RegId::PC), + // hold the address of the blob ID. + op::addi( + REG_ADDRESS_OF_DATA_AFTER_CODE, + REG_ADDRESS_OF_DATA_AFTER_CODE, + num_of_instructions * Instruction::SIZE as u16, + ), + // The code is going to be loaded from the current value of SP onwards, save + // the location into REG_START_OF_LOADED_CODE so we can jump into it at the end. + op::move_(REG_START_OF_LOADED_CODE, RegId::SP), + // REG_GENERAL_USE to hold the size of the blob. + op::bsiz(REG_GENERAL_USE, REG_ADDRESS_OF_DATA_AFTER_CODE), + // Push the blob contents onto the stack. + op::ldc(REG_ADDRESS_OF_DATA_AFTER_CODE, 0, REG_GENERAL_USE, 1), + // Jump into the memory where the contract is loaded. + // What follows is called _jmp_mem by the sway compiler. + // Subtract the address contained in IS because jmp will add it back. + op::sub( + REG_START_OF_LOADED_CODE, + REG_START_OF_LOADED_CODE, + RegId::IS, + ), + // jmp will multiply by 4, so we need to divide to cancel that out. + op::divi(REG_START_OF_LOADED_CODE, REG_START_OF_LOADED_CODE, 4), + // Jump to the start of the contract we loaded. + op::jmp(REG_START_OF_LOADED_CODE), + ] + }; + + let offset = extract_data_offset(&binary)?; + + if binary.len() < offset { + return Err(fuels_core::error!( + Other, + "data section offset is out of bounds, offset: {offset}, binary len: {}", + binary.len() + )); + } + + let data_section = binary[offset..].to_vec(); + + if !data_section.is_empty() { + let num_of_instructions = u16::try_from(get_instructions(0).len()) + .expect("to never have more than u16::MAX instructions"); + + let instruction_bytes = get_instructions(num_of_instructions) + .into_iter() + .flat_map(|instruction| instruction.to_bytes()); + + let blob_bytes = blob_id.iter().copied(); + + Ok(instruction_bytes + .chain(blob_bytes) + .chain(data_section.len().to_be_bytes()) + .chain(data_section) + .collect()) + } else { + let num_of_instructions = u16::try_from(get_instructions_no_data_section(0).len()) + .expect("to never have more than u16::MAX instructions"); + + let instruction_bytes = get_instructions_no_data_section(num_of_instructions) + .into_iter() + .flat_map(|instruction| instruction.to_bytes()); + + let blob_bytes = blob_id.iter().copied(); + + Ok(instruction_bytes.chain(blob_bytes).collect()) + } +} + +fn validate_loader_can_be_made_from_code( + mut code: Vec, + configurables: Configurables, +) -> Result<()> { + configurables.update_constants_in(&mut code); + + // BlobId currently doesn't affect our ability to produce the loader code + transform_into_configurable_loader(code, &Default::default())?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use fuels_core::Configurables; + use std::io::Write; + use tempfile::NamedTempFile; + + #[test] + fn test_executable_regular_from_bytes() { + // Given: Some bytecode + let code = vec![1u8, 2, 3, 4]; + + // When: Creating an Executable from bytes + let executable = Executable::::from_bytes(code.clone()); + + // Then: The executable should have the given code and default configurables + assert_eq!(executable.state.code, code); + assert_eq!(executable.state.configurables, Default::default()); + } + + #[test] + fn test_executable_regular_load_from() { + // Given: A temporary file containing some bytecode + let code = vec![5u8, 6, 7, 8]; + let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); + temp_file + .write_all(&code) + .expect("Failed to write to temp file"); + let path = temp_file.path().to_str().unwrap(); + + // When: Loading an Executable from the file + let executable_result = Executable::::load_from(path); + + // Then: The executable should be created successfully with the correct code + assert!(executable_result.is_ok()); + let executable = executable_result.unwrap(); + assert_eq!(executable.state.code, code); + assert_eq!(executable.state.configurables, Default::default()); + } + + #[test] + fn test_executable_regular_load_from_invalid_path() { + // Given: An invalid file path + let invalid_path = "/nonexistent/path/to/file"; + + // When: Attempting to load an Executable from the invalid path + let executable_result = Executable::::load_from(invalid_path); + + // Then: The operation should fail with an error + assert!(executable_result.is_err()); + } + + #[test] + fn test_executable_regular_with_configurables() { + // Given: An Executable and some configurables + let code = vec![1u8, 2, 3, 4]; + let executable = Executable::::from_bytes(code); + let configurables = Configurables::new(vec![(2, vec![1])]); + + // When: Setting new configurables + let new_executable = executable.with_configurables(configurables.clone()); + + // Then: The executable should have the new configurables + assert_eq!(new_executable.state.configurables, configurables); + } + + #[test] + fn test_executable_regular_code() { + // Given: An Executable with some code and configurables + let code = vec![1u8, 2, 3, 4]; + let configurables = Configurables::new(vec![(1, vec![1])]); + let executable = + Executable::::from_bytes(code.clone()).with_configurables(configurables); + + // When: Retrieving the code after applying configurables + let modified_code = executable.code(); + + assert_eq!(modified_code, vec![1, 1, 3, 4]); + } + + #[test] + fn test_loader_extracts_code_and_data_section_correctly() { + // Given: An Executable with valid code + let padding = vec![0; 8]; + let offset = 20u64.to_be_bytes().to_vec(); + let some_random_instruction = vec![1, 2, 3, 4]; + let data_section = vec![5, 6, 7, 8]; + let code = [ + padding.clone(), + offset.clone(), + some_random_instruction.clone(), + data_section, + ] + .concat(); + let executable = Executable::::from_bytes(code.clone()); + + // When: Converting to a loader + let loader = executable.convert_to_loader().unwrap(); + + let blob = loader.blob(); + let data_stripped_code = [padding, offset, some_random_instruction].concat(); + assert_eq!(blob.as_ref(), data_stripped_code); + + let loader_code = loader.code(); + let blob_id = blob.id(); + assert_eq!( + loader_code, + transform_into_configurable_loader(code, &blob_id).unwrap() + ) + } + + #[test] + fn test_executable_regular_convert_to_loader_with_invalid_code() { + // Given: An Executable with invalid code (too short) + let code = vec![1u8, 2]; // Insufficient length for a valid data offset + let executable = Executable::::from_bytes(code); + + // When: Attempting to convert to a loader + let result = executable.convert_to_loader(); + + // Then: The conversion should fail with an error + assert!(result.is_err()); + } + + #[test] + fn executable_with_no_data_section() { + // to skip over the first 2 half words and skip over the offset itself, basically stating + // that there is no data section + let data_section_offset = 16u64; + + let code = [vec![0; 8], data_section_offset.to_be_bytes().to_vec()].concat(); + + Executable::from_bytes(code).convert_to_loader().unwrap(); + } +} diff --git a/packages/fuels-programs/src/lib.rs b/packages/fuels-programs/src/lib.rs index 755e14c274..9b0ee77497 100644 --- a/packages/fuels-programs/src/lib.rs +++ b/packages/fuels-programs/src/lib.rs @@ -1,3 +1,4 @@ pub mod calls; pub mod contract; +pub mod executable; pub mod responses; diff --git a/packages/fuels-test-helpers/src/lib.rs b/packages/fuels-test-helpers/src/lib.rs index 659eae860d..673ed2c598 100644 --- a/packages/fuels-test-helpers/src/lib.rs +++ b/packages/fuels-test-helpers/src/lib.rs @@ -157,6 +157,10 @@ pub async fn setup_test_provider( fn testnet_chain_config() -> ChainConfig { let mut consensus_parameters = ConsensusParameters::default(); let tx_params = TxParameters::default().with_max_size(10_000_000); + // on a best effort basis, if we're given an old core we won't fail only because we couldn't + // set the limit here + let _ = consensus_parameters.set_block_transaction_size_limit(10_000_000); + let contract_params = ContractParameters::default().with_contract_max_size(1_000_000); consensus_parameters.set_tx_params(tx_params); consensus_parameters.set_contract_params(contract_params); diff --git a/packages/fuels-test-helpers/src/service.rs b/packages/fuels-test-helpers/src/service.rs index 0e40b812a4..11c3926e68 100644 --- a/packages/fuels-test-helpers/src/service.rs +++ b/packages/fuels-test-helpers/src/service.rs @@ -91,8 +91,9 @@ impl FuelService { graphql_config: GraphQLConfig { addr: node_config.addr, max_queries_depth: 16, - max_queries_complexity: 20000, + max_queries_complexity: 80000, max_queries_recursive_depth: 16, + max_queries_directives: 10, request_body_bytes_limit: 16 * 1024 * 1024, query_log_threshold_time: Duration::from_secs(2), api_request_timeout: Duration::from_secs(60),