본문으로 건너뛰기

Programmable Transaction Blocks

Sui에서 transaction은 자산의 흐름에 대한 기본 기록 그 이상이다. Sui의 transaction은 입력에 대해 실행되어 transaction의 결과를 정의하는 여러 command로 구성된다. programmable transaction block (PTB)로 명명된 이러한 command 그룹은 Sui의 모든 사용자 transaction을 정의한다. PTB를 사용하면 사용자가 새로운 Move 패키지를 publish하지 않고도 단일 transaction에서 여러 Move 함수를 호출하고, 자신의 object를 관리하며, 자신의 코인을 관리할 수 있다. 자동화와 transaction builder를 염두에 두고 설계된 PTB는 transaction을 생성하는 가볍고 유연한 방식이다. 그러나 루프와 같은 더 복잡한 프로그래밍 패턴은 지원되지 않으며, 그러한 경우에는 새로운 Move 패키지를 publish해야 한다.

언급한 대로, 각 PTB는 개별 transaction command로 구성된다(때로는 단순히 transaction 또는 command라고도 한다). 각 transaction command는 순서대로 실행되며, 한 transaction command의 결과를 이후의 어떤 transaction command에서도 사용할 수 있다. 블록 내의 모든 transaction command의 효과, 구체적으로는 object 수정 또는 transfer는 transaction의 끝에서 원자적으로 적용된다. 하나의 transaction command가 실패하면 전체 블록이 실패하며 command의 어떤 효과도 적용되지 않는다.

PTB는 단일 실행에서 최대 1,024개의 고유 operation을 수행할 수 있는 반면, 전통적인 블록체인의 transaction은 동일한 결과를 달성하기 위해 1,024개의 개별 실행이 필요하다. 이 구조는 더 저렴한 가스 수수료도 촉진한다. 개별 transaction을 촉진하는 비용은 동일한 transaction을 PTB로 함께 묶는 비용보다 항상 더 크다.

이 주제의 나머지 부분은 transaction command 실행의 의미론을 다룬다. 이는 Sui object 모델과 Move 언어에 대한 친숙함을 가정한다. 해당 주제에 대한 자세한 내용은 다음 문서를 참조하라:

Transaction type

실행 의미론과 관련된 PTB의 부분은 두 가지이다. transaction 전송자 또는 가스 limit과 같은 다른 transaction 정보가 참조될 수 있지만 범위 밖이다. PTB의 구조는 다음과 같다:

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

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

  • inputs 값은 인수의 vector인 [Input]이다. 이러한 인수는 object이거나 command에서 사용할 수 있는 pure value이다. object은 전송자가 소유하거나 shared/immutable object이다. pure value는 u64 또는 String 값과 같은 단순 Move 값을 나타내며, 해당 bytes로부터 순수하게 구성할 수 있다. 역사적 이유로 Rust 구현에서 Input은 CallArg이다.
  • commands 값은 command의 vector인 [Command]이다. 가능한 command는 다음과 같다:
    • TransferObjects는 하나 이상의 여러 object을 지정된 address로 전송한다.
    • SplitCoins는 하나의 코인에서 하나 이상의 여러 코인을 분리한다. 이는 어떤 sui::coin::Coin<_> object이든 될 수 있다.
    • MergeCoins는 하나 이상의 여러 코인을 하나의 코인으로 병합한다. 모든 sui::coin::Coin<_> object은 병합될 수 있으며, 모두 동일한 타입이어야 한다.
    • MakeMoveVec는 Move value의 vector(잠재적으로 빈 vector)를 생성한다. 이는 주로 MoveCall에 대한 인수로 사용될 Move value vector를 구성하는 데 사용된다.
    • MoveCall은 publish된 패키지의 entry 또는 public Move 함수를 호출한다.
    • Publish는 새로운 패키지를 생성하고 패키지의 각 module에 대해 init 함수를 호출한다.
    • Upgrade는 기존 패키지를 upgrade한다. upgrade는 해당 패키지의 sui::package::UpgradeCap을 요구한다.

Inputs and results

Inputs와 results는 transaction command에서 사용할 수 있는 두 가지 타입의 값이다. Inputs는 PTB에 제공되는 값이며, results는 PTB command에 의해 생성되는 값이다. Inputs는 object 또는 단순 Move value이며, results는 임의의 Move value(object 포함)이다.

Inputs와 results는 값의 배열을 채우는 것으로 볼 수 있다. Inputs의 경우 단일 배열이 있지만, results의 경우 각 개별 transaction command마다 하나의 배열이 있어 result value의 2D-array를 생성한다. 이 값들은 borrowing(가변 또는 불변), copying(타입이 허용하는 경우), 또는 moving(재인덱싱 없이 배열에서 값을 꺼냄)으로 접근할 수 있다.

Inputs

PTB에 대한 Input 인수는 크게 object 또는 pure value로 분류된다. 이 인수의 직접 구현은 종종 transaction builder 또는 SDK에 의해 가려진다. 이 섹션은 input 목록인 [Input]을 지정할 때 Sui 네트워크가 필요로 하는 정보 또는 데이터를 설명한다. 각 Input은 object인 Input::Object(ObjectArg)이거나(사용 중인 object을 지정하기 위한 필수 메타데이터를 포함함), pure value인 Input::Pure(PureArg)이며(값의 bytes를 포함함) 둘 중 하나이다.

object input의 경우, 필요한 메타데이터는 ownership of the object 타입에 따라 다르다. ObjectArg enum의 데이터는 다음과 같다:

object이 address에 의해 소유되었거나(또는 immutable이라면), ObjectArg::ImmOrOwnedObject(ObjectID, SequenceNumber, ObjectDigest)를 사용하라. 이 triple은 각각 object의 ID, sequence number(버전으로도 알려짐), 그리고 object 데이터의 digest를 지정한다.

object이 shared라면, Object::SharedObject { id: ObjectID, initial_shared_version: SequenceNumber, mutable: bool }를 사용하라. ImmOrOwnedObject와 달리 shared object의 버전과 digest는 네트워크의 합의 프로토콜에 의해 결정된다. initial_shared_version은 object이 처음 shared 되었을 때의 버전이며, 합의가 해당 object을 포함하는 transaction을 아직 보지 못했을 때 사용된다. 모든 shared object은 mutate될 수 있지만, mutable 플래그는 이 transaction에서 해당 object을 가변으로 사용할지 여부를 나타낸다. mutable 플래그가 false로 설정된 경우 object은 read-only이며, 시스템은 다른 read-only transaction을 병렬로 스케줄할 수 있다.

object이 다른 object에 의해 소유되는 경우(예: TransferObjects command 또는 sui::transfer::transfer을 통해 object의 ID로 전송된 경우), ObjectArg::Receiving(ObjectID, SequenceNumber, ObjectDigest)를 사용하라. object 데이터는 ImmOrOwnedObject 경우와 동일하다.

pure input의 경우 제공되는 유일한 데이터는 BCS bytes이며, 이는 Move value를 구성하기 위해 역직렬화된다. 모든 Move value가 BCS bytes로부터 구성될 수 있는 것은 아니다. 이는 bytes가 특정 Move 타입에 대해 기대되는 레이아웃과 일치하더라도, 해당 타입이 Pure value로 허용되는 타입 중 하나가 아니라면 그 타입의 값으로 역직렬화될 수 없음을 의미한다. 다음 타입은 pure value로 사용할 수 있도록 허용된다:

  • 모든 primitive 타입:
    • u8
    • u16
    • u32
    • u64
    • u128
    • u256
    • bool
    • address
  • 문자열로, ASCII 문자열(std::ascii::String) 또는 UTF8 문자열(std::string::String) 중 하나이다. 어느 경우든 bytes는 해당 인코딩으로 유효한 문자열인지 검증된다.
  • object ID sui::object::ID.
  • vector<T>로, T는 pure input에 대해 유효한 타입이며 재귀적으로 검사된다.
  • option인 std::option::Option<T>로, T는 pure input에 대해 유효한 타입이며 재귀적으로 검사된다.

흥미롭게도, bytes는 MoveCall 또는 MakeMoveVec에서와 같이 command에서 타입이 지정될 때까지 검증되지 않는다. 이는 주어진 pure input이 여러 타입의 Move value를 인스턴스화하는 데 사용될 수 있음을 의미한다. 자세한 내용은 Arguments section을 참조하라.

Results

각 transaction command는 값의 (비어 있을 수도 있는) 배열을 생성한다. 값의 타입은 임의의 Move 타입이 될 수 있으므로, input과 달리 값은 object 또는 pure value로 제한되지 않는다. 생성되는 result의 수와 그 타입은 각 transaction command에 특화된다. 각 command의 구체 사항은 해당 command의 섹션에서 찾을 수 있지만, 요약하면 다음과 같다:

  • MoveCall: result의 수와 타입은 호출되는 Move 함수에 의해 결정된다. 참조를 반환하는 Move 함수는 현재 지원되지 않는다.
  • SplitCoins: 하나의 코인에서 하나 이상의 여러 코인을 생성한다. 각 coin의 타입은 sui::coin::Coin<T>이며, 특정 코인 타입 T는 split되는 코인과 일치한다.
  • Publish: 새로 publish된 패키지에 대한 upgrade capability인 sui::package::UpgradeCap을 반환한다.
  • Upgrade: upgrade된 패키지에 대한 upgrade receipt인 sui::package::UpgradeReceipt를 반환한다.
  • TransferObjectsMergeCoins는 어떤 result도 생성하지 않는다(빈 result vector).

Argument structure and usage

각 command는 사용되는 input 또는 result를 지정하는 Argument를 받는다. 사용 방식(참조 또는 값)은 인수의 타입과 command의 기대 인수에 따라 추론된다. 먼저 Argument enum의 구조를 살펴보라.

  • Input(u16)는 input 인수이며, u16은 input vector에서 input의 인덱스이다. 예를 들어 input vector가 [Object1, Object2, Pure1, Object3]라면, Object1Input(0)으로, Pure1Input(2).로 접근된다.

  • GasCoin은 가스를 지불하는 데 사용되는 SUI 코인에 대한 object을 나타내는 특수 input 인수이다. 이는 가스 코인이 각 transaction마다 항상 존재하며 다른 input에는 없는 특별한 제약(아래 참조)이 있기 때문에 다른 input과 분리되어 유지된다. 또한 가스 코인을 분리하면 사용이 명시적이 되어, 스폰서는 전송자가 가스 이외의 목적으로 가스 코인을 사용하지 않기를 원할 수 있는 sponsored transaction에 유용하다.

    가스 코인은 TransferObjects command를 제외하고는 값으로(by-value) 가져갈 수 없다. 가스 코인의 owned 버전이 필요하다면, 먼저 SplitCoins를 사용해 단일 코인을 split할 수 있다.

    이 제한은 실행이 끝날 때 남은 가스가 코인으로 반환되기 쉽게 하기 위해 존재한다. 즉, 가스 코인이 wrapped되거나 삭제되었다면 초과 가스가 반환될 명확한 위치가 없게 된다. 자세한 내용은 Execution section을 참조하라.

  • NestedResult(u16, u16)는 이전 command의 값을 사용한다. 첫 번째 u16 은 command vector에서 command의 인덱스이고, 두 번째 u16은 해당 command의 result vector에서 result의 인덱스이다. 예를 들어 command vector가 [MoveCall1, MoveCall2, TransferObjects]이고 MoveCall2 의 result vector가 [Value1, Value2]라면, Value1NestedResult(1, 0)으로, Value2NestedResult(1, 1)로 접근된다.

  • Result(u16)NestedResult의 특수 형태로, Result(i)는 대략 NestedResult(i, 0)와 동등하다. 그러나 NestedResult(i, 0)와 달리 Result(i)는 인덱스 i의 result 배열이 비어 있거나 값이 둘 이상이면 에러가 발생한다. Result의 궁극적 의도는 전체 result 배열에 접근하도록 하는 것이지만, 아직 지원되지 않는다. 따라서 현재 상태에서는 모든 상황에서 Result 대신 NestedResult를 사용할 수 있다.

Execution

PTB 실행에서 input vector는 input object 또는 pure value bytes로 채워진다. 그런 다음 transaction command가 순서대로 실행되고, results는 result vector에 저장된다. 마지막으로 transaction의 효과가 원자적으로 적용된다. 다음 섹션은 실행의 각 측면을 더 자세히 설명한다.

Start of execution

실행 시작 시 PTB 런타임은 이미 로드된 input object을 가져와 input 배열에 로드한다. object은 이미 네트워크에 의해 검증되어 존재 여부와 유효한 소유권 같은 규칙을 확인한다. pure value bytes도 배열에 로드되지만, 사용될 때까지 검증되지 않는다.

이 단계에서 가장 중요한 점은 가스 코인에 대한 효과이다. 실행 시작 시 최대 가스 예산(SUI 기준)이 가스 코인에서 인출된다. 사용되지 않은 가스는 실행 종료 시 코인의 소유자가 변경되었더라도 가스 코인으로 반환된다.

Executing a transaction command

각 transaction command는 이후 순서대로 실행된다. 먼저 모든 command에 공통인 argument 관련 규칙을 살펴보라.

Arguments

각 argument는 참조(by-reference) 또는 값(by-value)으로 사용할 수 있다. 사용 방식은 argument의 타입과 command의 타입 시그니처에 기반한다.

  • 시그니처가 &mut T를 기대한다면, 런타임은 argument의 타입이 T인지 확인한 뒤 가변으로 borrow한다.
  • 시그니처가 &T를 기대한다면, 런타임은 argument의 타입이 T인지 확인한 뒤 불변으로 borrow한다.
  • 시그니처가 T를 기대한다면, 런타임은 argument의 타입이 T인지 확인한 뒤 T: copy이면 copy하고, 그렇지 않으면 move한다. Sui의 어떤 object도 copy를 가지지 않는데, 이는 모든 object에 존재하는 고유 ID 필드 sui::object::UIDcopy ability가 없기 때문이다.

argument가 move된 후에 어떤 형태로든 다시 사용되면 transaction은 실패한다. move된 후 argument를 그 위치(input 또는 result 인덱스)로 복원할 방법은 없다.

argument가 copy되었지만 drop ability가 없다면, 마지막 사용은 move로 추론된다. 그 결과, argument가 copy를 가지지만 drop이 없다면 마지막 사용은 반드시 값으로(by value)여야 한다. 그렇지 않으면 drop이 없는 값이 사용되지 않았기 때문에 transaction은 실패한다.

argument borrowing에는 참조를 통한 고유하고 안전한 사용을 보장하기 위한 다른 규칙도 있다. argument가 다음과 같다면:

  • 가변으로 borrow되면, 미해결(outstanding) borrow가 없어야 한다. 미해결 가변 borrow가 있는 상태에서의 중복 borrow는 dangling reference(유효하지 않은 메모리를 가리키는 참조)를 초래할 수 있다.
  • 불변으로 borrow되면, 미해결 가변 borrow가 없어야 한다. 중복 불변 borrow는 허용된다.
  • Move되면, 미해결 borrow가 없어야 한다. borrow된 값을 move하면 미해결 borrow가 dangling되어 안전하지 않게 된다.
  • Copied되면, 가변 또는 불변 여부와 관계없이 미해결 borrow가 있어도 된다. 일부 경우 예상치 못한 결과를 낳을 수는 있지만 안전성 문제는 없다.

object input은 기대하는 대로 해당 object의 타입 T를 가진다. 그러나 ObjectArg::Receiving input의 경우 object 타입 Tsui::transfer::Receiving<T>로 wrapped된다. 이는 object이 전송자가 아니라 다른 object에 의해 소유되기 때문이다. 그리고 그 부모 object로 소유권을 증명하기 위해 sui::transfer::receive 함수를 호출하여 wrapper를 제거한다.

GasCoin은 값으로(by-value) 사용(moved)하는 데 특별한 제약이 있다. TransferObjects command로만 값으로(by-value) 사용할 수 있다.

shared object도 값으로(by-value) 사용에 제약이 있다. 이 제약은 transaction 끝에서 shared object이 여전히 shared이거나 삭제되었음을 보장하기 위해 존재한다. shared object은 unshared(소유권 변경)될 수 없고 wrapped될 수도 없다. shared object은 다음과 같다:

  • mutable이 아닌 것으로 표시되어(read-only로 사용되는 경우) 값으로 사용할 수 없다.
  • transfer되거나 freeze될 수 없다. 그러나 이러한 검사는 동적으로 수행되지 않고 transaction의 끝에서만 수행된다. 예를 들어 TransferObjects는 shared object이 전달되면 성공하지만, 실행 끝에서 transaction이 실패한다.
  • wrapped될 수 있고 transient하게 dynamic field가 될 수 있지만, transaction 끝까지는 다시 shared되거나 삭제되어야 한다.

pure value는 사용될 때까지 타입 체크가 수행되지 않는다. pure value가 타입 T를 가지는지 확인할 때, 먼저 T가 pure value에 대해 유효한 타입인지(이전 목록 참조) 확인한다. 유효하다면 bytes를 검증한다. bytes가 각 타입에 대해 유효하기만 하면 여러 타입으로 pure value를 사용할 수 있다. 예를 들어 문자열을 ASCII 문자열 std::ascii::String과 UTF8 문자열 std::string::String으로 사용할 수 있다. 그러나 pure value를 가변으로 borrow한 후에는 타입이 고정되며, 이후 모든 사용은 해당 타입이어야 한다.

TransferObjects

이 command는 TransferObjects(ObjectArgs, AddressArg) 형태이며 ObjectArgs는 object의 vector이고 AddressArg는 object이 전송되는 address이다.

  • ObjectArgs: [Argument] 는 object이어야 하지만, object이 같은 타입일 필요는 없다.
  • address argument AddressArg: Argument는 address여야 하며, Pure input 또는 result에서 올 수 있다.
  • 모든 argument, object과 address는 값으로 취해진다.
  • 이 command는 어떤 result도 생성하지 않는다(빈 result vector).
  • 이 command의 시그니처는 Move로 표현될 수 없지만, 대략 (vector<forall T: key + store. T>, address): () 시그니처를 가진다고 생각할 수 있으며, 여기서 forall T: key + store. Tvector가 object의 heterogeneous vector임을 나타낸다.

SplitCoins

이 command는 SplitCoins(CoinArg, AmountArgs) 형태이며, CoinArg는 split되는 코인이고 AmountArgs는 분리할 금액의 vector이다.

  • transaction이 서명될 때 네트워크는 AmountArgs가 비어 있지 않음을 검증한다.
  • coin argument CoinArg: Argumentsui::coin::Coin<T> 타입의 코인이어야 하며, T는 split되는 코인의 타입이다. 이는 어떤 코인 타입도 될 수 있으며 SUI 코인으로 제한되지 않는다.
  • AmountArgs: [Argument]u64 값이어야 하며, Pure input 또는 result에서 올 수 있다.
  • coin argument CoinArg는 가변 참조로 취해진다.
  • amount arguments AmountArgs는 값으로 취해지며(copy된다).
  • command의 result는 코인의 vector인 sui::coin::Coin<T>이다. 코인 타입 T는 split되는 코인과 동일하며, result 수는 argument 수와 일치한다.
  • Move로 표현된 대략적 시그니처로는 <T: key + store>(coin: &mut sui::coin::Coin<T>, amounts: vector<u64>): vector<sui::coin::Coin<T>>와 유사하며, result vectoramounts vector와 동일한 길이를 갖는 것이 보장된다.

MergeCoins

MergeCoins(CoinArg, ToMergeArgs) 형태이며, CoinArgToMergeArgs 코인이 병합되는 대상 코인이다. 즉 여러 코인(ToMergeArgs)을 하나의 코인(CoinArg)으로 병합한다.

  • transaction이 서명될 때 네트워크는 ToMergeArgs가 비어 있지 않음을 검증한다.
  • coin argument CoinArg: Argumentsui::coin::Coin<T> 타입의 코인이어야 하며, T는 병합되는 코인의 타입이다. 이는 어떤 코인 타입도 될 수 있으며 SUI 코인으로 제한되지 않는다.
  • coin arguments ToMergeArgs: [Argument]sui::coin::Coin<T> 값이어야 하며, 여기서 TCoinArg와 동일한 타입이다.
  • coin argument CoinArg는 가변 참조로 취해진다.
  • merge arguments ToMergeArgs는 값으로 취해진다(move된다).
  • 이 command는 어떤 result도 생성하지 않는다(빈 result vector).
  • Move로 표현된 대략적 시그니처로는 <T: key + store>(coin: &mut sui::coin::Coin<T>, to_merge: vector<sui::coin::Coin<T>>): ()와 유사하다.

MakeMoveVec

이 command는 MakeMoveVec(VecTypeOption, Args) 형태이며, VecTypeOption은 구성되는 vector의 요소 타입을 지정하는 선택적 인수이고 Args는 vector의 요소로 사용될 인수의 vector이다.

  • transaction이 서명될 때 네트워크는 Args의 빈 vector에 대해 타입을 지정해야 한다면 그 타입이 지정되었는지 검증한다.
  • 타입 VecTypeOption: Option<TypeTag>는 구성되는 vector 요소의 타입을 지정하는 선택적 인수이다. TypeTag는 vector 요소의 Move 타입, 즉 생성되는 vector<T>에서의 T이다.
    • object vector의 경우 T: key일 때 타입을 지정할 필요가 없다.
    • 타입이 object 타입이 아니거나 vector가 비어 있을 때는 타입을 반드시 지정해야 한다.
  • Args: [Argument]는 vector의 요소이다. arguments는 object, pure value, 또는 이전 command의 result를 포함해 어떤 타입이든 될 수 있다.
  • arguments Args는 값으로 취해진다. T: copy이면 copy되고, 그렇지 않으면 move된다.
  • 이 command는 타입 vector<T>single result를 생성한다. vector 요소는 NestedResult를 사용해 개별적으로 접근할 수 없다. 대신 전체 vector를 다른 command의 인수로 사용해야 한다. 요소를 개별적으로 접근하고 싶다면 MoveCall command를 사용하여 Move code 내부에서 수행할 수 있다.
  • 이 command의 시그니처는 Move로 표현될 수 없지만, 대략 (T...): vector<T> 시그니처를 가진다고 생각할 수 있으며, 여기서 T... 는 타입 T의 가변 개수 인수를 나타낸다.

MoveCall

이 command는 MoveCall(Package, Module, Function, TypeArgs, Args) 형태이며, Package::Module::Function은 호출되는 Move 함수를 지정하고, TypeArgs는 해당 함수에 대한 타입 인수의 vector이며, Args는 Move 함수에 대한 인수의 vector이다.

  • 패키지 Package: ObjectID는 호출되는 모듈을 포함하는 패키지의 Object ID이다.
  • 모듈 Module: String은 호출되는 함수를 포함하는 모듈의 이름이다.
  • 함수 Function: String은 호출되는 함수의 이름이다.
  • type arguments TypeArgs: [TypeTag] 는 호출되는 함수에 대한 타입 인수이며, 함수의 타입 파라미터 제약을 만족해야 한다.
  • arguments Args: [Argument]는 호출되는 함수에 대한 인수이며, 함수 시그니처에 지정된 파라미터에 대해 유효해야 한다.
  • 다른 command와 달리, arguments의 사용 방식과 result 수는 호출되는 Move 함수의 시그니처에 따라 동적으로 결정된다.

Publish

이 command는 Publish(ModuleBytes, TransitiveDependencies) 형태이며, ModuleBytes는 publish되는 module의 bytes이고 TransitiveDependencies는 링크할 패키지 Object ID dependency의 vector이다.

transaction이 서명될 때 네트워크는 ModuleBytes가 비어 있지 않음을 검증한다. ModuleBytes: [[u8]]는 publish되는 모듈의 bytes를 포함하며, 각 [u8] 요소는 하나의 모듈이다.

transitive dependencies TransitiveDependencies: [ObjectID]는 새 패키지가 의존하는 패키지의 Object ID이다. 각 모듈은 dependency로 사용하는 패키지를 나타내지만, transitive object ID는 해당 패키지의 버전을 선택하기 위해 제공되어야 한다. 즉, 이 object ID는 모듈에서 dependency로 표시된 패키지의 버전을 선택하는 데 사용된다.

패키지의 모듈이 검증된 후, 각 모듈의 init 함수가 module byte vector ModuleBytes와 동일한 순서로 호출된다.

이 command는 새로 publish된 패키지에 대한 upgrade capability인 sui::package::UpgradeCap 타입의 단일 result를 생성한다.

Upgrade

이 command는 Upgrade(ModuleBytes, TransitiveDependencies, Package, UpgradeTicket) 형태이며, Package는 upgrade되는 패키지의 object ID를 나타낸다. ModuleBytesTransitiveDependenciesPublish command와 유사하게 동작한다.

ModuleBytesTransitiveDependencies에 대한 자세한 내용은 Publish command를 참조하라. 다만 upgrade된 모듈에 대해서는 어떤 init 함수도 호출되지 않는다.

Package: ObjectID는 upgrade되는 패키지의 Object ID이다. 패키지는 존재해야 하며 최신 버전이어야 한다.

UpgradeTicket: sui::package::UpgradeTicket는 upgrade되는 패키지에 대한 upgrade ticket이며 sui::package::UpgradeCap으로부터 생성된다. ticket은 값으로 취해진다(move된다).

이 command는 upgrade에 대한 proof를 제공하는 sui::package::UpgradeReceipt 타입의 단일 result를 생성한다. upgrade에 대한 자세한 내용은 Upgrading Packages를 참조하라.

End of execution

실행 종료 시 남은 값이 검사되고 transaction에 대한 효과가 계산된다.

input에 대해 다음 검사가 수행된다:

  • 수정이 없으므로 남아 있는 immutable 또는 readonly input object은 건너뛴다.
  • 남아 있는 mutable input object은 원래 소유자에게 반환된다–shared였다면 shared로 남고, owned였다면 owned로 남는다.
  • 남아 있는 pure input value는 drop된다. pure input value는 허용되는 타입이 모두 copydrop을 가지므로 copydrop을 가져야 함을 유의하라.
  • 어떤 shared object에 대해서도 삭제되었거나 다시 shared되었는지만 확인해야 한다. 다른 어떤 operation(wrap, transfer, freezing 등)은 에러를 발생시킨다.

result에 대해 다음 검사가 수행된다:

  • drop ability를 가진 남아 있는 result는 drop된다.
  • 값이 copy는 있지만 drop이 없다면 마지막 사용은 값으로(by-value)였어야 한다. 그 방식으로 마지막 사용은 move로 취급된다.
  • 그렇지 않으면 drop이 없는 사용되지 않은 값이 있으므로 에러가 발생한다.

실행 시작 시 가스 코인에서 차감된 남은 SUI는 소유자가 변경되었더라도 코인으로 반환된다. 즉, 실행 시작 시 가능한 최대 가스가 차감된 다음, 실행 끝에서 사용되지 않은 가스가 반환된다(모두 SUI로). TransferObjects로만 가스 코인을 값으로(by-value) 취할 수 있으므로, 가스 코인은 wrapped되거나 삭제되지 않았을 것이다.

그 다음 총 효과(생성, 변경, 삭제된 object을 포함함)가 실행 레이어 밖으로 전달되며 Sui 네트워크에 의해 적용된다.

Example

PTB 실행의 예제를 따라가 보자. 이 예제는 모든 규칙을 완전히 보여주지는 않지만 실행의 전반적인 흐름을 보여준다.

100 MIST의 비용이 드는 두 개의 아이템을 marketplace에서 구매하려고 한다고 가정하라. 하나는 자신이 보유하고, object과 남은 코인을 address 0x808의 친구에게 전송한다. 이 모든 것을 하나의 PTB에서 수행할 수 있다:

{
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)),
]
}

inputs에는 친구의 address, marketplace object, 그리고 coin split에 대한 값이 포함된다. commands에서는 코인을 split하고, market place 함수를 호출하고, 가스 코인과 하나의 object을 전송하고,(sui::tx_context::sender를 통해) 자신의 address를 가져온 다음, 남은 object을 자신에게 전송한다. 단순화를 위해 문서는 패키지 이름을 이름으로 참조하지만, 실제로는 패키지의 Object ID로 참조된다는 점에 유의하라.

이를 따라가기 위해 먼저 가스 object, inputs, results에 대한 메모리 위치를 살펴보라.

Gas Coin: sui::coin::Coin<SUI> { id: gas_coin, balance: sui::balance::Balance<SUI> { value: 1_000_000u64 } }
Inputs: [
Pure(/* @0x808 BCS bytes */ ...),
some_package::some_marketplace::Marketplace { id: market_id, ... },
Pure(/* 100u64 BCS bytes */ ...),
]
Results: []

여기서는 두 개의 object이 로드되어 있으며, 값이 1_000_000u64인 가스 코인과 타입이 some_package::some_marketplace::Marketplace 인 marketplace object이 있다(이 이름과 표현은 이후 단순화를 위해 축약된다). pure argument는 로드되지 않았으며 BCS bytes로 존재한다.

각 command마다 가스가 차감되지만, 실행의 그 측면은 상세히 시연되지 않는다는 점에 유의하라.

Before commands: start of execution

실행 전에 가스 코인에서 가스 예산을 제거하라. 가스 예산이 500_000이라고 가정하면 가스 코인은 이제 값이 500_000u64가 된다.

Gas Coin: Coin<SUI> { id: gas_coin, ... value: 500_000u64 ... } // The maximum gas is deducted
Inputs: [
Pure(/* @0x808 BCS bytes */ ...),
Marketplace { id: market_id, ... },
Pure(/* 100u64 BCS bytes */ ...),
]
Results: []

이제 command를 실행할 수 있다.

Command 0: SplitCoins

첫 번째 command SplitCoins(GasCoin, [Input(2)])는 가스 코인에 가변 참조로 접근하고 Input(2)에 있는 Pure argument를 100u64u64 value로 로드한다. u64copy ability를 가지므로 Input(2)에 있는 Pure input은 move되지 않으며, 대신 bytes가 복사된다.

result로 새로운 코인 object이 생성된다.

이는 다음과 같은 업데이트된 메모리 위치를 제공한다:

Gas Coin: Coin<SUI> { id: gas_coin, ... value: 499_900u64 ... }
Inputs: [
Pure(/* @0x808 BCS bytes */ ...),
Marketplace { id: market_id, ... },
Pure(/* 100u64 BCS bytes */ ...),
]
Results: [
[Coin<SUI> { id: new_coin, value: 100u64 ... }], // The result of SplitCoins
],

Command 1: MoveCall

이제 command는 MoveCall(“some_package”, “some_marketplace”, “buy_two”, [], [Input(1), NestedResult(0, 0)])이다. arguments Input(1)NestedResult(0, 0)를 사용하여 함수 some_package::some_marketplace::buy_two를 호출한다. 이들이 어떻게 사용되는지를 판단하려면 함수의 signature를 살펴봐야 하며, 이 예제에서는 signature가 다음과 같다고 가정한다.

entry fun buy_two(
marketplace: &mut Marketplace,
coin: Coin<Sui>,
ctx: &mut TxContext,
): (Item, Item)

여기서 Item은 판매되는 두 object의 타입이다.

marketplace 파라미터의 타입이 &mut Marketplace이므로 Input(1)을 가변 참조로 사용한다. Marketplace object의 값에 일부 수정이 이루어진다고 가정하라. 그러나 코인 파라미터의 타입이 Coin<Sui>이므로 NestedResult(0, 0)을 값으로 사용한다. TxContext input은 런타임에 의해 자동으로 제공된다.

이는 _가 object이 move되었음을 나타내는 업데이트된 메모리 위치를 제공한다.

Gas Coin: Coin<SUI> { id: gas_coin, ... value: 499_900u64 ... }
Inputs: [
Pure(/* @0x808 BCS bytes */ ...),
Marketplace { id: market_id, ... }, // Any mutations are applied
Pure(/* 100u64 BCS bytes */ ...),
]
Results: [
[ _ ], // The coin was moved
[Item { id: id1 }, Item { id: id2 }], // The results from the Move call
],

buy_two가 자신의 Coin object argument를 삭제하고 BalanceMarketplace object로 transfer한다고 가정한다.

Command 2: TransferObjects

TransferObjects([GasCoin, NestedResult(1, 0)], Input(0))는 가스 코인과 첫 번째 item을 Input(0)의 address로 transfer한다. 모든 input은 by value이며, object는 copy를 가지지 않으므로 move된다. result는 생성되지 않지만 object의 소유권은 변경되며, 이는 메모리 위치가 아니라 transaction effects에서 확인된다.

이제 다음과 같은 업데이트된 메모리 위치를 가진다.

Gas Coin: _ // The gas coin is moved
Inputs: [
Pure(/* @0x808 BCS bytes */ ...),
Marketplace { id: market_id, ... },
Pure(/* 100u64 BCS bytes */ ...),
]
Results: [
[ _ ],
[ _ , Item { id: id2 }], // One item was moved
[], // No results from TransferObjects
],

Command 3: MoveCall

이번에는 sui::tx_context::sender에 대해 또 다른 Move call을 수행하며, 시그니처는 다음과 같다.

public fun sender(ctx: &TxContext): address

전송자의 address를 Pure input으로 직접 전달할 수도 있었지만, 이 예제는 PTB의 추가 유틸리티 일부를 호출하는 것을 보여주며, 이 함수는 entry 함수는 아니지만 모든 argument를 제공할 수 있기 때문에 public 함수 또한 호출할 수 있다. 이 경우 유일한 argument인 TxContext는 런타임에 의해 제공된다. 함수의 result는 전송자의 address이다. 이 값은 Pure input과 동일하게 취급되지 않으며, 타입은 address로 고정되고 호환되는 BCS 표현을 가지더라도 다른 타입으로 역직렬화될 수 없다는 점에 유의하라.

이제 다음과 같은 업데이트된 메모리 위치를 가진다.

Gas Coin: _
Inputs: [
Pure(/* @0x808 BCS bytes */ ...),
Marketplace { id: market_id, ... },
Pure(/* 100u64 BCS bytes */ ...),
]
Results: [
[ _ ],
[ _ , Item { id: id2 }],
[],
[/* senders address */ ...], // The result of the Move call
],

Command 4: TransferObjects

마지막으로 남은 item을 자신에게 전송하라. 이는 이전 TransferObjects command와 유사하다. 마지막 Item을 값으로 사용하고 전송자의 address도 값으로 사용한다. Item은 copy가 없으므로 move되고, addresscopy가 있으므로 copy된다.

최종 메모리 위치는 다음과 같다.

Gas Coin: _
Inputs: [
Pure(/* @0x808 BCS bytes */ ...),
Marketplace { id: market_id, ... },
Pure(/* 100u64 BCS bytes */ ...),
]
Results: [
[ _ ],
[ _ , _ ],
[],
[/* senders address */ ...],
[], // No results from TransferObjects
],

After commands: end of execution

실행 종료 시 런타임은 남아 있는 값(세 개의 inputs와 전송자의 address)을 검사한다. 다음은 effects가 생성되기 전에 수행되는 검사를 요약한 것이다:

  • 남아 있는 어떤 input object도 원래 소유자에게 반환되는 것으로 표시된다.
    • 가스 코인은 Moved되었다. 그리고 Marketplace는 동일한 owner를 유지하며 shared이다.
  • 남아 있는 모든 값은 drop을 가져야 한다.
    • Pure input은 인스턴스화할 수 있는 모든 타입이 drop을 가지므로 drop을 가진다.
    • 전송자의 address는 primitive 타입 addressdrop을 가지므로 drop을 가진다.
    • 다른 모든 results는 move되었다.
  • 남아 있는 어떤 shared object도 삭제되었거나 다시 shared되었어야 한다.
    • Marketplace object은 move되지 않았으므로 owner는 shared로 남는다.

이러한 검사들이 수행된 후 effects를 생성하라.

  • 가스 코인에서 split된 코인인 new_coin은 동일한 transaction에서 생성되고 삭제되었기 때문에 effects에 나타나지 않는다.
  • 가스 코인과 id1을 가진 item은 0x808로 transfer된다.
    • 가스 코인은 balance를 업데이트하기 위해 mutate된다. 최대 예산 500_000의 남은 가스는 owner가 변경되었더라도 가스 코인으로 반환된다.
    • id1을 가진 Item은 새로 생성된 object이다.
  • id2를 가진 item은 전송자의 address로 transfer된다.
    • id2를 가진 Item은 새로 생성된 object이다.
  • Marketplace object은 반환되고, shared로 남으며, mutate된다.
    • object은 shared로 남지만 그 내용은 mutate된다.