Libra — Move Language 기초편

Seungwon Go
ReturnValues
Published in
14 min readJun 23, 2019

By Seungwon Go, CEO & Founder at ReturnValues (seungwon.go@returnvalues.com)

Move는 Libra 블록체인을 위해 만들어진 새로운 프로그래밍 언어입니다.

그럼 리브라의 Move 는 어떤 언어일까요? 백서에서는 Move는 토큰 위변조, 이중 지불 등 비정상 행위를 Move 언어 자체에서 처음부터 방지한다고 나와있습니다. 그리고 전통적인 프로그래밍 언어의 변수 타입인 integer, boolean, string 등과 Asset의 성격은 근본적으로 다르기 때문에, 이에 걸맞는 새로운 변수 타입이 있어야 하고 Move 에서는 Asset을 위한 새로운 변수 타입을 제공합니다. 비트코인이나 이더리움 같은 기존의 블록체인의 경우 digital asset을 표현할 수 있는 변수 타입이 없고, 정수형 타입을 사용함으로써 가지는 많은 문제점이 있었습니다.

Key Features of Move

Move Transaction Scripts Enable Programmable Transactions

  • 리브라 블록체인의 트랜잭션 안에는 트랜잭션 스크립트를 포함하고 있습니다.
  • 트랜잭션 스크립트는 한개 이상의 Move 모듈의 프로시저를 호출함으로써 리브라 블록체인의 global storage에 있는 Move 리소스와 상호작용 합니다.
  • 트랜잭션 스크립트는 전역상태에 있지 않고, 다른 트랜잭션 스크립트가 실행 시킬 수 없습니다. 트랜잭션 스크립트는 single-use 프로그램입니다.

Move Modules Allow Composable Smart Contracts

Move 모듈은 리브라 블록체인의 전역상태를 업데이트 하기 위한 규칙을 정의 합니다. Move 모듈은 다른 블록체인(이더리움)의 스마트컨트랙트라고 보시면 됩니다. 모듈은 사용자 account에 퍼블리싱 할 수 있는 리소스 타입을 선언합니다. 리브라 블록체인에서 각각의 account는 임의의 수의 리소스와 모듈을 위한 컨테이너입니다.

  • 모듈은 struct 타입과 프로시저를 선언합니다.
  • Move 모듈의 프로시저는 선언된 타입에 대한 생성, 접근, 파괴를 위한 규칙을 정의 합니다.
  • 모듈은 재사용 가능합니다. 하나의 모듈에 선언된 struct 타입은 다른 모듈에서 struct 타입으로 선언해서 사용할 수 있습니다. 모듈은 다른 모듈에 선언된 프로시저를 호출 할 수 있습니다. 그리고 트랜잭션 스크립트는 다른 모듈의 퍼블릭 프로시저를 호출 할 수 있습니다.
  • 궁극적으로 리브라 사용자는 자신의 account 로 모듈을 퍼블리싱 할 수 있습니다.

Move Has First Class Resources

  • Move 언어의 중요 특징 중 하나는 커스텀 리소트 타입을 정의 할 수 있다는 것입니다.
  • 리소스는 데이터 구조로 저장되거나, 프로시저에 파라미터로 전달되거나, 프로 시저로 부터 반환 될 수 있습니다.
  • Move 리소스는 절대 복사되거나, 재사용되거나, 삭제되거나 하지 않습니다. 리소스 타입은 타입을 정의한 모듈에서만 생성하거나, 삭제할 수 있습니다. Move VM은 bytecode verifier를 통과하지 않은 코드는 실행하지 않습니다.
  • 리브라 currency는 LibraCoin.T.라는 리소스 타입으로 구현됩니다.

리브라를 설치하고 나면, libra폴더 밑에 language 폴더가 보입니다.

language 폴더 밑으로 bytecode_verifier, compiler, functional_tests, stdlib, vm 폴더가 보입니다.

이 5가지 디렉토리 구조에 대해서 Move 백서는 아래와 같이 설명하고 있습니다.

  • vm 폴더 : 가상머신 VM에는 바이트코드 포맷, 바이트코드 해석기, 그리고 트랜잭션 블록을 실행하기 위한 인프라가 포함되어 있습니다.
  • bytecode_verifier 폴더 : bytecode verifier는 잘못된 Move 바이트코드를 거절하기 위한 정적 분석도구를 포함하고 있습니다. VM은 새로운 Move 코드를 실행하기에 앞서 bytecode verifier를 실행합니다. 컴파일러는 bytecode verifier을 실행하고 오류를 프로그래머에게 표시합니다.
  • compiler 폴더 : Move IR(Intermediate Representation) compiler는 사람이 읽을 수 있는 프로그램 텍스트를 Move 바이트코드로 컴파일합니다. IR compiler는 테스팅 툴입니다.
  • stdlib 폴더 : standard libraryLibraAccount,LibraCoin. 같은 코어 시스템 모듈을 위한 Move IR 코드를 포함합니다.
  • functional_tests 폴더 : VM, bytecode verifier, compiler를 위한 테스트 폴더입니다. Move IR에 쓰여져서, 테스팅 프레임워크에 의해 실행됩니다. 이 테스트는 Move IR로 작성되고 주석으로 인코딩 된 특수 지시문에서 테스트를 실행할 때 예상되는 결과를 구문 분석하는 테스트 프레임 워크에 의해 실행됩니다.

이제 몇가지 코드를 보면서 Move 언어의 특징을 살펴보도록 하겠습니다.

Peer-to-Peer Payment Transaction Script

public main(payee: address, amount: u64) { 
let coin: 0x0.Currency.Coin = 0x0.Currency.withdraw_from_sender(copy(amount));
0x0.Currency.deposit(copy(payee), move(coin));
}

위의 메소드는 두개의 파라미터를 받게 구현되었습니다. 첫번째는 리브라 코인을 받을 수신자 주소(payee: address)이고, 두번째는 리브라 코인 금액(amount: u64) 입니다. 코드가 정확히 이해되지 않더라도, 수신자에게 일정 코인을 전송하는 메소드 임을 아실 수 있을것입니다.

위에 코드에서는 총 2개 스탭을 통해 메소드가 수행이 됩니다. 첫번째 라인을 보면,

let coin: 0x0.Currency.Coin = 0x0.Currency.withdraw_from_sender(copy(amount));

Sender는 withdraw_from_sender라는 프로시저를 실행시켜서, sender의 계좌에서 수신자에게 보낼 코인을 가져옵니다. 이때 코드를 보시면, 0x0.Currency 가 보일겁니다. 잘 아시겠지만 0x0은 account 주소인데요, 여기서는 Currency 모듈이 저장되어 있는 account 주소입니다. Currency 모듈에서 제공하는 withdraw_from_sender 프로시저 호출하므로써, sender의 계좌에서 코인을 가져와서 coin 이라는 변수에 0x0.Currency.Coin 타입으로 저장합니다.

두번째 라인의 코드를 보면,

0x0.Currency.deposit(copy(payee), move(coin));

0x0.Currency 모듈에서 deposit 이라는 프로시저를 제공하는 것을 확인할 수 있고, 이 deposit 프로시저는 수신자(payee)에게 coin을 전송하는 것을 확인할 수있습니다. 그런데 생소한 코드가 눈에 들어옵니다. copy, move.

기존에 우리가 경험했던 많은 프로그래밍 언어에서는 볼수 없었던, 표현식입니다. 이 중 move(coin)을 보시면, move라는 메소드를 이용해서 coin을 전송함으로써, 악의적인 재진입 공격을 막을 수 있다고 나옵니다. 즉 수신자에게 한번 코인을 전송하고 나면, 다시 해당 메소드를 호출했을때, bytecode verification error가 발생합니다. 이처럼 Move 에서는 기존 이더리움 스마트컨트랙에서 가지고 있던 재진입 공격을 자체 언어의 기능을 이용해서 해결되도록 구현되어져 있습니다. Move는 실제 종이 화폐 한장을 제가 누군가에게 주고 나면, 똑같은 그 종이 화폐 한장은 이미 누군가에게 줘서 없는 것이기 때문에 또 다른 사람에게 절대 줄수 없다는 논리대로 구현이 되어 있습니다. 그래서 코인은 move하고(move(coin)), 주소는 copy로(copy(payee)) 사용하게 됩니다.

여기서 Move 언어의 특징 중 하나인 resource safety를 이해할 수 있습니다. Move 언어는 Move resource (coin 같은 자원)에 대한 복사, 재사용, 유실이 절대 될 수 없음을 보장해 줍니다. 단 payee 주소 처럼, 제한되지 않은 값은 복사가 가능합니다.

자 그럼 위에 코드에서 사용된 Currency 모듈을 어떻게 구현하면 될지 살펴보도록 하겠습니다.

모듈 구현하기

먼저 Currency 모듈 선언은 아래와 같이 합니다.

module Currency { 
resource Coin { value: u64 }
// ...
}

모듈 이름은 Currency 이고, Coin 이라는 리소스 타입을 선언했습니다. Coin 리소스는 u64(a 64-bit unsigned integer) 타입의 value 필드 하나가 포함되어 있습니다.

다른 모듈이나 트랜잭션 스크립트는 Currency 모듈의 퍼블릭 프로시저를 통해서 value 필드에 대한 쓰기 및 참조만 할 수 있습니다. 즉, Currency 모듈의 프로시저만 Coin 리소스에 대한 생성 및 삭제가 가능합니다. 이말은 모듈을 만든 사람만이 선언된 리소스에 대한 완벽한 통제 권한을 갖는다는것을 의미합니다. Currency 모듈을 제외한 다른 모듈에서는 단지 Coin 리소스에 대한 move 만 수행할 수 있습니다. 이를 Resource Safety 라고 합니다.

다음으로는 Currency 모듈의 deposit 프로시저를 구현해 보도록 하겠습니다.

public deposit(payee: address, to_deposit: Coin) { 
let to_deposit_value: u64 = Unpack(move(to_deposit));
let coin_ref: &mut Coin = BorrowGlobal(move(payee));
let coin_value_ref: &mut u64 = &mut move(coin_ref).value;
let coin_value: u64 = *move(coin_value_ref);
*move(coin_value_ref) = move(coin_value) + move(to_deposit_value);
}

먼저 첫번째 라인 코드를 보면,

let to_deposit_value: u64 = Unpack(move(to_deposit));

Unpack은 Move언어에 빌트인 기능 중 하나인데, 리소스 타입 객체를 파괴하는 기능을 합니다. Move 언어에서는 특이하게도 코인을 파괴하면 리소스에 바인딩 된 값을 얻을 수 있다고 합니다. 즉 to_deposit_value 에는 sender가 전송한 코인 값이 저장이 됩니다.

두번째 라인 코드를 보면,

let coin_ref: &mut Coin = BorrowGlobal(move(payee));

여기도 역시 BorrowGlobal 이라는 빌트인 기능을 사용했는데요, 주소를 입력값으로 받아 주소가 생성하는 인스턴스를 레퍼런스값으로 반환합니다. 코인자체가 아닌 코인 리소스의 레퍼런스 값을 반환합니다. 그리고 여기서 Coin 타입으로 coin_ref를 선언했는데, 보시면 &mut 라고 생소한 코드가 보입니다. &mut는 리소스가 아니라 리소스의 레퍼런스 값을 받을 때 사용합니다. 여기서는 Coin이 아닌 Coin 레퍼런스 값을 받기 위해 사용되었습니다.

세번째 라인 코드를 보면,

let coin_value_ref: &mut u64 = &mut move(coin_ref).value;

coin_ref에 바인딩 된 레퍼런스 값을 획득하기 위해서 coin_ref의 참조값을 coin_value_ref로 할당 합니다.

네번째 라인 코드를 보면,

let coin_value: u64 = *move(coin_value_ref); 

수신자의 Coin 레퍼런스 값을 이용해서 수신자가 보유하고 있는 전체 코인 값을 가져와서 coin_value에 저장합니다.

마지막 라인 코드를 보면,

*move(coin_value_ref) = move(coin_value) + move(to_deposit_value);

이렇게 수신자가 보유하고 있는 코인 값에 sender가 전송한 코인 값을 더해서 최종적으로 수신자의 보유 코인 값은 업데이트 하게 됩니다.

다음으로 withdraw_from_sender 프로시저를 구현해 보도록 하겠습니다.

public withdraw_from_sender(amount: u64): Coin { 
let transaction_sender_address: address = GetTxnSenderAddress();
let coin_ref: &mut Coin = BorrowGlobal(move(transaction_sender_address));
let coin_value_ref: &mut u64 = &mut move(coin_ref).value;
let coin_value: u64 = *move(coin_value_ref);
RejectUnless(copy(coin_value) >= copy(amount));
*move(coin_value_ref) = move(coin_value) - copy(amount);
let new_coin: Coin = Pack(move(amount));
return move(new_coin);
}

첫번째 라인 코드를 보면,

let transaction_sender_address: address = GetTxnSenderAddress();

sender의 address를 GetTxnSenderAddress() 라는 함수를 이용해서 얻어 옵니다. GetTxnSenderAddress 역시 빌트인 된 기능으로써, 현재 실행된 트랜잭션에서 sender의 address를 가져오는 함수 입니다.

두번째 라인 코드를 보면,

let coin_ref: &mut Coin = BorrowGlobal(move(transaction_sender_address));

BorrowGlobal 함수를 이용해서 sender의 Coin 리소스 참조 값을 반환합니다.

세번째, 네번째 라인 코드를 보면,

let coin_value_ref: &mut u64 = &mut move(coin_ref).value; 
let coin_value: u64 = *move(coin_value_ref);

coin_ref의 참조값을 가져와서 Sender가 보유하고 있는 코인 값을 coin_value 로 할당합니다.

다섯번째 라인 코드를 보면,

RejectUnless(copy(coin_value) >= copy(amount));

sender의 코인 리소스의 값을 전송할 코인 값 만큼 줄이기 전에, RejectUnless를 통해 sender가 보유한 코인 값이 전송할 코인 값 보다 크거나 같다고 간주합니다. 만약 여기서 sender가 보유한 코인 값이 전송할 코인 값 보다 작다면, 에러가 나게 됩니다. 이더리움의 require 함수와 유사하다고 보시면 될것 같습니다.

여섯번째 라인 코드를 보면,

*move(coin_value_ref) = move(coin_value) - copy(amount);

sender의 Coin 레퍼런스에 sender가 원래 소유하고 있던 코인 값에서 수신자에게 전송한 코인 값 만큼 뺀 값을 할당하게 됩니다.

마지막으로

let new_coin: Coin = Pack(move(amount)); 
return move(new_coin);

수신자에게 전송할 코인을 Pack 함수를 이용해서 Pack(Unpack 과 반대)한 후 new_coin 에 할당한 후 new_coin을 반환하여 완성이 됩니다.

지금까지 Move 언어의 기본 특징과 간단히 구현된 모듈을 통해 Move 언어에 대한 기초적인 이해를 해보았습니다. 다음 포스팅에서는 language 폴더의 5가지 디렉토리의 코드를 하나씩 파헤쳐 보도록 하겠습니다.

--

--