본문으로 건너뛰기

프로그래머블 트랜잭션 블록 (PTB)

Sui의 트랜잭션은 트랜잭션 결과를 정의하기 위해 입력에 대해 실행되는 명령 그룹으로 구성된다. **프로그래머블 트랜잭션 블록 (PTB)**라고 부르는 이 명령 그룹이 Sui의 모든 사용자 트랜잭션을 정의한다. PTB를 사용하면 사용자는 새 Move 패키지를 publish하지 않고도 하나의 트랜잭션 안에서 여러 Move 함수를 호출하고, 자신의 객체를 관리하고, 자신의 코인을 관리할 수 있다. 자동화와 트랜잭션 builder를 염두에 두고 설계된 PTB는 트랜잭션을 생성하는 가볍고 유연한 방식이다.

그러나 loop 같은 더 복잡한 프로그래밍 패턴은 지원되지 않는다. 이런 경우에는 새 Move 패키지를 publish해야 한다.

PTB 안의 개별 트랜잭션 명령은 순서대로 실행된다. 한 트랜잭션 명령의 결과를 같은 PTB 안에 있는 이후 트랜잭션 명령에서 사용할 수 있다. 객체 수정이나 transfer 같은 블록 안 각 트랜잭션 명령의 effect는 트랜잭션 마지막에 원자적으로 적용된다. 한 트랜잭션 명령이 실패하면 전체 블록이 실패하고 어떤 명령의 effect도 적용되지 않는다.

PTB는 한 번의 실행에서 최대 1,024개의 고유 operation을 수행할 수 있지만, 전통적인 블록체인의 트랜잭션은 같은 결과를 얻기 위해 1,024번의 개별 실행이 필요하다. 이 구조는 더 저렴한 가스 수수료도 촉진한다. 개별 트랜잭션을 처리하는 비용은 같은 트랜잭션을 PTB로 묶어 처리하는 비용보다 항상 더 높다.

Transaction component 구성​

실행 의미론과 관련해 PTB에는 두 부분이 있다. 트랜잭션 sender나 gas limit 같은 다른 트랜잭션 정보는 참조될 수 있지만 여기서는 범위 밖이다. PTB의 구조는 다음과 같다:

{
inputs: [Input],
commands: [Command],
}

두 주요 구성 요소를 더 자세히 보면 다음과 같다:

  • inputs 값은 인자 벡터 [Input]이다. 이 인자들은 command에서 사용할 수 있는 객체이거나 pure value이다. 객체는 sender가 소유한 것이거나 공유 객체 또는 불변 객체이다. pure value는 u64나 String 값처럼 바이트만으로 순수하게 구성할 수 있는 단순한 Move 값이다. 역사적인 이유로 Rust 구현에서는 Input을 CallArg라고 부른다.

  • commands 값은 command 벡터 [Command]이다. 가능한 command는 다음과 같다:

  • tx.splitCoins(coin, amounts): 제공된 코인에서 정의한 amount만큼 분할해 새 코인을 만든다. 이후 트랜잭션에서 사용할 수 있도록 코인을 반환한다.

    • 예시: tx.splitCoins(tx.gas, [tx.pure.u64(100), tx.pure.u64(200)])
  • tx.mergeCoins(destinationCoin, sourceCoins): sourceCoins를 destinationCoin에 병합한다.

    • 예시: tx.mergeCoins(tx.object(coin1), [tx.object(coin2), tx.object(coin3)])
  • tx.transferObjects(objects, address): 객체 목록을 지정한 address로 전송한다.

    • 예시: tx.transferObjects([tx.object(thing1), tx.object(thing2)], tx.pure.address(myAddress))
  • tx.moveCall({ target, arguments, typeArguments }): Move 호출을 실행한다. Sui Move 호출이 반환하는 값을 그대로 반환한다.

    • 예시: tx.moveCall({ target: '0x2::devnet_nft::mint', arguments: [tx.pure.string(name), tx.pure.string(description), tx.pure.string(image)] })
  • tx.makeMoveVec({ type, elements }): moveCall에 전달할 수 있는 객체 vector를 구성한다. vector를 입력으로 정의할 다른 방법이 없기 때문에 이 command가 필요하다.

    • 예시: tx.makeMoveVec({ elements: [tx.object(id1), tx.object(id2)] })
  • tx.publish(modules, dependencies): Move 패키지를 publish한다. upgrade capability 객체를 반환한다.

  • tx.upgrade(modules, dependencies, packageId: EXAMPLE_PACKAGE_ID, ticket): 기존 패키지를 업그레이드한다. 업그레이드된 모듈에 대해서는 어떤 init 함수도 호출되지 않는다.

각 PTB 명령에 대해 더 알아본다.

Input과 결과​

입력은 PTB에 제공되는 값이고, 결과는 PTB command가 만들어 내는 값이다. 입력은 객체이거나 단순한 Move 값이며, 결과는 객체를 포함한 임의의 Move 값이다. 입력과 결과는 PTB 실행을 통과하는 데이터 흐름을 이룬다.

이를 값 배열을 채우는 것으로 볼 수 있다. 입력은 단일 배열이지만 결과는 각 개별 트랜잭션 command마다 하나씩 배열이 있어 결과 값의 2D 배열이 만들어진다. 타입이 허용한다면 이 값들은 가변 또는 불변으로 borrow하거나, 복사하거나, 이동해 접근할 수 있으며, 이동은 재인덱싱 없이 값을 배열 밖으로 가져간다.

inputs and results에 대해 더 알아본다.

Argument structure와 usage​

command는 사용할 입력 또는 결과를 지정하는 Argument 값을 받는다. 런타임은 command가 기대하는 타입을 바탕으로 인자를 reference로 전달할지 값으로 전달할지를 추론한다.

Argument enum에는 4개 variant가 있다:

  • Input(u16): 인덱스로 입력을 참조한다. 예를 들어 입력이 [Object1, Object2, Pure1, Object3]라면 Object1에는 Input(0), Pure1에는 Input(2)를 사용한다.

  • GasCoin: 가스 지불에 사용하는 SUI 코인을 참조한다. 주소 잔액 기반 가스 지불(빈 가스 지불 목록)을 사용할 때 GasCoin은 송신자 또는 스폰서 주소 잔액에서 생성된 synthetic coin을 참조한다. 이 값은 특별한 제약이 있으므로 다른 입력과 분리되어 있다:

    • 어디에서든 &mut 또는 &로 사용할 수 있다.
    • TransferObjects 또는 sui::coin::send_funds를 통해서만 값으로 사용한다.
      • GasCoin에서 소유 Coin<SUI>이 필요하면 먼저 SplitCoins를 사용한다.
  • NestedResult(u16, u16): 이전 command의 결과를 참조한다. 첫 번째 인덱스는 command를, 두 번째 인덱스는 그 command에서 어떤 결과인지를 지정한다. 예를 들어 command 1이 [Value1, Value2]를 반환한다면 Value1에는 NestedResult(1, 0), Value2에는 NestedResult(1, 1)을 사용한다.

  • Result(u16): NestedResult(i, 0)의 축약형이지만 인덱스 i의 command가 정확히 1개의 결과를 반환할 때만 유효하다. 0개 또는 여러 결과를 반환하는 command에는 NestedResult를 사용한다.

Execution 실행​

PTB를 실행할 때 입력 벡터는 입력 객체 또는 pure value bytes로 채워진다. 그 다음 트랜잭션 command가 순서대로 실행되고, 결과는 결과 벡터에 저장된다. 마지막으로 트랜잭션의 effect가 원자적으로 적용된다.

Execution 시작​

실행 시작 시 PTB 런타임은 입력 객체를 가져와 입력 배열에 적재한다. 이 객체들은 존재 여부와 유효한 소유권 같은 규칙을 검사하며 네트워크가 이미 검증한 상태이다. pure value bytes도 배열에 적재되지만 사용할 때까지는 검증되지 않는다.

이 단계에서 가장 중요한 점은 가스 코인에 대한 effect이다. 실행 시작 시 최대 가스 예산(SUI 기준)이 가스 코인에서 인출된다. 사용되지 않은 gas는 실행 끝에 가스 코인으로 반환되며, 코인의 owner가 바뀌었더라도 마찬가지이다.

Object consumption 처리​

Move command가 생성하거나 반환한 모든 객체는 소비되거나(파기, 전송, 다른 command에서 사용), 타입에 drop ability가 있다면 명시적으로 drop되어야 한다.

PTB에서 Move command를 통해 객체를 생성한 뒤 이를 파기하거나 전송하거나 다음 command에서 사용하지 않으면 트랜잭션은 error와 함께 실패한다.

Pre-execution validation 검증​

트랜잭션에 서명되면 네트워크는 특정 command에 대해 다음 검증을 수행한다:

  • SplitCoins와 MergeCoins: 인자 배열(AmountArgs, ToMergeArgs)이 비어 있지 않은지 검증한다.

  • MakeMoveVec: 비어 있는 Args vector에는 type이 지정되어야 함을 검증한다.

  • Publish: ModuleBytes가 비어 있지 않은지 검증한다.

Argument usage rule 규칙​

각 인자는 reference 또는 값으로 사용될 수 있다. 사용 방식은 인자의 타입과 command의 타입 signature에 따라 결정된다:

  • signature가 &mut T를 기대하면 런타임은 인자의 타입이 T인지 확인하고 이후 가변 borrow를 수행한다.

  • signature가 &T를 기대하면 런타임은 인자의 타입이 T인지 확인하고 이후 불변 borrow를 수행한다.

  • signature가 T를 기대하면 런타임은 인자의 타입이 T인지 검증한 다음, T: copy이면 값을 복사하고 그렇지 않으면 이동한다. 객체는 sui::object::UID에 copy ability가 없기 때문에 항상 이동된다.

인자가 이동된 뒤 어떤 형태로든 다시 사용되면 트랜잭션은 실패한다. 이동된 인자를 원래 위치(입력 또는 결과 인덱스)로 되돌리는 방법은 없다.

인자가 복사되었지만 drop ability가 없다면 마지막 사용은 이동으로 추론된다. 그 결과 인자에 copy는 있고 drop이 없다면 마지막 사용은 값으로 이뤄져야 한다. 그렇지 않으면 drop이 없는 값이 사용되지 않았기 때문에 트랜잭션이 실패한다.

Borrowing rule 규칙​

borrow는 안전한 reference 사용을 보장하기 위해 추가 규칙을 따른다:

  • Mutable borrow: 다른 borrow가 남아 있어서는 안 된다. 겹치는 mutable borrow는 dangling reference를 만들 수 있다.

  • Immutable borrow: mutable borrow가 남아 있어서는 안 된다. 여러 immutable borrow는 허용된다.

  • Move: borrow가 남아 있어서는 안 된다. 빌린 값을 이동하면 기존 reference가 무효화된다.

  • Copy: 남아 있는 borrow와 관계없이 허용된다.

Special 객체 handling 처리​

객체 입력은 underlying 객체의 타입 T를 가진다. 예외는 ObjectArg::Receiving 입력이며, 이 경우 타입은 sui::transfer::Receiving<T>이다. 이 wrapper는 객체가 sender가 아니라 다른 객체에 의해 소유됨을 나타낸다. 부모 객체와 함께 sui::transfer::receive를 호출해 이를 unwrap하고 소유권을 증명한다.

공유 객체는 트랜잭션 끝까지 shared 상태를 유지하거나 삭제되도록 보장하기 위해 값으로 사용하는 데 제약이 있다:

  • 읽기 전용 공유 객체(mutable이 아닌 것으로 표시됨)는 값으로 사용할 수 없다.

  • 공유 객체는 transfer하거나 freeze할 수 없다. 이런 operation은 실행 중에는 성공하지만 트랜잭션 마지막에 실패를 일으킨다.

  • 공유 객체는 실행 중에 wrap되거나 dynamic 필드로 변환될 수 있지만, 트랜잭션이 끝나기 전에 다시 공유되거나 삭제되어야 한다.

Move call rule 규칙​

PTB는 public 함수과 모든 entry 함수를 호출할 수 있다. 여기에는 private entry fun f()와 public(package) entry fun f()도 포함된다. Non-entry private 함수과 public(package) 함수는 PTB에서 호출할 수 없다. 이런 이유로 public 함수에 entry를 추가할 필요는 없다.

정보

이전에는 entry 함수가 public 함수보다 parameter와 return type에 더 제한적인 signature restriction을 가졌다. 이러한 restriction은 제거되었다. 이제 entry 함수는 모든 public 함수과 같은 signature를 가질 수 있다.

return type: Move call은 reference(&T 또는 &mut T)를 return할 수 없다. 이 restriction은 향후 제거될 예정이다.

Private generics: 일부 framework 함수에는 같은 모듈에 정의된 type으로만 instantiate할 수 있는 type parameter가 있다. PTB는 모듈이 아니므로 이러한 type을 제공할 수 없고, 따라서 해당 함수를 직접 호출할 수 없다. 예를 들어 transfer와 share_object 같은 특정 sui::transfer 함수는 호출 모듈에 정의된 type을 요구하므로 PTB에서 호출할 수 없다. 대신 public_transfer 및 public_share_object variant를 사용한다.

TxContext handling: TxContext parameter(&TxContext 또는 &mut TxContext)는 runtime이 자동으로 inject하며 caller가 제공하지 않는다. TxContext는 parameter list의 어느 위치에나 나타날 수 있고, immutable(&TxContext)인 한 하나의 함수에 여러 TxContext parameter가 있을 수 있다. 이러한 parameter는 indexing을 위해 user-supplied 인자 count에 포함되지 않는다.

Non-public entry 함수 제한​

Non-public entry 함수는 entry로 선언되었지만 public이 아닌 함수가다. 이는 private(entry fun f())이거나 public(package)(public(package) entry fun f())이다. 이러한 함수는 PTB에서 직접 호출할 수 있지만 다른 패키지에서는 호출할 수 없다.

Non-public entry function에는 하나의 restriction이 있다. Function이 호출될 때 argument가 "hot" clique(아래 참조)에 있으면 안 된다. 이 restriction의 직관은 non-public entry function의 argument가 function 실행 후 behavior를 강제할 수 있는 outstanding hot potato value와 얽히지 않도록 하는 것이다.

Hot potato values

Type에 drop ability와 store ability가 모두 없으면 그 value는 hot potato이다. Hot potato value는 트랜잭션이 완료되기 전에 consume(값으로 move)되어야 하며, 조용히 drop되거나 store될 수 없다.

Cliques

System은 clique를 사용해 어떤 value가 얽혀 있는지 추적한다. 목표는 위에서 설명한 것처럼 hot potato value가 non-public entry 함수 호출 밖의 behavior를 강제하지 못하게 하는 것이다. Clique는 live hot potato value 수와 그 value가 상호작용한 value를 count하여 이러한 "entanglement"를 model한다:

  • 각 PTB 입력은 hot count 0인 자체 clique에서 시작한다.
  • value가 command의 인자로 함께 사용되면 clique가 merge된다. hot count는 더해진다.
  • command의 hot potato return value는 merge된 clique의 hot count를 증가시킨다.
  • hot potato를 move(consume)하면 해당 clique의 hot count가 감소한다.
  • non-public entry call 전에 인자의 merged clique는 hot count 0이어야 한다.

Non-public entry 함수는 hot potato value를 consume할 수 있지만, 해당 clique의 마지막 hot value여야 한다. Hot count check는 인자가 consume된 뒤 함수가 verify되기 전에 발생한다.

Shared 객체 consumed by value

공유 객체를 값으로 consume하면 해당 clique는 영구적으로 "always hot"으로 표시된다. 공유 객체는 wrap될 수 없고 다시 share되거나 delete되어야 하므로, 값을 consume하는 것은 resolve될 수 없는 hot potato와 유사하게 처리된다. Non-public entry 함수는 공유 객체를 값으로 직접 받을 수 있지만, 이전에 값으로 consume된 공유 객체와 상호작용한 clique의 value는 받을 수 없다.

Examples

다음 모듈을 고려한다:

module ex::m;

public struct HotPotato()

public fun hot<T>(x: &mut Coin<T>): HotPotato { ... }

entry fun spend<T>(x: &mut Coin<T>) { ... }

public fun cool(h: HotPotato) { ... }

다음 invalid PTB에서는 command 0의 HotPotato가 spend 호출 시 Input(0)과 같은 clique에서 아직 live 상태이다:

// Invalid PTB
// Input 0: Coin<SUI>
// 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));

하지만 spend를 호출하기 전에 hot potato가 consume되면 clique count가 0으로 내려가고 call이 성공한다:

// Valid PTB
// Input 0: Coin<SUI>
// 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

Flash loan scenario에서는 entanglement가 transitively 확장된다. 어떤 value가 loan에 직접 관여하지 않았더라도 loan의 hot potato와 같은 clique에 있으면 non-public entry call에 사용할 수 없다:

module flash::loan;

public struct Loan { amount: u64 }

public fun issue(bank: &mut Bank, amount: u64): (Balance<SUI>, Loan) { ERROR }

public fun repay(bank: &mut Bank, loan: Loan, repayment: Balance<SUI>) { ERROR }
// 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));

Command 1에서 만든 Coin이 flash loan에 직접 관여하지 않았더라도 outstanding Loan hot potato(NestedResult(0,1))와 같은 clique의 일부이다. spend를 호출하기 전에 loan을 repay하면 PTB가 valid해진다.

Pure value type checking 검증​

pure value는 사용 시점까지 타입 검사를 받지 않는다. pure value의 타입이 T인지 검사할 때 시스템은 먼저 T가 pure value에 유효한 타입인지 확인한다(Inputs section의 목록을 참조한다). 유효하다면 바이트를 검증한다. bytes가 각 타입에 대해 모두 유효하기만 하면 하나의 pure value를 여러 타입과 함께 사용할 수 있다. 예를 들어 문자열은 ASCII string인 std::ascii::String으로도, UTF8 string인 std::string::String으로도 사용할 수 있다. 그러나 pure value를 가변 borrow한 뒤에는 타입이 고정되며, 이후의 모든 사용은 그 타입으로 이뤄져야 한다.

Publish 및 upgrade rule​

두 command는 module bytes와 dependency ID를 command 구조 안에 직접 embedded한다. 하지만 이 값들은 PTB Argument 값이 아니다.

Publish:

  • 하나의 sui::package::UpgradeCap을 반환한다.
  • 패키지가 staged된 뒤 runtime은 각 모듈의 init 함수가 있으면 제공된 모듈 순서(직렬화된 bytes의 vector 순서)대로 호출한다. init은 &mut TxContext와 선택적으로 one-time witness를 받는다. 추가 init 인자는 아직 지원되지 않지만 계획되어 있다.
  • 패키지는 실행 중 init 함수에서 사용할 수 있다. 패키지는 init 함수가 실행되기 전에 staged된다.

Upgrade:

  • 정확히 하나의 PTB 인자, 즉 sui::package::UpgradeTicket(by value)을 받는다.
  • 하나의 sui::package::UpgradeReceipt를 반환한다.
  • init 함수를 호출하지 않는다. 업그레이드에서 새로 추가된 모듈은 init 함수를 가질 수 없다. 이 제한은 향후 릴리스에서 해제될 예정이다.
  • ticket의 module digest와 package ID가 정확히 일치해야 한다.
  • ticket에서 upgrade policy(compatible, additive, dep-only)를 enforcement한다.

Execution 종료​

실행 마지막에는 남아 있는 값을 검사하고 트랜잭션의 effect를 계산한다.

For 입력:

  • 남아 있는 immutable 또는 read only 입력 객체는 건너뛴다(수정이 일어나지 않음).

  • 남아 있는 mutable 입력 객체는 원래 owner에게 반환된다(shared는 shared로, owned는 owned로 남는다).

  • 남아 있는 pure 입력 value는 drop된다(허용되는 타입은 모두 copy와 drop을 가진다).

  • 공유 객체는 삭제되거나 다시 공유되기만 한다. 그 외 operation(wrap, transfer, freezing)은 error를 일으킨다.

For 결과:

  • drop ability를 가진 남은 결과는 drop된다.

  • 값에 copy는 있지만 drop이 없다면 마지막 사용은 값에 의한 사용(이동으로 취급)이어야 한다.

  • 그렇지 않으면 drop이 없는 unused value에 대한 error가 발생한다.

For gas:

실행 시작 시 가스 코인에서 차감된 남은 SUI는 owner가 바뀌었더라도 코인에 반환된다. 가능한 최대 gas는 실행 시작 시 차감되고, 사용되지 않은 gas는 끝에 반환된다(모두 SUI 기준). 가스 코인은 TransferObjects로만 값으로 꺼낼 수 있으므로 wrap되거나 삭제되지 않는다.

그 뒤 전체 effect(생성, 수정, 삭제된 객체)가 실행 계층 밖으로 전달되고 Sui network가 이를 적용한다.

Execution example 예시​

각 command의 실행을 따라가 보면 입력이 command를 통해 어떻게 흐르는지, 결과가 어떻게 누적되는지, 최종 트랜잭션 effect가 어떻게 결정되는지를 확인할 수 있다.

100 MIST를 지불하고 marketplace에서 item 2개를 산다고 가정해 보자. 하나는 자신이 보관하고 다른 하나와 남은 코인은 address 0x808에 있는 친구에게 보낸다.

{
inputs: [
Pure(/* @0x808 BCS bytes */ ...),
Object(SharedObject { /* Marketplace shared object */ id: market_id, ... }),
Pure(/* 100u64 BCS bytes */ ...),
]
commands: [
SplitCoins(GasCoin, [Input(2)]),
MoveCall("some_package", "some_marketplace", "buy_two", [], [Input(1), NestedResult(0, 0)]),
TransferObjects([GasCoin, NestedResult(1, 0)], Input(0)),
MoveCall("sui", "tx_context", "sender", [], []),
TransferObjects([NestedResult(1, 1)], NestedResult(3, 0)),
]
}

입력에는 친구의 address, marketplace 객체, 코인 분할 값이 포함된다. command는 코인을 분할하고, marketplace 함수를 호출하고, 가스 코인과 객체 하나를 보내고, 자신의 address를 가져온 뒤(sui::tx_context::sender를 통해), 남은 객체를 자신에게 전송한다.

Command 0: SplitCoins(GasCoin, [Input(2)])

가스 코인에 가변 reference로 접근하고 Input(2)를 100u64로 적재한다(이동이 아니라 복사). 새 코인을 만든다.

StateValue
Gas coin balance499,900 MIST
Result[Coin<SUI> { id: new_coin, value: 100 }]

Command 1: MoveCall("some_package", "some_marketplace", "buy_two", ...)

buy_two(marketplace: &mut Marketplace, coin: Coin<SUI>, ctx: &mut TxContext): (Item, Item)를 호출한다. Input(1)은 mutable reference로, NestedResult(0, 0)은 값으로 사용한다(코인이 이동되어 소비된다).

StateValue
Results[0][0]moved
Result[Item { id: id1 }, Item { id: id2 }]

Command 2: TransferObjects([GasCoin, NestedResult(1, 0)], Input(0))

가스 코인과 첫 번째 item을 0x808로 전송한다. 두 객체 모두 값으로 가져가며(이동된다).

StateValue
Gas coinmoved
Results[1][0]moved
Result[]

Command 3: MoveCall("sui", "tx_context", "sender", [], [])

sender(ctx: &TxContext): address를 호출한다. sender의 address를 반환한다.

StateValue
Result[sender_address]

Command 4: TransferObjects([NestedResult(1, 1)], NestedResult(3, 0))

두 번째 item을 sender에게 전송한다. item은 값으로 이동되고 address는 값으로 복사된다.

StateValue
Results[1][1]moved
Result[]

Initial state 상태​

Gas Coin: Coin<SUI> { id: gas_coin, balance: 1_000_000u64 }
Inputs: [Pure(@0x808), Marketplace { id: market_id }, Pure(100u64)]
Results: []

최대 가스 예산인 500_000이 차감된 뒤 상태는 다음과 같다:

Gas Coin: Coin<SUI> { id: gas_coin, balance: 500_000u64 }

Final state 상태​

Gas Coin: _ (moved)
Inputs: [Pure(@0x808), Marketplace { id: market_id } (mutated), Pure(100u64)]
Results: [
[_],
[_, _],
[],
[sender_address],
[]
]

End of execution check 확인​

  • Input 객체: 가스 코인은 이동되었다. Marketplace는 shared 상태로 남으며 수정되었다.

  • Results: 남아 있는 값은 모두 drop ability를 가진다(Pure 입력, sender address). 다른 결과는 모두 이동되었다.

  • Shared 객체: Marketplace는 이동되지 않았으며 shared 상태로 남는다.

Transaction effects:

  • gas에서 분할한 코인(new_coin)은 같은 트랜잭션 안에서 생성되고 삭제되므로 나타나지 않는다.

  • 가스 코인과 Item { id: id1 }은 0x808로 전송된다. 남은 gas는 owner 변경 여부와 관계없이 가스 코인으로 반환된다.

  • Item { id: id2 }는 sender에게 전송된다.

  • Marketplace 객체는 shared 상태로 반환되며 수정되었다.