소프트웨어 트랜잭셔널 메모리(STM)를 이용한 스마트 컨트랙트에 동시성 부여하기
이더리움은 분산된 신뢰 환경에서 프로그램을 실행할 수 있는 스마트 컨트랙트 기능을 제공한다. 스마트 컨트랙트는 튜링 완전한 언어인 Solidity로 작성하고 바이트 코드로 컴파일되어 가상 머신 상에서 수행한다. 사용하는 스마트 컨트랙트를 수행하기 위해 함수 명과 인자를 입력하고 개인키로 서명한 뒤 트랜잭션을 작성하여 블록체인에 제출한다.
현재 이더리움은 제출된 트랜잭션을 모두 순차 실행(Sequential Processing)을 한다. 멀티 코어 환경이 기본인 현대 컴퓨터에서 코어 하나만 사용하는 순차 실행은 컴퓨터의 연산력을 100% 사용할 수 없다. 그렇다고 동시 처리를 하려고 하니 스마트 컨트랙트가 서로 복잡하게 얽혀있어서 쉽게 동시처리(Concurrency)를 하기 어렵다.
이번 포스팅에서는 스마트 컨트랙트의 순차 실행 환경을 동시 처리할 수 있게 시도한 논문인 “Adding Concurrency to Smart Contracts”를 정리한다[1]. 이 논문은 ACM PODC 2017에 제출된 페이퍼로 소프트웨어 트랜잭셔널 메모리(STM)을 이용하는 방법으로 동시성을 부여하였다.
컴퓨터공학에서 동시성(Concurrency)이란 독립적인 작업(Multi-Task)들을 동시에 처리하는 것을 말한다. 반면, 병렬성(Parallelism)이란 하나의 작업(Single-Task)을 여러 개로 분할 하여 멀티 코어에서 실행하는 의미로 자주 쓰이기 때문에 여기서는 동시성이란 용어를 계속 사용할 것이다.
구성
- 쉽지 않은, 스마트 컨트랙트 병렬 처리
- 소프트웨어 트랜잭셔널 메모리
- Lock-based와 STM의 비교
- 실험
- 결론
쉽지 않은, 스마트 컨트랙트 병렬 처리
블록체인에 참여한 모든 노드는 블록에 저장된 트랜잭션을 순차적으로 실행해서 동일한 상태를 만들고 머클 루트를 이용해 서로의 상태가 일치하는지 확인한 뒤, 다음 블록을 생성하기위한 단계로 넘어간다. 스마트 컨트랙트는 누가 실행하든 항상 결과가 결정적(Deterministic)이어야 한다. 그래서 Solidity언어에는 랜덤 숫자 생성, 파일 입출력, 네트워크 통신처럼 비결정적인 코드는 존재하지 않는다. 스마트 컨트랙트가 결정적인 상태 결과를 만들기 위해서는 다음 두 가지 조건 중 하나를 만족해야 한다.
- 모든 스마트 컨트랙트 코드는 동일한 순서로 실행되어야 한다. 동일한 순서로 실행된 코드는 항상 같은 상태 결과를 만든다. 현재 이더리움은 모든 스마트 컨트랙트를 순차적으로 실행하는 방식을 사용하고 있다.
- 모든 스마트 컨트랙트 코드는 병렬로 수행하되 동일한 자원에 접근하지 않아야 한다. 만약, Alice가 Bob에게 5 ETH를 보내는 트랜잭션 A와 Carol이 Michael 에게 3.5 ETH를 보내는 트랜잭션 B가 존재한다고 가정 할 때 이 두개의 트랜잭션은 어떤 순서로 동작해도 동일한 상태 결과를 만든다. 만약 아래 두 개의 코드가 존재할 때 서로 다른 순서로 동작한다면 최종 결과가 다를 것이다.
// 1번 코드
getBalance(“Alice”); – 5 ETH
alice.transfer(balance:5 ETH, to:“Bob”);
// 2번 코드
getBalance(“Alice”); – 5 ETH
alice.transfer(balance:3.5 ETH, to: “Carol”);
이번 포스팅에서 소개하려는 논문은 바로 2번 문제를 해결하여 멀티 코어로 트랜잭션을 병렬 처리 하는 방법을 제안한다. 아이디어를 간단하게 소개하자면 우선 트랜잭션 풀에 존재하는 트랜잭션들을 모두 병렬 처리 한 뒤, 동일한 자원에 접근하는 트랜잭션들만 따로 식별해서 실행 순서를 결정한다. 이 때, 동일한 자원에 접근하는 상태를 충돌(Conflict)라고 부르고 이 충돌이 발생하는 트랜잭션을 식별하기 위한 방법으로 소프트웨어 트랜잭셔널 메모리(STM: Software Transactional Memory)를 이용한다.
소프트웨어 트랜잭셔널 메모리 (STM)
소프트웨어 트랜잭셔널 메모리(이하 STM)은 실행 코드의 단위를 트랜잭션으로 보는 패러다임이다[3]. 트랜잭션은 원자성, 일관성, 고립성, 영구성 (ACID) 속성을 만족해야 한다. 관계형 데이터베이스(RDBMS)에서는 이를 만족하기 위해 다수의 요청을 하나의 트랜잭션으로 묶고, 하나의 요청이 실패하면 수행된 요청을 모두 번복한다. 마찬가지로 STM에서는 다수의 코드를 개별적으로 트랜잭션으로 보고 멀티-쓰레드를 생성해 동시 처리 한 다음 충돌이 발생한 트랜잭션만 번복(abort)해서 멀티 코어 환경의 장점을 이용한다.
STM은 이런 트랜잭션의 속성을 코드-레벨에서 추상적으로 구현한다. 이 논문에서는 스마트 컨트랙트를 수행할 때 추상 락(abstract lock)과 역로그(inverse log)를 생성한다. 두 개 이상의 컨트랙트가 동일한 변수를 변경하면 추상 락에 이를 기록하고 역로그에 변경한 변수 내용의 변경 이전의 내용을 기록한다. 만약 충돌이 발생한 경우에는 역로그를 이용해서 실행중인 컨트랙트의 내용을 번복한다. 이렇게 일단 저지르고 후에 수습하는 형태(논문에서는 Speculative한 방법이라고 표현한다)로 동시성 처리를 지원한다.
[그림 1] 기존 블록체인의 트랜잭션 처리 모델
기존 블록체인은 매 트랜잭션이 순차적으로 처리되고 공유 자원(변수의 값)에 한 번에 하나씩 접근하므로 충돌이 발생하지 않아 모든 결과가 결정적이다. 하지만 순차 처리는 멀티 코어 환경에서 모든 자원을 사용하지 않기 때문에 비효율적이다. 이 논문에서 소개한 STM을 이용한 방법을 구성도로 그려보자면 아래와 같다.
[그림 2] STM 기반 트랜잭션 동시처리기법의 간단한 구성도
- Commute 트랜잭션 : 임의의 두 트랜잭션이 서로 다른 자원을 사용할 때 이 트랜잭션을 Commute 하다라고 한다. Commute 트랜잭션은 서로 다른 쓰레드 상에서 바로 병렬로 처리 가능하다.
- Conflict 트랜잭션 : 임의의 두 트랜잭션이 서로 동일한 자원을 사용할 때 충돌(Conflict) 트랜잭션이라고 부른다. 충돌이 발생한 상황에서는 두 트랜잭션의 실행 순서에 따라 최종 상태가 달라진다. 따라서, 이 경우에 채굴자는 둘 중 어느것이 먼저 처리 되어야 하는지 기록하는 happen-before 그래프 H를 생성하고 이를 블록에 기록해야 한다. 추후 검증자는 이 H값을 확인하고 충돌이 발생한 트랜잭션에 대해서만 fork-join을 이용해 순차 처리를 한다.
- Inverse Log : 스마트 컨트랙트는 스토리지 연산을 수행할 때 역기능을 수행하는 작업을 Inverse Log에 기록한다. 모든 트랜잭션을 우선 병렬로 수행하고 충돌이 발생한 트랜잭션에 대해서만 Inverse Log를 이용해 작업을 취소한다. 그리고 이를 Lock Profile에 기록한다.
- Abstract Lock : 공유 자원에 충돌이 발생한 상황을 인지하기 위해 사용하는 추상 락이다.
위 구성도에 따른 알고리즘은 아래 [그림 3]과 같다.
[그림 3] 채굴자의 트랜잭션 병렬 처리 알고리즘
Lock-based와 STM의 비교
다중-쓰레드가 공유 자원에 접근해서 버그가 발생하는 것을 방지하기 위해 우리는 이미 뮤텍스(Mutex)와 락(Lock)을 이용할 수 있다. 여기서 락-기반 방식과 STM의 차이점을 간단하게 요약하자면, 락-기반 방식의 멀티 쓰레드 프로그래밍에서는 하나의 쓰레드가 자원을 이미 사용하고 있을 때, 다른 쓰레드는 대기를 해야한다. 또한 개발자가 쓰레드가 교착상태(Dead lock)빠지지 않도록 상호 배제, 비선점, 점유 대기 등을 신경써주어야 한다. STM 방식은 오버헤드가 증가해서 성능은 조금 포기하지만 그만큼 구현이 쉽고 직관적으로 이해할 수 있다는 장점이 있다.
실험
이 논문에서 실험을 진행한 방식이 재밌는데.. 실험을 블록체인 상에서 구현해서 하지 않았다. 이미 존재하는 스마트 컨트랙트를 Scala 프로그래밍 언어로 번역하고 ScalaSTM 라이브러리를 이용해서 JVM상에서 순차-처리와 STM을 이용한 동시처리 할 때 성능을 비교하였다. 여기서는 스마트 컨트랙트의 복잡성과 데이터 충돌 상황에 따른 성능을 비교하기 위해 4개의 컨트랙트를 Scala언어로 번역해서 실험을 진행하였다. 아래 [그림 4]는 이 중 Ballot과 Simple Auction 컨트랙트의 성능 비교 그래프를 보여준다.
[그림 4] 순차처리와 STM을 이용한 동시처리의 실행 시간 비교
그래프에서는 순차 처리 하는 경우(Serial), STM상에서 채굴자의 성능(Miner) 그리고 블록을 받아 검증하는 검증자의 성능(Validator). 이 세가지를 비교하였다. happen-before 그래프 H를 채굴자가 만들기 때문에 검증자와 알고리즘이 조금 달라서 이렇게 구분해서 성능을 비교하였다. 그림을 보면 컨트랙트 마다 다르지만 순차처리를 하는 경우보다 대략 1.5x 정도 더 빠르게 실행할 수 있다는 것을 알 수 있다. 이는 물론 쓰레드의 개수나 충돌되는 트랜잭션 비율에 따라 다를 수 있다.
결론
블록체인의 확장성 문제를 해결하기 위해 현재 다양한 방법들이 논의되고 있다. 현대 연구분야에서 대표적으로 주목 받고 있는 것을 세 가지 분야로 분류하자면 영지식 증명(zk-snark), 신뢰 실행 환경(TEE), 레이어-2 솔루션이 있다. 나는 개인적으로 노드의 종류를 나누어서 네트워크 참여자의 역할을 구분 하는 것은 직관적이고 깔끔한 해결책은 아니라고 생각한다. 그래서 궁극적으로는 TEE나 zk-snark 둘 중 하나로 문제를 해결 해야 할 것 같은데 이 중 TEE가 현재로서는 가장 현실적인 대안이라고 본다.
레퍼런스
[1] Adding Concurrency to Smart Contracts
[2] 데이터베이스, 블록체인, 그리고 트릴레마
[3] Beyond Locks: Software Transactional Memory
[4] Software Transactional Memory