diff --git a/docs/content/concepts/sui-move-concepts.mdx b/docs/content/concepts/sui-move-concepts.mdx index 4396e3a21e07..1b644cf08bbd 100644 --- a/docs/content/concepts/sui-move-concepts.mdx +++ b/docs/content/concepts/sui-move-concepts.mdx @@ -64,6 +64,10 @@ Make your function private (don't add the `public` visibility keyword) and mark In addition to this Sui-specific use case, there are other rules and restrictions for `entry` functions: -- `entry` functions can only return types with the `drop` ability. +- Non-public `entry` functions (private or `public(package)`) have restrictions on arguments that are entangled with hot potato values in a PTB. See [Non-public entry function restrictions](/guides/developer/transactions/ptbs/prog-txn-blocks#non-public-entry-function-restrictions) for details. -- `entry` functions can only take objects as inputs if they weren't used as inputs in any non-`entry` functions in the same PTB. \ No newline at end of file +:::info + +Note that while previously `entry` functions had additional restrictions on their signatures (their types for parameters and return values), this is no longer the case. `entry` functions can have the same arguments or return values as a `public` function in a PTB. + +::: diff --git a/docs/content/guides/developer/dev-cheat-sheet.mdx b/docs/content/guides/developer/dev-cheat-sheet.mdx index 30e69a4f0e51..7a66a8e47642 100644 --- a/docs/content/guides/developer/dev-cheat-sheet.mdx +++ b/docs/content/guides/developer/dev-cheat-sheet.mdx @@ -30,7 +30,7 @@ Quick reference on best practices for Sui Network developers. - Packages are immutable, so any published package can be called forever. Use [object versioning](/guides/developer/packages/upgrade.mdx#versioned-shared-objects) to prevent older versions from being called. - If you upgrade a package `P1` to `P2`, other packages and clients that depend on `P1` will continue using `P1`. They do not auto-update to `P2`. Both dependent packages and client code must be explicitly updated to point at `P2`. - Packages that expect to be extended by dependent packages can avoid breaking their extensions with each upgrade by providing a standard (unchanging) interface that all versions conform to. See the [message sending](https://github.com/wormhole-foundation/wormhole/blob/74dea3bf22f0e27628b432c3e9eac05c85786a99/sui/wormhole/sources/publish_message.move) across a bridge example from Wormhole. Extension packages that produce outbound messages can use [`prepare_message`](https://github.com/wormhole-foundation/wormhole/blob/74dea3bf22f0e27628b432c3e9eac05c85786a99/sui/wormhole/sources/publish_message.move#L68-L90) from any version of the Wormhole package to produce a [`MessageTicket`](https://github.com/wormhole-foundation/wormhole/blob/74dea3bf22f0e27628b432c3e9eac05c85786a99/sui/wormhole/sources/publish_message.move#L52-L66) while client code that sends the message must pass that `MessageTicket` into [`publish_message`](https://github.com/wormhole-foundation/wormhole/blob/74dea3bf22f0e27628b432c3e9eac05c85786a99/sui/wormhole/sources/publish_message.move#L92-L152) in the latest version of the package. - - `public` function signatures cannot be deleted or changed, but `public(friend)` functions can. Use `public(friend)` or private visibility liberally unless you are exposing library functions that will live forever. + - `public` function signatures cannot be deleted or changed, but `public(package)` functions can. Use `public(package)` or private visibility liberally unless you are exposing library functions that will live forever. - It is not possible to delete `struct` types, change their definition or add new [abilities](https://move-book.com/reference/abilities) via an upgrade. Introduce new types carefully as they will live forever. ### Testing diff --git a/docs/content/guides/developer/objects/object-ownership/immutable.mdx b/docs/content/guides/developer/objects/object-ownership/immutable.mdx index 74aee7787ce3..e9b10fc5d3d7 100644 --- a/docs/content/guides/developer/objects/object-ownership/immutable.mdx +++ b/docs/content/guides/developer/objects/object-ownership/immutable.mdx @@ -37,9 +37,9 @@ This function creates a new `ColorObject` and immediately makes it immutable bef ## When to use immutable objects -After an object becomes immutable, the rules of who can use this object in Sui Move calls change: +After an object becomes immutable, the rules of who can use this object in Move calls change: -1. You can only pass an immutable object as a read-only, immutable reference to Sui Move entry functions as `&T`. +1. You can only pass an immutable object as a read-only, immutable reference to Move functions as `&T`. 2. All network participants can access immutable objects. @@ -166,4 +166,4 @@ public fun update( } ``` -The function fails because the `ColorObject` is immutable. \ No newline at end of file +The function fails because the `ColorObject` is immutable. diff --git a/docs/content/guides/developer/transactions/ptbs/inputs-and-results.mdx b/docs/content/guides/developer/transactions/ptbs/inputs-and-results.mdx index f41e4756d8a3..92a49e334f55 100644 --- a/docs/content/guides/developer/transactions/ptbs/inputs-and-results.mdx +++ b/docs/content/guides/developer/transactions/ptbs/inputs-and-results.mdx @@ -8,7 +8,7 @@ Programmable transaction blocks (PTBs) operate on inputs and produce results. In ## Inputs -Input arguments to a PTB are broadly categorized as either objects or pure values. The direct implementation of these arguments is often obscured by transaction builders or SDKs. Each `Input` is either an object, `Input::Object(ObjectArg)`, which contains the necessary metadata to specify the object being used, or a pure value, `Input::Pure(PureArg)`, which contains the bytes of the value. +Input arguments to a PTB are broadly categorized as either objects or pure values. The direct implementation of these arguments is often obscured by transaction builders or SDKs. Each `Input` is either an object, `CallArg::Object(ObjectArg)`, which contains the necessary metadata to specify the object being used, or a pure value, `CallArg::Pure(Vec)`, which contains the BCS bytes of the value. For object inputs, the metadata needed differs depending on the type of [ownership of the object](/guides/developer/objects/object-ownership). The data for the `ObjectArg` enum follows: @@ -32,19 +32,19 @@ For pure inputs, the only data provided is the [BCS](https://github.com/MystenLa For vectors and options, `T` must be a valid pure input type. This rule applies recursively. -Bytes are not validated until the type is specified in a command, for example in `MoveCall` or `MakeMoveVec`. This means that a given pure input could be used to instantiate Move values of several types. +Bytes are not validated until a command specifies the expected type, for example in `MoveCall` or `MakeMoveVec`. Each distinct type creates a separate typed copy of the bytes, so the same pure input can be used at multiple types as long as the bytes are valid for each. Each typed copy is treated as its own input, so mutations affect only that type's value. ## Results Each transaction command produces an array of values. The array can be empty. The type of the value can be any arbitrary Move type, so unlike inputs, the values are not limited to objects or pure values. The number of results generated and their types are specific to each transaction command: -- `MoveCall`: The number of results and their types are determined by the Move function being called. Move functions that return references are not supported at this time. +- `MoveCall`: The number of results and their types are determined by the Move function being called. Move calls cannot return references (`&T` or `&mut T`); this restriction will be lifted in the future. - `SplitCoins`: Produces one or more coins from a single coin. The type of each coin is `sui::coin::Coin` where the specific coin type `T` matches the coin being split. -- `Publish`: Returns the upgrade capability `sui::package::UpgradeCap` for the newly published package. +- `Publish`: Returns a single `sui::package::UpgradeCap`. Module bytes and dependency IDs are embedded in the command structure; they are not `Argument` values. After the package is staged, `init` functions run for each module in order. -- `Upgrade`: Returns the upgrade receipt `sui::package::UpgradeReceipt` for the upgraded package. +- `Upgrade`: Takes exactly one `Argument`: a `sui::package::UpgradeTicket` (by value). Returns a single `sui::package::UpgradeReceipt`. Module bytes and dependency IDs are embedded in the command, not supplied as `Argument` values. Does not call `init` functions. - `TransferObjects` and `MergeCoins` produce an empty result vector. @@ -104,7 +104,7 @@ const hero = tx.moveCall({ typeArguments: [], }); -//According to Move function new_sword, this moveCall will return an Object of Type Hero +// According to Move function new_sword, this moveCall will return an Object of Type Sword const sword = tx.moveCall({ target: `0x123::hero::new_sword`, arguments: [tx.pure.u64(10)], diff --git a/docs/content/guides/developer/transactions/ptbs/prog-txn-blocks.mdx b/docs/content/guides/developer/transactions/ptbs/prog-txn-blocks.mdx index 5d0fb4ba0e89..d6f43b6129b8 100644 --- a/docs/content/guides/developer/transactions/ptbs/prog-txn-blocks.mdx +++ b/docs/content/guides/developer/transactions/ptbs/prog-txn-blocks.mdx @@ -4,7 +4,7 @@ description: Programmable transaction blocks are a group of commands that comple keywords: [ programmable transaction blocks, transaction blocks, program transaction blocks, PTB, PTBs, what are PTBs, what is PTB, transaction group, programmable group of transactions, program transaction block, block of transactions, group of transactions ] --- -Transactions on Sui are composed of groups of commands that execute on inputs to define the result of the transaction. Referred to as **programmable transaction blocks (PTBs)**, these groups of commands define all user transactions on Sui. PTBs allow a user to call multiple Move functions, manage their objects, and manage their coins in a single transaction without publishing a new Move package. Designed with automation and transaction builders in mind, PTBs are a lightweight and flexible way of generating transactions. +Transactions on Sui are composed of groups of commands that execute on inputs to define the result of the transaction. Referred to as **programmable transaction blocks (PTBs)**, these groups of commands define all user transactions on Sui. PTBs allow a user to call multiple Move functions, manage their objects, and manage their coins in a single transaction without publishing a new Move package. Designed with automation and transaction builders in mind, PTBs are a lightweight and flexible way of generating transactions. However, more intricate programming patterns, such as loops, are not supported. In such cases, you must publish a new Move package. @@ -44,6 +44,7 @@ Looking closer at the two main components: - `tx.moveCall({ target, arguments, typeArguments })`: Executes a Move call. Returns whatever the Sui Move call returns. - Example: `tx.moveCall({ target: '0x2::devnet_nft::mint', arguments: [tx.pure.string(name), tx.pure.string(description), tx.pure.string(image)] })` + - `tx.makeMoveVec({ type, elements })`: Constructs a vector of objects that can be passed into a moveCall. This is required as there's no other way to define a vector as an input. - Example: `tx.makeMoveVec({ elements: [tx.object(id1), tx.object(id2)] })` @@ -86,11 +87,11 @@ At the beginning of execution, the PTB runtime takes the input objects and loads The most important thing to note at this stage is the effects on the gas coin. At the beginning of execution, the maximum gas budget (in terms of SUI) is withdrawn from the gas coin. Any unused gas is returned to the gas coin at the end of execution, even if the coin has changed owners. -### Object consumption +### Object consumption -All objects created or returned by Move commands must either be consumed (destroyed, transferred, or used by another command) or explicitly dropped if the type has the `drop` ability. +All objects created or returned by Move commands must either be consumed (destroyed, transferred, or used by another command) or explicitly dropped if the type has the `drop` ability. -In a PTB, if you create an object through a Move command and do not destroy, transfer, or use it in a subsequent command, the transaction will fail with an error. +In a PTB, if you create an object through a Move command and do not destroy, transfer, or use it in a subsequent command, the transaction will fail with an error. ### Pre-execution validation @@ -140,9 +141,140 @@ Shared objects have restrictions on by value usage to ensure they remain shared - Shared objects can be wrapped or converted to dynamic fields during execution, but must be re-shared or deleted before the transaction completes. +#### Move call rules + +PTBs can call any `public` function and any `entry` function, whether private (`entry fun f()`), or `public(package)` (`public(package) entry fun f()`). Non-entry private and `public(package)` functions cannot be called from PTBs. Note that in this way, there is no reason to add `entry` to a `public` function. + +:::info + +Previously, `entry` functions had signature restrictions that limited their parameter and return types compared to `public` functions. These restrictions have been removed. `entry` functions can now have the same signature as any `public` function. + +::: + +**Return types:** +Move calls cannot return references (`&T` or `&mut T`). However, this restriction will be lifted in the future. + +**Private generics:** +Some framework functions have type parameters that can only be instantiated with types defined in the same module. Since PTBs are not modules, they cannot supply these types and therefore cannot call these functions directly. For example, certain `sui::transfer` functions like `transfer` and `share_object` require a type defined in the calling module, and as such cannot be called from a PTB. Instead, use the `public_transfer` and `public_share_object` variants. + +**`TxContext` handling:** +`TxContext` parameters (`&TxContext` or `&mut TxContext`) are automatically injected by the runtime; callers do not supply them. `TxContext` can appear at any position in the parameter list, and a function can have multiple `TxContext` parameters as long as they are immutable (`&TxContext`). These parameters are not counted toward the user-supplied argument count for indexing purposes. + +#### Non-public entry function restrictions + +A non-public `entry` function is a function declared with `entry` that is **not** `public`. It is either private (`entry fun f()`) or `public(package)` (`public(package) entry fun f()`). These functions can be called directly in PTBs but not from other packages. + +Non-public `entry` functions have a single restriction: their arguments cannot be in a "hot" clique (see below) when the function is called. The intuition behind this restriction is to ensure that arguments to non-public `entry` functions are not entangled with outstanding [hot potato](https://move-book.com/programmability/hot-potato-pattern) values that could force behavior after the function executes. + +**Hot potato values** + +A value is a hot potato if its type has neither the `drop` nor the `store` ability. Hot potato values must be consumed (moved by value) before the transaction completes; they cannot be silently dropped or stored. + +**Cliques** + +The system tracks which values are entangled using cliques. As described above, the intuition behind to goal is to prevent hot potato values from forcing behavior outside of the call to the non-public `entry` function. The cliques provide a modeling of this "entanglement" by keeping a count of how many hot potato values are live and which values they have interacted with: + +- Each PTB input starts in its own clique with a hot count of 0. +- When values are used together as arguments in a command, their cliques merge. The hot counts add together. +- Hot potato return values from a command increment the merged clique's hot count. +- Moving (consuming) a hot potato decrements its clique's hot count. +- Before a non-public `entry` call, the merged clique of its arguments must have a hot count of 0. + +A non-public `entry` function can consume hot potato values, but they must be the last hot values in their clique. The hot count check happens after arguments are consumed but before the function is verified. + +**Shared objects consumed by value** + +Consuming a shared object by value permanently marks its clique as "always hot." Since shared objects cannot be wrapped (they must be re-shared or deleted) consuming one by value is treated similarly to a hot potato that can never be resolved. A non-public `entry` function can receive a shared object by value directly, but it cannot receive a value whose clique previously interacted with a shared object consumed by value. + +**Examples** + +Consider the following module: + +```move +module ex::m; + +public struct HotPotato() + +public fun hot(x: &mut Coin): HotPotato { ... } + +entry fun spend(x: &mut Coin) { ... } + +public fun cool(h: HotPotato) { ... } +``` + +In this invalid PTB, the `HotPotato` from command 0 is still alive in the same clique as `Input(0)` when `spend` is called: + +```rust +// Invalid PTB +// Input 0: Coin +// cliques: { Input(0) } => 0 +0: ex::m::hot(Input(0)); +// cliques: { Input(0), Result(0) } => 1 +1: ex::m::spend(Input(0)); // INVALID: Input(0)'s clique has count > 0 +2: ex::m::cool(Result(0)); +``` + +However, if the hot potato is consumed before calling `spend`, the clique's count drops to 0 and the call succeeds: + +```rust +// Valid PTB +// Input 0: Coin +// cliques: { Input(0) } => 0 +0: ex::m::hot(Input(0)); +// cliques: { Input(0), Result(0) } => 1 +1: ex::m::cool(Result(0)); +// cliques: { Input(0) } => 0 +2: ex::m::spend(Input(0)); // Valid: Input(0)'s clique has count 0 +``` + +In a flash loan scenario, entanglement extends transitively. Even if a value was not directly involved in the loan, being in the same clique as the loan's hot potato makes it ineligible for a non-public `entry` call: + +```move +module flash::loan; + +public struct Loan { amount: u64 } + +public fun issue(bank: &mut Bank, amount: u64): (Balance, Loan) { ERROR } + +public fun repay(bank: &mut Bank, loan: Loan, repayment: Balance) { ERROR } +``` + +```rust +// Invalid PTB +// Input 0: flash::loan::Bank, Input 1: u64 +// cliques: { Input(0) } => 0, { Input(1) } => 0 +0: flash::loan::issue(Input(0), Input(1)) +// cliques: { Input(0), Input(1), NestedResult(0,0), NestedResult(0,1) } => 1 +1: sui::coin::from_balance(NestedResult(0,0)); +// cliques: { Input(0), Input(1), NestedResult(0,1), Result(1) } => 1 +2: ex::m::spend(Result(1)); // INVALID: Result(1)'s clique has count > 0 +3: sui::coin::into_balance(Result(1)); +4: flash::loan::repay(Input(0), NestedResult(0,1), Result(3)); +``` + +Even though the `Coin` created in command 1 was not directly involved in the flash loan, it is part of a clique with the outstanding `Loan` hot potato (`NestedResult(0,1)`). Repaying the loan before calling `spend` would make the PTB valid. + #### Pure value type checking -Pure values are not type checked until their usage. When checking if a pure value has type `T`, the system verifies whether `T` is a valid type for a pure value (see the list in the [Inputs section](/guides/developer/transactions/ptbs/inputs-and-results)). If it is, the bytes are validated. You can use a pure value with multiple types as long as the bytes are valid for each type. For example, you can use a string as an ASCII string `std::ascii::String` and as a UTF8 string `std::string::String`. However, after you mutably borrow the pure value, the type becomes fixed, and all future usages must be with that type. +Pure values are not type checked until they are used. The first time a pure value appears as an argument expecting type `T`, the system checks that `T` is a valid pure type (see the [Inputs section](/guides/developer/transactions/ptbs/inputs-and-results)) and that the bytes deserialize to `T`. Each distinct type creates a separate typed copy of the bytes, so the same pure input can be used at multiple types as long as the bytes are valid for each. For example, you can use the same bytes as an ASCII string `std::ascii::String` and as a UTF-8 string `std::string::String`. Each typed copy is treated as its own input, so mutations affect only that type's value, not all values created from the same bytes. + +#### Publish and upgrade rules + +Both commands embed their module bytes and dependency IDs directly in the command structure, however, these are not PTB `Argument` values. + +**Publish:** + +- Returns a single `sui::package::UpgradeCap`. +- After the package is staged, the runtime calls each module's `init` function (if present) in the order the modules are provided (in the vector of serialized bytes). `init` receives `&mut TxContext` and optionally a one-time witness. Additional `init` arguments are not yet supported but are planned. +- The package is available to `init` functions during execution. It is staged before they run. + +**Upgrade:** + +- Takes exactly one PTB argument: a `sui::package::UpgradeTicket` (by value). +- Returns a single `sui::package::UpgradeReceipt`. +- Does not call `init` functions. Newly added modules in an upgrade cannot have `init` functions. This restriction will be lifted in a future release. +- The module digest and package ID in the ticket must match exactly. +- The upgrade policy (compatible, additive, dep-only) is enforced from the ticket. ### End of execution @@ -172,7 +304,7 @@ Any remaining SUI deducted from the gas coin at the beginning of execution is re The total effects (created, mutated, and deleted objects) are then passed out of the execution layer and applied by the Sui network. -## Execution example +## Execution example By following each command's execution, you can see how inputs flow through commands, how results accumulate, and how the final transaction effects are determined. @@ -286,4 +418,4 @@ Results: [ - `Item { id: id2 }` transferred to sender. -- Marketplace object returned as shared (mutated). \ No newline at end of file +- Marketplace object returned as shared (mutated).