Move 문해력 키우기(3): Integration of MoveVM and Blockchain

verse2
verse2
Published in
19 min readJun 25, 2024

Integration of MoveVM and Blockchain: Key takeaways

  • Move는 스택 머신으로써 블랙박스 형태의 안전한 실행 환경을 제공하고 있다.
  • Aptos는 블랙박스인 MoveVM과 상호작용하는 AptosVM을 통해 스마트 컨트랙트 플랫폼으로 활용하고 있다.

도입
VM은 네러티브다. 새로운 체인, 새로운 VM을 제안하며 시장의 주목을 받으려는 시도는 쉬우면서도 효과적인 GTM (Go To Market) 전략이다. 그렇지만 확실한 기술력 없이 새로운 VM을 만들고, 그것을 블록체인과 통합시키는 것은 거의 불가능하다. 단순 브랜딩 전략으로 새로운 VM을 제안하던 프로젝트들은 시간이 지남에 따라 잊혀지고 오랜 기간 검증이 된 프로젝트들만 살아남게 되었다.

Comparison to VMs

Move는 Smart Contract에 특화된 언어이긴 하지만 사실 Move 언어를 실행하는 MoveVM은 블록체인과 별개의 프로그램이다. MoveVM은 블록의 생성, 합의, 트랜잭션 처리 등에 대한 로직과는 무관하게 단순히 바이트 코드(bytecode)로 특정 모듈(Module)을 보관하였다가 입력 값이 들어왔을 때, 모듈을 실행하여 출력하는 결과를 반환하는 프로그램이다. 이를 좀 더 쉽게 이해하기 위해 기본적인 VM을 통해 컨트랙트가 배포되고 실행되는 동작 과정을 그림으로 표현하면 아래와 같다.

Smart Contract Execution Flow
  1. 개발자가 Solidity (EVM)나 Rust (WasmVM, SolVM) 등의 언어로 스마트 컨트랙트를 개발한다.
  2. 컴파일 프로그램 (solc 등)을 이용하여 컨트랙트 코드를 바이트 코드로 변경한다. 여기서 바이트 코드는 컨트랙트의 로직을 담고 있는 인스트럭션 (instruction)의 모음이다.
  3. 컨트랙트를 블록체인에 배포한다. 배포된 컨트랙트는 블록체인에 저장된다.
  4. 컨트랙트 사용자는 블록체인 클라이언트 (Geth 등)를 이용하여 컨트랙트를 호출한다. 컨트랙트는 트랜잭션 내에 호출하려는 컨트랙트 정보와 입력값을 담아 실행하는 방법으로 호출 가능하다.
  5. 트랜잭션이 정상적으로 실행이 된다면, 즉 스마트 컨트랙트가 정상적으로 실행 된다면 결과를 반환하고 블록체인 내 상태에 저장된다.

VM 마다 컨트랙트를 저장, 실행하는 방법이 조금씩 다르긴 하지만 일반적으로 위와 같은 흐름을 통해 실행된다. 이번 아티클에서는 MoveVM의 실행 흐름과 Aptos와 같은 블록체인과 어떻게 결합되어 동작하는 지에 대해 알아보도록 하겠다.

#1. Interact with MoveVM

우선, Move 프로그램은 런타임 동안 시스템 메모리에 접근하지 않고 가상 머신 (virtual machine) 내에서만 실행되어 블랙박스 형태로 MoveVM 이외 외부 환경에 의해 영향을 받지 않으며 동작한다. 또한 Move 프로그램은 스택(stack) 내에서 실행이 된다. 컨트랙트 실행 스택에는 다음과 같은 요소들을 포함하고 있다.

  • 호출 스택 (Call Stack)

호출 스택은 프로시저 실행에 필요한 모든 컨택스트 정보를 포함하고 있으며 각 명령어는 코드 크기를 줄이기 위해 인코딩 되어있다. 프로시저가 다른 프로시저를 호출하는 Call 명령어를 실행할 때, 새로운 호출 스택 객체가 생기게 된다.

Move Call Stack Architecture

MoveVM은 데이터 저장과 호출스택(프로세스 로직) 저장을 분리한다. 예를 들어, EVM에서 ERC20 토큰을 구현하려면 개발자는 로직을 작성하고 각 사용자의 상태를 계약에 저장해야 하지만, MoveVM에서는 사용자 상태(계정 주소 아래의 자원)가 독립적으로 저장되며, 프로그램 호출은 자원에 대한 권한과 필수 규칙을 준수해야 한다. 이는 유연성을 다소 희생하지만 보안과 실행 효율성을 크게 향상시킨다.

  • 메모리 (Memory)와 전역 변수 (Global Storage)

메모리는 일시적인 실행 데이터와 변수를 저장한다. 힙 (heap) 구조의 최우선 저장소 (first-order store)로 포인터(pointer) 저장 불가능한 영역이다. 전역 변수는 동일한 컨택스트 내에서 다른 함수 사이에 접근 가능한 변수 정보를 포함한다. 메모리 셀 (cell) 포인터를 저장하며 인덱싱(indexing)이 가능하여 빠르게 값을 호출한다.
전역 변수에 접근하기 위해서는 주소와 해당 주소에 맵핑되어 있는 타입을 함께 제공해야 한다. 이러한 특징 때문에 Move 컨트랙트 개발 시 스트럭트 태그 (struct tag)라 사용해야하며 0x1::{MODULE_NAME}::{RESOURCE_NAME} 와 같은 형태의 타입을 제공해야 한다. 이 같은 분리를 통해 Move는 VM 내부적인 동작을 단순화 하고 문법적으로 정형화 된 패턴을 제공하게 된다.

  • 오퍼랜드 스택 (Operand Stack)

오퍼랜드는 컨트랙트 실행 중 표현식을 평가하고 중간 결과를 저장하는데 사용된다. 이 스택은 실행 과정에서 임시 계산 값이나 연산에 필요한 데이터를 보관하는 장소로, 각종 연산의 입력 값이나 그 결과를 임시로 저장하며, 프로그램이 다양한 명령어를 수행할 때 계산 과정을 관리하는 데 중요한 역할을 한다.

예를 들어, 두 수를 더하는 간단한 연산에서는 먼저 두 수가 오퍼랜드 스택에 푸시(push)되고, 덧셈 명령이 실행될 때 이 두 수는 스택에서 팝(pop)되어 계산이 수행된다. 계산 결과는 다시 스택에 푸시되어 다음 연산의 입력으로 사용될 수 있다. 이런 방식으로 컨트랙트의 실행 도중 필요한 모든 데이터 흐름과 처리 과정을 지원한다. 오퍼랜드 스택은 복잡한 연산을 순차적으로 처리하는 데 중추적인 역할을 하며, 특히 계산 중간 결과의 임시 저장 및 함수 호출 관리에 있어 중요한 기능을 수행한다.

여기서 MoveVM의 흥미로운 점 중 하나는 바로 정적 점프 (static jump)이다. 정적 점프란 컨트랙트 실행 과정 중 분기 명령을 만나게 되었을 때 분기 조건에 따라 정해진 위치로 인스트럭션이 이동 되는 것을 의미한다. MoveVM의 정적 점프는 점프 오프셋 (offset)이 사전에 결정되며, EVM처럼 동적으로 점프하지 않는다. 이는 모듈 내의 의존성이 비순환적(acyclic)이며, 모듈 자체도 동적으로 할당되지 않아 함수 호출의 불변성을 강화하고 재진입 (re-entrancy) 가능성을 제거한다.

Move는 이러한 메모리 관리 기법과 분기 처리 메커니즘을 통해 프로그램의 논리를 명확하고 효율적으로 실행할 수 있게 하며, 특히 블록체인과 같이 보안과 효율성이 중요한 환경에서 큰 장점을 제공하는데 많은 노력을 쏟고 있다.

#2. Aptos Core 톺아보기

  1. How Aptos interact with MoveVM?

Aptos는 MoveVM을 스마트 컨트랙트 플랫폼으로 사용하고 있는 대표적인 레이어 1 체인이다. 모든 스마트 컨트랙트는 기본적으로 트랜잭션이라는 유저의 입력을 받아 정해진 결과를 출력하는 블랙박스 형태로 동작하게 된다. AptosVM (aka Aptos MoveVM) 역시 매 블록마다 포함되어 있는 정보를 읽어 정해진 순서에 따라 실행되게 된다. 블록 내에는 트랜잭션 스크립트 (transaction script)와 스테이트 뷰 (state view)의 정보를 담고 있는 입력으로 받으며 각각에 대한 간략한 설명은 아래와 같다.

  • Transaction script: 사용자가 특정 행동을 수행하고자 할 때 제출하는 스크립트나 코드 조각. 블록체인의 상태를 변경하거나 특정 상태를 조회하는 작업 등을 수행하는 것이 가능하다.
  • State view: 블록의 높이나 시간에 따른 블록체인의 특정 버전에서의 데이터나 코드의 스냅샷. 읽기 전용 (read-only)으로만 값에 접근 하는 것이 가능하며 블록체인의 상태를 특정 시점에 정확하게 가져오기 위한 인터페이스 역할을 담당한다. 실제 스테이트 뷰 구현은 아래와 같다.
/// `StateView` is a trait that defines a read-only snapshot of the global state. It is passed to
/// the VM for transaction execution, during which the VM is guaranteed to read anything at the
/// given state.
pub trait TStateView {
type Key;

/// For logging and debugging purpose, identifies what this view is for.
fn id(&self) -> StateViewId {
StateViewId::Miscellaneous
}
/// Gets the state value bytes for a given state key.
fn get_state_value_bytes(&self, state_key: &Self::Key) -> Result<Option<Bytes>> {
let val_opt = self.get_state_value(state_key)?;
Ok(val_opt.map(|val| val.bytes().clone()))
}
/// Gets the state value for a given state key.
fn get_state_value(&self, state_key: &Self::Key) -> Result<Option<StateValue>>;
/// Get state storage usage info at epoch ending.
fn get_usage(&self) -> Result<StateStorageUsage>;
fn as_in_memory_state_view(&self) -> InMemoryStateView {
unreachable!("in-memory state view conversion not supported yet")
}
}

Aptos 네트워크에 제출 된 모든 트랜잭션은 아래와 같은 세 가지 단계를 거쳐 처리되게 된다.

  • Transaction Prologue: 트랜잭션이 유효한 지를 검사하는 단계. 실제 체인의 상태를 변경하기 전, 해당 트랜잭션이 유효하게 동작하는 지 (무한 루프를 돌거나, 가스가 부족하여 거되거나 )를 검증하는 과정이다. 트랜잭션 검증이 완료 될 경우 ‘성공’ 또는 ‘실패’로 결과가 반환되며 어떤 결과이든 실제 체인 상태를 변경하지는 않는다.
  • Transaction Execution: 검증이 완료된 트랜잭션에 대해 해당 트랜잭션을 수행하여 실제 체인 상태를 변경하는 과정이다. 트랜잭션의 수행 결과로 스테이트 뷰로 부터 write_set 이라는 체인 상태를 아토믹(atomic)하게 변경할 수 있는 값을 생성하게 된다. 아래와 같이 실제 스마트 컨트랙트 호출 시 시뮬레이션을 통해 해당 트랜잭션의 write_set을 미리 조회하는 것이 가능하다.
Writeset on Transaction

Writeset on Transaction

  • Transaction Epilogue: 트랜잭션 수행 결과에 따른 특정 작업 (action)을 수행한다. 예를 들어 트랜잭션에 필요한 가스를 트랜잭션 제출자의 잔고에서 출금하는 작업과 같은 것이 대표적이다.

AptosVM은 스테이트 뷰에서 트랜잭션 실행에 컨트랙트 코드나 링크되어있는 모듈 등 필요한 정보들을 조회하여 처리한다. 또한 트랜잭션이 체인을 업데이트 결과를 추적하여 순차적으로 트랜잭션이 처리되게 하며 병렬처리와 동시처리가 가능하게 한다.

2. 구현체 살펴보기

Aptos는 기존에 존재하는 MoveVM을 완벽하게 감싼 래퍼(wrapper) 구현체인 AptosVM Runtime을 구현하였으며, 해당 구현체는 앞서 언급은 3단계에 거쳐 트랜잭션을 수행하게 된다. 실제 AptosVM Runtime이 어떤 식으로 구현되었는 지를 자세히 살펴보기 위해 앞서 말한 세 단계의 코드를 살펴보면 아래와 같다.

우선 Prologue의 경우에는 트랜잭션에 담겨져 있는 정보인 페이로드 (payload)를 인자로 받게 된다. 트랜잭션을 수행하기 전, 트랜잭션 사이즈가 유효한 지를 먼저 검사(check_gas)하고 해당 페이로드의 내용에 따라 동작을 수행하게 된다. payload는 대표적으로 Move 트랜잭션을 수행하는 스크립트 (script), 엔트리 함수 (entry function) 등이 담겨 있다. 트랜잭션이 체인에 반영되기 전까지 정보를 들고 있을 세션 (session)을 트랜잭션에 필요한 저장소로 활용하게 된다.

// aptos-move/aptos-vm/src/aptos_vm.rs
fn run_prologue_with_payload(
&self,
session: &mut SessionExt,
resolver: &impl AptosMoveResolver,
payload: &TransactionPayload,
txn_data: &TransactionMetadata,
log_context: &AdapterLogSchema,
traversal_context: &mut TraversalContext,
) -> Result<(), VMStatus> {
check_gas(
get_or_vm_startup_failure(&self.gas_params, log_context)?,
self.gas_feature_version,
resolver,
txn_data,
self.features(),
log_context,
)?;

match payload {
TransactionPayload::Script(_) | TransactionPayload::EntryFunction(_) => {
transaction_validation::run_script_prologue(
session,
txn_data,
log_context,
traversal_context,
)
},
.....
}
}

두 번째로는 트랜잭션의 유효성이 검증된 후 해당 트랜잭션을 수행하게 된다. 트랜잭션 수행에 필요한 자산 (가스비, 트랜잭션에 사용되는 금액 등)이 계정에 예치되어 있는지를 확인하며, 페이로드의 값을 확인하여 스마트 컨트랙과 상호작용 해야하는 경우에는 해당 컨트랙트를 호출하여 트랜잭셔늘 수행하게 된다. 프롤로그 단계에서 시뮬레이션을 통해 수행해서 나온 결과가 유효한 지를 한 번 더 확인한 후 유효하다면 실제 체인에 업데이트를 하게 된다.

// aptos-move/aptos-vm/src/aptos_vm.rs
pub fn execute_user_transaction(
&self,
resolver: &impl AptosMoveResolver,
txn: &SignedTransaction,
log_context: &AdapterLogSchema,
) -> (VMStatus, VMOutput) {
let balance = txn.max_gas_amount().into();
let mut gas_meter = unwrap_or_discard!(self.make_standard_gas_meter(balance, log_context));

let traversal_storage = TraversalStorage::new();
let mut traversal_context = TraversalContext::new(&traversal_storage);
self.execute_user_transaction_impl(
resolver,
txn,
log_context,
&mut gas_meter,
&mut traversal_context,
)
}

마지막 에필로그 단계는 트랜잭션을 수행 후 마무리 작업을 하게 되며, 가스 관련 검증, Move 컨트랙트 에러 처리, 이벤트 생성 등과 같은 역할을 수행하게 된다.

// aptos-move/aptos-vm/src/aptos_vm.rs
fn run_epilogue(
session: &mut SessionExt,
gas_remaining: Gas,
fee_statement: FeeStatement,
txn_data: &TransactionMetadata,
features: &Features,
traversal_context: &mut TraversalContext,
) -> VMResult<()> {
let txn_gas_price = txn_data.gas_unit_price();
let txn_max_gas_units = txn_data.max_gas_amount();

// We can unconditionally do this as this condition can only be true if the prologue
// accepted it, in which case the gas payer feature is enabled.
if let Some(fee_payer) = txn_data.fee_payer() {
let (func_name, args) = {
let mut args = vec![
MoveValue::Signer(txn_data.sender),
MoveValue::Address(fee_payer),
MoveValue::U64(fee_statement.storage_fee_refund()),
MoveValue::U64(txn_gas_price.into()),
MoveValue::U64(txn_max_gas_units.into()),
MoveValue::U64(gas_remaining.into()),
];
if txn_data.required_deposit.is_some() {
args.push(txn_data.required_deposit.as_move_value());
(
&APTOS_TRANSACTION_VALIDATION.user_epilogue_gas_payer_return_deposit_name,
args,
)
} else {
(
&APTOS_TRANSACTION_VALIDATION.user_epilogue_gas_payer_name,
args,
)
}
};
session.execute_function_bypass_visibility(
&APTOS_TRANSACTION_VALIDATION.module_id(),
func_name,
vec![],
serialize_values(&args),
&mut UnmeteredGasMeter,
traversal_context,
)
} else {
// Regular tx, run the normal epilogue
let (func_name, args) = {
let mut args = vec![
MoveValue::Signer(txn_data.sender),
MoveValue::U64(fee_statement.storage_fee_refund()),
MoveValue::U64(txn_gas_price.into()),
MoveValue::U64(txn_max_gas_units.into()),
MoveValue::U64(gas_remaining.into()),
];
if txn_data.required_deposit.is_some() {
args.push(txn_data.required_deposit.as_move_value());
(
&APTOS_TRANSACTION_VALIDATION.user_epilogue_return_deposit_name,
args,
)
} else {
(&APTOS_TRANSACTION_VALIDATION.user_epilogue_name, args)
}
};
session.execute_function_bypass_visibility(
&APTOS_TRANSACTION_VALIDATION.module_id(),
func_name,
vec![],
serialize_values(&args),
&mut UnmeteredGasMeter,
traversal_context,
)
}
.map(|_return_vals| ())
.map_err(expect_no_verification_errors)?;
// Emit the FeeStatement event
if features.is_emit_fee_statement_enabled() {
emit_fee_statement(session, fee_statement, traversal_context)?;
}
maybe_raise_injected_error(InjectedError::EndOfRunEpilogue)?;
Ok(())
}

결론

본 아티클을 통해 MoveVM이 Aptos와 같은 블록체인 플랫폼과 어떤 방식으로 통합되어 동작하는 지에 대해 코드레벨에서 이해할 수 있었다. MoveVM은 블랙박스 형태의 견고한 프로그램 실행환경을 제공함으로써 안정성을 제공하며, Aptos는 이를 활용하여 사용자에게 좀 더 나은 인터페이스를 제공하여 더 나은 유저 경험을 제공하고 있음을 확인할 수 있었다. 단순히 트랜잭션 처리 과정 뿐만아니라 데이터베이스, 네트워크 수준에서도 MoveVM을 100프로 효율적으로 활용하기 위한 고민을 볼 수 있으며 추후 이에 대한 내용 또한 다룰 수 있게 준비할 예정이다.

작성자 : Harvey
검수자 : Luis

Reference

--

--

verse2
verse2
Editor for

Build, incubate, invest — Making all possible in the crypto. / verse2.io