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또는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