개발 이유 Sui Move

Sui 는 기존 Diem 설계를 개선한 최초의 블록체인으로 Move, 그리고 이러한 개선의 구체적인 사례를 공유합니다.

개발 이유 Sui Move

'Sui Move ' 용어는 더 이상 사용되지 않습니다. 대신 'Move on Sui' 또는 간단히 'Move' 용어를 사용하는 것이 좋습니다.

Move 는 2018년 Libra 프로젝트 초창기에 탄생했으며, 두 명의 Mysten 창립자(Evan과 저)도 Libra의 창립 팀에 속해 있었습니다. 새로운 언어를 만들기로 결정하기 전에 초기 리브라 팀은 기존 스마트 컨트랙트 사용 사례와 언어를 집중적으로 연구하여 개발자가 원하는 것이 무엇인지, 기존 언어가 제공하지 못하는 부분이 무엇인지 파악했습니다. 저희가 파악한 핵심 문제는 스마트 컨트랙트는 자산과 액세스 제어에 관한 것이지만, 초기 스마트 컨트랙트 언어에는 이 두 가지에 대한 유형/값 표현이 부족하다는 것이었습니다. Move 가설은 이러한 핵심 개념에 대한 최고 수준의 추상화를 제공한다면 스마트 컨트랙트의 안전성과 스마트 컨트랙트 프로그래머의 생산성을 모두 크게 향상시킬 수 있다는 것입니다. 당면한 작업에 적합한 어휘를 사용하면 모든 것이 달라집니다. 수년 동안 수많은 사람이 Move 의 설계와 구현에 기여했으며, 이 언어가 '웹3의 자바스크립트'라는 대담한 목표를 가지고 핵심 아이디어에서 플랫폼에 구애받지 않는 스마트 컨트랙트 언어로 발전해 왔습니다.

오늘, 저희는 Move 를 Sui 에 통합하는 이정표를 발표하게 되어 기쁘게 생각합니다. Sui Move 은 기능이 완벽하고 고급 도구가 지원되며 다음과 같은 광범위한 문서와 예제를 포함하고 있습니다:

  • Sui Move 객체를 사용한 프로그래밍 튜토리얼 시리즈
  • Sui Move 기본 사항, 디자인 패턴 및 샘플의 쿡북
  • Mysten Move 팀이 개발한 코드 이해 및 오류 진단을 지원하는 향상된 VSCode 플러그인!
  • Move 빌드, 테스트, 패키지 관리, 문서 생성 및 Move Prover와 통합 sui CLI
  • 대체 가능한 토큰, 대체 불가능한 토큰, 탈중앙화 금융, 게임 등 다양한 예시를 살펴볼 수 있습니다.

2021년 말에 Sui 작업을 시작하면서 Move 을 새롭게 살펴보고 초기 디자인 결정 중 노후화되지 않은 부분과 Move 을 개선하여 Sui의 고유한 기능을 활용할 수 있는 방법을 모두 반영했습니다. 이전에 Sui Move 의 새로운 기능에 대해 언어 수준에서 설명한 적이 있지만, 이러한 차이점을 도입하게 된 동기에 대해서는 자세히 다루지 않았습니다. 이 글의 나머지 부분에서는 예제 중심의 방식으로 이 질문에 대해 자세히 다룹니다.

잠깐만요, 다른 Move가 있다고요?

Move 는 크로스 플랫폼 임베디드 언어입니다. 핵심 언어 자체는 매우 간단합니다. 구조체, 정수, 주소와 같은 일반적인 개념은 있지만 계정, 트랜잭션, 시간, 암호화 등과 같은 블록체인에 특화된 개념은 없습니다. 이러한 기능은 Move 을 통합하는 블록체인 플랫폼에서 제공해야 합니다. 중요한 점은 이러한 블록체인은 Move의 자체 포크가 필요하지 않으며, 각 플랫폼은 동일한 Move VM, 바이트코드 검증기, 컴파일러, 증명자, 패키지 관리자 및 CLI를 사용하지만 이러한 핵심 구성 요소 위에 구축되는 코드를 통해 블록체인 전용 기능을 추가한다는 것입니다.

디엠은 Move 을 임베드한 최초의 블록체인이었으며, 이후 0L, 스타코인, 앱토스 등 Move 기반 체인들은 대부분 디엠 스타일의 접근 방식을 사용했습니다. Diem 스타일( Move )은 몇 가지 장점이 있지만, Diem의 허가된 특성과 Diem 블록체인의 특정 구현 세부 사항(특히 스토리지 모델)으로 인해 몇 가지 기본적인 스마트 콘트랙트 사용 사례의 구현이 어렵습니다. 특히, Move 및 Diem의 원래 설계는 대체 불가능한 토큰이 폭발적으로 인기를 끌기 전의 것으로, 대체 불가능한 토큰 관련 사용 사례를 구현하기 까다롭게 만드는 몇 가지 단점이 있습니다.

이 글에서는 원래 Diem 스타일의 Move 임베딩에 문제가 있음을 보여주는 세 가지 예시를 살펴보고 Sui Move 에서 이 문제를 어떻게 해결했는지 설명하겠습니다. Move 에 대한 기본적인 이해가 있다고 가정하지만, 프로그래밍 배경 지식이 있는 분이라면 누구나 핵심을 이해할 수 있기를 바랍니다.

원활한 대량 자산 생성

에셋을 대량으로 생성하고 배포할 수 있는 기능은 웹3.0 사용자를 온보딩하고 참여를 유도하는 데 매우 중요합니다. Twitch 스트리머가 기념 NFT를 배포하고 싶거나, 크리에이터가 특별 이벤트 티켓을 발송하고 싶거나, 게임 개발자가 모든 플레이어에게 새로운 아이템을 에어드롭하고 싶을 수도 있습니다.

다음은 디엠 스타일로 자산을 대량 발행하는 코드를 작성하려는 (실패한) 시도입니다 Move. 이 코드는 수신자 주소 벡터를 입력으로 받아 각 주소에 대한 자산을 생성하고 자산 전송을 시도합니다.

struct CoolAsset { id: GUID, creation_date: u64 } has key, store
public entry fun mass_mint(creator: &signer, recipients: vector<address>) {
  assert!(signer::address_of(creator) == CREATOR, EAuthFail);
  let i = 0;
  while (!vector::is_empty(recipients)) {
    let recipient = vector::pop_back(&mut recipients);
    assert!(exists<Account>(recipient), ENoAccountAtAddress);
    let id = guid::create(creator);
    let creation_date = timestamp::today();
    // error! recipient must be `&signer`, not `address`
    move_to(recipient, CoolAsset { id, creation_date })
}

Diem 스타일( Move)에서 글로벌 스토리지는 (주소, 유형 이름) 쌍으로 키가 지정됩니다. 즉, 모든 주소는 주어진 유형의 자산을 하나만 저장할 수 있습니다. 따라서 move_to(수신자, CoolAsset { ... } 를 전송하려고 합니다. 쿨에셋 아래에 저장하여 수신자 주소를 입력합니다.

그러나 이 코드는 다음 줄에서 컴파일에 실패합니다. move_to(수신자, ...). 핵심 문제는 Diem 스타일( Move)에서는 다음과 같은 유형의 값을 보낼 수 없다는 것입니다. 쿨에셋 주소로 A 아니면:

  1. A가 아닌 주소가 A에서 계정을 생성하기 위해 트랜잭션을 전송합니다.
  2. 의 소유자는 A 유형의 객체를 수신하도록 명시적으로 옵트인하도록 트랜잭션을 전송합니다. 쿨에셋

자산을 받는 데만 두 번의 트랜잭션이 필요합니다! 계정 생성을 신중하게 제한하고 스토리지 시스템의 제한으로 인해 계정이 너무 많은 자산을 보유하는 것을 방지해야 하는 허가형 시스템인 Diem에게는 이러한 방식이 적합했습니다. 그러나 이는 자산 분배를 온보딩 메커니즘으로 사용하거나, 이더리움 및 유사한 블록체인에서와 같이 일반적으로 자산이 사용자 간에 자유롭게 이동하도록 허용하려는 개방형 시스템에는 매우 제한적인 방식입니다[1].

이제 Sui Move 에서 동일한 코드를 살펴보겠습니다:

struct CoolAsset { id: VersionedID, creation_date: u64 } has key
public entry fun mass_mint(recipients: vector<address>, ctx: &mut TxContext) {
  assert!(tx_context::sender(ctx) == CREATOR, EAuthFail);
  let i = 0;
  while (!vector::is_empty(recipients)) {
    let recipient = vector::pop_back(&mut recipients);
    let id = tx_context::new_id(ctx);
    let creation_date = tx_context::epoch(); // Sui epochs are 24 hours
    transfer(CoolAsset { id, creation_date }, recipient)
  }
}

Sui Move의 객체 ID로 키가 지정된 전역 저장소입니다. 모든 구조체는 어빌리티는 전 세계적으로 고유해야 하는 "Sui 객체"입니다. id 필드에 입력합니다. 제한된 move_to 구성, Sui Move 소개 전송 프리미티브는 Sui 객체에서 사용할 수 있습니다. 내부적으로 이 프리미티브는 다음과 같이 매핑됩니다. id쿨에셋 를 글로벌 스토리지에 저장하고 메타데이터를 추가하여 해당 값을 소유한 주체가 수신자.

Sui 버전의 흥미로운 속성 mass_mint 다른 모든 트랜잭션(호출하는 다른 트랜잭션 포함)과 통신한다는 것입니다. mass_mint!). Sui 런타임은 이를 감지하고 합의가 필요 없는 비잔틴 일관된 브로드캐스트 "빠른 경로"를 통해 이 함수를 호출하는 트랜잭션을 전송합니다. 이러한 트랜잭션은 병렬로 커밋 및 실행될 수 있습니다! 프로그래머는 위의 코드를 작성하기만 하면 나머지는 런타임이 알아서 처리하므로 별도의 노력이 필요하지 않습니다.

미묘하게도, 위의 코드가 작동하더라도 이 코드의 Diem 변형은 그렇지 않습니다. exists<Account> 그리고 GUID::CREATE 호출은 다른 트랜잭션을 생성하는 다른 트랜잭션과 경합 지점을 만들 수 있습니다. GUID를 터치하거나 계정 리소스를 사용해야 합니다. 경우에 따라서는 경합 지점을 피하기 위해 Diem 스타일의 Move 코드를 다시 작성할 수도 있지만, 관용적으로 작성하는 많은 관용적 방식( Move )은 병렬 실행을 방해하는 미묘한 병목 현상을 유발합니다.

네이티브 자산 소유권 및 이전

실제로 컴파일 및 실행되는 해결 방법을 사용하여 Diem 스타일의 Move 코드를 확장해 보겠습니다. 이를 위한 관용적 방법은 "래퍼 패턴"입니다. Bob은 직접적으로 move_to a 쿨에셋 를 앨리스의 주소로 보내려면 앨리스에게 수신에 "옵트인"하도록 요청합니다. 쿨에셋의 래퍼 유형을 먼저 게시하는 것입니다. CoolAssetStore 컬렉션 유형()를 추가합니다. 앨리스는 이 작업을 수행하기 위해 opt_in 함수를 호출합니다. 그런 다음 Bob이 쿨에셋 그의 CoolAssetStore 앨리스의 CoolAssetStore.

이 코드에서 또 하나의 주름을 추가해 보겠습니다. 쿨에셋은 생성된 지 최소 30일이 지난 경우에만 양도할 수 있습니다. 이러한 종류의 정책은 투기꾼이 이벤트 티켓을 구매하거나 뒤집는 것을 방지하여 진성 팬이 합리적인 가격으로 티켓을 쉽게 구매할 수 있도록 하려는 스트리머에게 중요합니다.

struct CoolAssetStore has key {
  assets: Table<TokenId, CoolAsset>
}
public fun opt_in(addr: &signer) {
  move_to(addr, CoolAssetHolder { assets: table::new() }
}
public entry fun cool_transfer(
  addr: &signer, recipient: address, id: TokenId
) acquires CoolAssetStore {
  // withdraw
  let sender = signer::address_of(addr);
  assert!(exists<CoolAssetStore>(sender), ETokenStoreNotPublished);
  let sender_assets = &mut borrow_global_mut<CoolAssetStore>(sender).assets; 
  assert!(table::contains(sender_assets, id), ETokenNotFound);
	let asset = table::remove(&sender_assets, id);
  // check that 30 days have elapsed
  assert!(time::today() > asset.creation_date + 30, ECantTransferYet)
	// deposit
	assert!(exists<CoolAssetStore>(recipient), ETokenStoreNotPublished);
  let recipient_assets = &mut borrow_global_mut<CoolAssetStore>(recipient).assets; 
  assert!(table::contains(recipient_assets, id), ETokenIdAlreadyUsed);
  table::add(recipient_assets, asset)
}

이 코드는 작동합니다. 하지만 앨리스에서 밥으로 자산을 전송하는 간단한 목표를 달성하는 데는 꽤 복잡한 방법입니다! 다시 Sui Move 변형을 살펴봅시다:

public entry fun cool_transfer(
  asset: CoolAsset, recipient: address, ctx: &mut TxContext
) {
  assert!(tx_context::epoch(ctx) > asset.creation_date + 30, ECantTransferYet);
  transfer(asset, recipient)
}

이 코드는 훨씬 더 짧습니다. 여기서 주목해야 할 핵심 사항은 다음과 같습니다. cool_transfer항목 함수(트랜잭션을 통해 Sui 런타임에서 직접 호출할 수 있음을 의미)이지만, 매개변수 유형이 쿨에셋 를 입력으로 받습니다. Sui 런타임의 마법이 다시 작동하고 있습니다! 트랜잭션에는 작업하려는 개체 ID 집합과 Sui 런타임이 포함됩니다:

  • ID를 객체 값으로 변환합니다( BORROW_GLOBAL_MUT 그리고 table_remove 부분)을 추가합니다.
  • 트랜잭션의 발신자가 객체를 소유하고 있는지 확인합니다( 서명자::주소_of 부분 + 위의 관련 코드). 이 부분은 특히 흥미로운데, 곧 설명하겠습니다: Sui 에서 안전한 객체 소유권 소유권 확인은 런타임의 일부입니다!
  • 호출된 함수의 매개변수 유형과 비교하여 객체 값의 유형을 확인합니다. cool_transfer
  • 객체 값 및 기타 인수를 다음 매개 변수에 바인딩합니다. cool_transfer 함수를 호출합니다.

이를 통해 Sui Move 프로그래머는 로직의 "출금" 부분의 상용구를 건너뛰고 바로 흥미로운 부분인 30일 만료 정책 확인으로 넘어갈 수 있습니다. 마찬가지로 "입금" 부분도 크게 단순화하여 Sui Move 전송 구조체를 사용합니다. 마지막으로, 다음과 같은 래퍼 유형을 도입할 필요가 없습니다. CoolAssetStore 내부 컬렉션(아이디 인덱싱된 Sui 글로벌 스토리지)을 사용하면 주소에 지정된 유형의 값을 임의의 수만큼 저장할 수 있습니다.

또 한 가지 차이점은 Diem 스타일에는 5가지 방법이 있다는 것입니다. cool_transfer 는 중단(즉, 전송을 완료하지 않고 실패하여 사용자에게 가스 요금을 청구)할 수 있는 반면에 Sui Move cool_transfer 30일 정책을 위반한 경우 한 가지 방법으로만 취소할 수 있습니다.

객체 소유권 검사를 런타임으로 오프로드하면 인체공학적인 측면뿐만 아니라 안전성 측면에서도 큰 이점을 얻을 수 있습니다. 런타임 수준에서 이를 안전하게 구현하면 빌드 시 이러한 검사를 구현하는 실수(또는 완전히 잊어버리는 실수!)를 방지할 수 있습니다.

마지막으로 Sui Move 엔트리 포인트 함수 시그니처를 확인합니다. cool_transfer( asset: CoolAsset, ...) 는 함수가 수행할 작업에 대한 많은 정보를 제공합니다(불투명한 Diem 스타일 함수 서명과 대조적으로). 이 함수는 전송 권한을 요청하는 것으로 생각할 수 있습니다. 쿨에셋와는 다른 함수 f(asset: &mut CoolAsset, ...) 가 쓰기 권한(전송 권한은 아님)을 요청합니다. 쿨에셋g(asset: &CoolAsset, ...) 는 읽기 권한만 요청합니다.

이 정보는 함수 시그니처에서 직접 사용할 수 있으므로(실행이나 정적 분석이 필요하지 않습니다!) 지갑과 다른 클라이언트 도구에서 직접 사용할 수 있습니다. Sui 지갑에서 저희는 다음과 같은 작업을 진행 중입니다. 사람이 읽을 수 있는 서명 요청 이러한 구조화된 함수 서명을 활용하여 사용자에게 iOS/안드로이드 스타일의 권한 프롬프트를 제공합니다. 지갑은 "이 트랜잭션은 귀하의 지갑을 읽을 수 있는 권한을 요청합니다. 쿨에셋를 작성하고 자산 컬렉션를 클릭하고 콘서트 티켓. 계속하시겠습니까?".

사람이 읽을 수 있는 서명 요청은 지갑 사용자가 어떤 영향을 미칠지 이해하지 못한 채 무턱대고 거래에 서명해야 하는 기존의 많은 플랫폼(Diem 스타일 Move!을 사용하는 플랫폼 포함)에 존재하는 대규모 공격 벡터를 해결합니다. 저희는 지갑 경험을 덜 위험하게 만드는 것이 암호화폐 지갑의 주류 채택을 촉진하기 위한 핵심 단계라고 생각하며, 사람이 읽을 수 있는 서명 요청과 같은 기능을 활성화하여 이러한 목표를 지원하기 위해 Sui Move 을 설계했습니다.

이기종 자산 번들링

마지막으로 다양한 유형의 에셋을 묶는 예시를 살펴보겠습니다. 이는 매우 일반적인 사용 사례로, 프로그래머가 여러 유형의 NFT를 컬렉션으로 패키징하거나, 마켓플레이스에서 함께 판매할 아이템을 번들로 묶거나, 기존 아이템에 액세서리를 추가하고 싶을 수 있습니다. 구체적으로 다음과 같은 시나리오를 가정해 보겠습니다:

  • 앨리스가 정의한 캐릭터 게임에서 사용할 오브젝트
  • 앨리스는 나중에 생성된 다양한 유형의 타사 액세서리로 캐릭터의 액세서리를 지원하고자 합니다.
  • 누구나 액세서리를 만들 수 있어야 하지만, 액세서리의 소유자는 캐릭터 는 액세서리를 추가할지 여부를 결정해야 합니다.
  • 전송 캐릭터 는 모든 액세서리를 자동으로 전송해야 합니다.

이번에는 Sui Move 코드부터 시작하겠습니다. Sui 런타임에 내장된 객체 소유권 기능의 또 다른 측면을 활용하겠습니다: 객체가 다른 객체에 의해 소유될 수 있습니다.. 모든 객체에는 고유한 소유자가 있지만 부모 객체는 임의의 수의 자식 객체를 가질 수 있습니다. 부모/자식 객체 관계는 전송_투_객체 함수의 형제인 전송 함수를 소개합니다.

// in the Character module, created by Alice
struct Character has key {
  id: VersionedID,
  favorite_color: u8,
  strength: u64,
  ...
}
/// The owner of `c` can choose to add `accessory`
public entry fun accessorize<T: key>(c: &mut Character, accessory: T) {
  transfer_to_object(c, accessory)
}
// ... in a module added later by Bob
struct SpecialShirt has key {
  id: VersionedID,
  color: u8
}
public entry fun dress(c: &mut Character, s: Shirt) {
  // a special shirt has to be the character's favorite color
  assert!(character::favorite_color(c) == s.color, EBadColor);
  character::accessorize(c, shirt)
}
// ... in a  module added later by Clarissa
struct Sword has key {
  id: VersionedID,
  power: u64
}
public entry fun equip(c: &mut Character, s: Sword) {
  // a character must be very strong to use a powerful sword
  assert!(character::strength(c) > sword.power * 2, ENotStrongEnough);
  character::accessorize(c, s)
}

이 코드에서 캐릭터 모듈에는 액세서리 함수를 사용하여 캐릭터의 소유자가 임의의 유형의 액세서리 객체를 자식 객체로 추가할 수 있습니다. 이를 통해 밥과 클라리사는 앨리스가 예상하지 못한 다른 속성과 기능을 가진 자신만의 액세서리 유형을 만들 수 있지만 앨리스가 이미 수행한 작업을 기반으로 구축할 수 있습니다. 예를 들어, 밥의 셔츠는 캐릭터가 좋아하는 색상인 경우에만 장착할 수 있고, 클라리사의 검은 캐릭터가 검을 휘두를 수 있을 만큼 강할 때만 사용할 수 있습니다.

Diem 스타일( Move)에서는 이러한 종류의 시나리오를 구현할 수 없습니다. 다음은 부족한 구현 전략에 대한 몇 가지 시도입니다:

// attempt 1
struct Character {
  // won't work because every Accessory would need to be the same type + have
  // the same fields. There is no subtyping in Move.
  // Bob's shirt needs a color, and Clarissa's sword needs power--no standard
  // representation of Accessory can anticipate everything devs will want to
  // create
  accessories: vector<Accessory>
}
// attempt 2
struct Character {
  // perhaps Alice anticipates the need for a Sword and a Shirt up front...
  sword: Option<Sword>,
  shirt: Option<Shirt>
  // ...but what happens when Daniel comes along later and wants to add Pants?
}
// attempt 3
// Does not support accessory compositions. For example: how do we represent a 
// Character with Pants and a Shirt, but no Sword?
struct Shirt { c: Character }
struct Sword { s: Shirt }
struct Pants { s: Sword }

주요 문제점은 디엠 스타일 Move:

  • 첫 번째 시도에서 알 수 있듯이 균질 한 컬렉션 만 지원되지만 액세서리는 근본적으로 이질적입니다.
  • 객체 간의 연결은 "래핑"(즉, 다른 객체 안에 객체를 저장하는 것)을 통해서만 생성할 수 있지만, 래핑할 수 있는 객체 집합을 미리 정의하거나(두 번째 시도에서처럼) 액세서리 구성을 지원하지 않는 임시 방식으로 추가해야 합니다(세 번째 시도에서처럼).

결론

Sui 은 Move 을 사용하는 방식에서 오리지널 Diem 설계와 크게 달라진 최초의 플랫폼입니다. Move 와 플랫폼의 고유한 기능을 완전히 활용하는 임베딩을 설계하는 것은 Move 언어와 기본 블록체인의 기능에 대한 깊은 이해가 필요한 예술이자 과학입니다. 저희는 Sui Move 의 발전과 이를 통해 구현될 새로운 사용 사례에 대해 매우 기대하고 있습니다!

[1] "특정 유형의 자산을 수신하려면 반드시 옵트인해야 한다"는 Diem 스타일의 Move 정책을 지지하는 또 다른 주장은 스팸 방지를 위한 좋은 메커니즘이라는 것입니다. 하지만 저희는 스팸 방지는 애플리케이션 계층에 속한다고 생각합니다. 사용자에게 자산 수령에 동의하기 위해 실제 비용이 드는 트랜잭션을 보내도록 요청하는 대신, 풍부한 사용자 정의 정책과 자동화된 스팸 필터를 통해 지갑 수준에서 스팸을 쉽게 처리할 수 있습니다.