PTB 개요
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또는publicMove 함수를 호출한다.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 타입:
u8u16u32u64u128u256booladdress
- 문자열로, 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를 반환한다.TransferObjects와MergeCoins는 어떤 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]라면,Object1은Input(0)으로,Pure1은Input(2).로 접근된다. -
GasCoin은 가스를 지불하는 데 사용되는SUI코인에 대한 object을 나타내는 특수 input 인수이다. 이는 가스 코인이 각 transaction마다 항상 존재하며 다른 input에는 없는 특별한 제약(아래 참조)이 있기 때문에 다른 input과 분리되어 유지된다. 또한 가스 코인을 분리하면 사용이 명시적이 되어, 스폰서는 전송자가 가스 이외의 목적으로 가스 코인을 사용하지 않기를 원할 수 있는 sponsored transaction에 유용하다.가스 코인은
TransferObjectscommand를 제외하고는 값으로(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]라면,Value1은NestedResult(1, 0)으로,Value2는NestedResult(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::UID에copyability가 없기 때문이다.
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 타입 T는 sui::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여야 하며,Pureinput 또는 result에서 올 수 있다. - 모든 argument, object과 address는 값으로 취해진다.
- 이 command는 어떤 result도 생성하지 않는다(빈 result vector).
- 이 command의 시그니처는 Move로 표현될 수 없지만, 대략
(vector<forall T: key + store. T>, address): ()시그니처를 가진다고 생각할 수 있으며, 여기서forall T: key + store. T는vector가 object의 heterogeneous vector임을 나타낸다.
SplitCoins
이 command는 SplitCoins(CoinArg, AmountArgs) 형태이며, CoinArg는 split되는 코인이고 AmountArgs는 분리할 금액의 vector이다.
- transaction이 서명될 때 네트워크는
AmountArgs가 비어 있지 않음을 검증한다. - coin argument
CoinArg: Argument는sui::coin::Coin<T>타입의 코인이어야 하며,T는 split되는 코인의 타입이다. 이는 어떤 코인 타입도 될 수 있으며SUI코인으로 제한되지 않는다. AmountArgs: [Argument]는u64값이어야 하며,Pureinput 또는 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>>와 유사하며, resultvector는amountsvector와 동일한 길이를 갖는 것이 보장된다.
MergeCoins
이 MergeCoins(CoinArg, ToMergeArgs) 형태이며, CoinArg는 ToMergeArgs 코인이 병합되는 대상 코인이다. 즉 여러 코인(ToMergeArgs)을 하나의 코인(CoinArg)으로 병합한다.
- transaction이 서명될 때 네트워크는
ToMergeArgs가 비어 있지 않음을 검증한다. - coin argument
CoinArg: Argument는sui::coin::Coin<T>타입의 코인이어야 하며,T는 병합되는 코인의 타입이다. 이는 어떤 코인 타입도 될 수 있으며SUI코인으로 제한되지 않는다. - coin arguments
ToMergeArgs: [Argument]는sui::coin::Coin<T>값이어야 하며, 여기서T는CoinArg와 동일한 타입이다. - 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가 비어 있을 때는 타입을 반드시 지정해야 한다.
- 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의 인수로 사용해야 한다. 요소를 개별적으로 접근하고 싶다면MoveCallcommand를 사용하여 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 함수의 시그니처에 따라 동적으로 결정된다.