자체 블록체인을 만들기로 했다: UTXO에서 StateCell까지
토큰을 얼마나 오래 보유했는지에 따라 권리를 계산하려면, 지금의 잔액에 과거의 기록을 더해야 한다. 소유기간 컨트랙트를 돌아본 글에서는 잔액이 바뀔 때마다 블록 번호와 변경 후 잔액을 남기는 방식으로 이 문제를 풀어봤다. 기존 블록체인 위에 스마트컨트랙트를 배포하는 것만으로도 보유기간을 반영한 수익배분을 실험할 수 있었다.
그런데 여러 자산이 같은 종류의 기록을 필요로 한다면 어떨까. 자산마다 이력을 남기는 구조를 만들고, 조회 방식을 정하고, 그 기록을 다시 읽는 작업을 반복하게 된다. 원장이 자산의 소유와 상태 변화를 공통 구조로 이해한다면 이 일을 어디까지 함께 처리할 수 있을까.
이 질문은 니고프로토콜이라는 자체 블록체인을 설계하게 된 계기 중 하나였다. 직접 원장을 만든다면 먼저 정해야 할 것이 있었다. 무엇을 하나의 상태로 저장하고, 그 상태의 변경을 어떻게 표현할 것인가.
StateCell 시리즈의 첫 글에서는 이 선택의 출발점을 설명한다. UTXO가 보여준 명시적인 상태 전이와 스마트컨트랙트가 열어준 개발 경험을 어떤 방식으로 같은 원장에 담으려 했는지에 관한 이야기다.
소유기간을 기록하던 코드에서 원장 설계로
당시 소유기간 구현이 보여준 것은 스마트컨트랙트의 가능성이었다. 블록체인 전체의 규칙을 바꾸지 않아도 새로운 권리 계산을 코드로 표현하고 실험할 수 있었다. 더 나은 컨트랙트를 만들거나, 오프체인 인덱서로 기록을 정리하는 방향으로도 그 연구를 이어갈 수 있었다.
나는 여기서 질문을 원장 쪽으로 넓혀보고 싶었다. 소유와 이전, 그리고 상태가 바뀐 관계를 여러 프로그램이 공통으로 사용할 수 있는 기반으로 만들고 싶었다. 자체 체인을 선택한다는 것은 그 기반의 저장 방식과 검증 규칙을 직접 정하는 동시에, 그 규칙이 모든 노드에서 같게 작동하도록 책임지는 일이기도 했다.
이때 말하는 스토리지 모델은 어떤 데이터베이스를 사용할지보다 앞선 문제다. 원장에 어떤 단위의 정보가 존재하는지, 거래는 무엇을 읽고 바꿀 수 있는지, 변경 전후를 어떻게 연결할지에 관한 약속이다. 이 약속에 따라 소유권 검증, 이력 추적, 프로그램 실행과 병렬 처리의 모양도 달라진다.
그 출발점으로 살펴볼 만한 모델이 UTXO였다.
A의 100을 B에게 60 보내는 두 가지 표현
A가 어떤 자산을 100단위 가지고 있고 B는 0이라고 하자. A는 B에게 60을 보내려 한다. 비교를 단순하게 하기 위해 이 글의 이전 예제에서는 수수료와 추가 발행·소각을 제외한다.
잔액을 계정별 숫자로 관리한다면 다음과 같이 표현할 수 있다.
이전 전 이전 후
A의 잔액 100 40
B의 잔액 0 60
거래를 실행하는 프로그램이 A의 잔액에서 60을 빼고 B의 잔액에 60을 더한다. 여기서는 잔액 변경만 단순화했으며, 실제 계정 기반 원장에는 서명과 거래 순서 등 추가 검증도 있다.
UTXO는 같은 이전을 다른 단위로 표현한다. UTXO는 Unspent Transaction Output, 아직 소비되지 않은 거래 출력이라는 뜻이다. 이전 거래가 만든 출력이 다음 거래에서 사용할 수 있는 자산의 단위가 된다. 지갑에 표시되는 잔액은 사용할 수 있는 출력들의 수량을 합한 값이다. Bitcoin 거래 구조 설명
A의 100이 하나의 출력 u0에 들어 있다면, A는 그 출력을 소비하고 새 출력 두 개를 만든다.
u0, u1, u2는 구분하기 위한 예제 이름이다.
입력: u0 — A의 100
│ 소비
▼
새 출력 생성
├─ u1 — B의 60
└─ u2 — A의 40
u0 안의 숫자를 100에서 40으로 고치는 대신, 100이 담긴 출력을 통째로 사용한다. B가 받을 60과
A에게 돌아올 잔돈 40은 각각 새 출력이 된다. 입력 100과 출력 합계 60 + 40은 같다.
이 거래가 반영된 뒤에는 u0를 다시 사용할 수 없다. A가 같은 u0로 다른 사람에게도 보내려 한다면
원장은 이중 소비로 거부한다. ‘소비’는 과거 거래 기록을 지운다는 뜻이 아니라, 해당 출력을 더 이상
사용 가능한 상태로 취급하지 않는다는 뜻이다.
이 예제에서는 편의상 ‘A의 출력’이라고 썼다. Bitcoin의 실제 출력에는 소비 조건이 붙으며, 흔한 경우 해당 키의 서명으로 그 조건을 충족한다. UTXO에 단순한 소유자 이름만 적혀 있는 것은 아니다.
어떤 거래가 서로 독립적인지 드러난다
이전이 끝나면 B에게는 u1의 60이, A에게는 u2의 40이 있다. 이제 두 사람이 각자 다른 사람에게
일부를 보낸다고 하자.
B의 거래: u1의 60 소비
→ C에게 10, B에게 잔돈 50
A의 거래: u2의 40 소비
→ D에게 15, A에게 잔돈 25
두 거래는 서로 다른 출력을 사용한다. 하나가 반영되어도 다른 거래의 입력과 계산 결과는 바뀌지 않는다. 최종 수량도 B=50, C=10, A=25, D=15로 처음의 100과 같다. 이 예제처럼 추가 공유 상태가 없고, 각 거래가 검증에 필요한 상태를 서로 다른 입력으로 특정할 수 있다면 두 거래를 독립적으로 검증할 수 있다.
반대로 두 거래가 모두 u1을 쓰겠다고 하면 독립적이지 않다. 또 u1을 만드는 첫 거래와 그것을
소비하는 B의 후속 거래 사이에는 선후 관계가 있다. 입력과 출력의 연결을 따라가면 이런 의존성을
찾을 수 있다.
UTXO에서 주목한 장점은 이처럼 거래가 사용하는 상태와 의존 관계를 명시한다는 것이다. 독립적인 가지를 찾아 병렬 처리할 여지가 생기고, 같은 출력을 두 번 쓰려는 충돌도 식별하기 쉽다. 병렬성을 얻으려면 실제 업무 상태 역시 여러 독립 출력으로 구성할 수 있어야 한다. eUTXO의 상태 분리와 병렬성 설명
계정 기반 모델에서도 서로 겹치지 않는 상태 접근을 찾아 병렬화할 수 있다. 다만 임의의 컨트랙트가 어떤 상태를 읽고 쓸지는 호출을 실행하면서 드러날 수 있다. 거래 형식에 의존성이 드러나는 경우와, 실행을 분석하거나 추적해서 알아내야 하는 경우에는 병렬 처리 설계에 필요한 작업이 달라진다.
실행 전에 알 수 있는 것이 많아진다
명시적인 입력과 출력은 거래를 만드는 사람에게도 도움이 된다. A가 처음의 60을 보내려고 할 때,
지갑은 u0를 선택하고 u1, u2의 수신자와 수량을 정해 거래를 구성할 수 있다. 노드에 제출하기
전부터 무엇을 소비하고 누구에게 얼마를 보낼지 확인할 수 있는 것이다.
여기서는 모든 노드가 같은 결과에 도달하는 결정론과, 거래 작성자가 결과를 미리 판단하는 예측 가능성을 나누어 볼 필요가 있다.
첫째는 같은 시작 상태와 실행 조건, 같은 거래 순서가 주어지면 모든 노드가 같은 결과를 얻는 것이다. 계정 기반 블록체인에서도 필요한 성질이다. 한 노드는 A에게 40이 남았다고 하고 다른 노드는 30이 남았다고 하면 같은 원장을 유지할 수 없다. 병렬로 실행하더라도 정해진 순서의 실행 결과와 일치해야 한다.
둘째는 거래를 제출하는 사람이 그 거래의 결과를 미리 판단할 수 있는 범위다. 검증에 필요한
상태와 조건을 거래의 입력으로 고정할 수 있다면, 다른 곳에서 일어난 무관한 상태 변화에 덜 의존한다.
예제의 u0는 사용되기 전까지 100이 담긴 그 출력이다. 다른 거래가 그 안의 수량만 80으로 바꾸고
같은 출력으로 남겨두는 방식은 아니다.
이렇게 검증 맥락을 제한하면 제출 전 검증이 쉬워진다. 실제 반영 여부는 블록 실행 시 입력이 아직 사용 가능한지, 유효 기간 같은 조건을 만족하는지 다시 검증하고 합의를 거쳐 결정한다. Cardano의 결정론 설명도 거래 결과를 미리 판단하는 것과 실제 원장에 반영될 수 있는 조건을 함께 다룬다. 거래 비용과 결정론
오프체인에서 거래를 만든다는 것도 이 흐름에서 의미가 있다. 필요한 출력 정보를 확보해 거래를 작성하고, 별도의 환경에서 내용을 검토하고 서명한 뒤, 나중에 네트워크에 제출할 수 있다. Bitcoin의 거래 예제에서도 작성·서명·전파가 나뉘며 오프라인 서명을 설명한다. 거래 작성과 오프라인 서명 예제
오프체인 작성과 서명 자체는 UTXO만의 기능은 아니다. Ethereum 거래도 개인키로 서명한 뒤 네트워크에 제출한다. UTXO에서 관심을 둔 부분은 서명 장소보다, 소비할 상태와 제안하는 출력을 거래 안에 담고 그 관계를 미리 검토할 수 있다는 점이었다. Ethereum의 거래 서명과 제출
스마트컨트랙트가 열어준 개발 방식도 담고 싶었다
UTXO의 구조가 마음에 들더라도 자산 이전만으로 원장이 할 일을 모두 표현할 수는 없다. 소유기간 연구만 돌아봐도 잔액 외에 기록할 변경점, 계산할 구간, 수익배분 정책과 청구 여부가 있었다. 업무가 늘어나면 그에 맞는 상태와 규칙도 계속 생긴다.
스마트컨트랙트는 이런 규칙을 프로그램으로 추가할 수 있게 해준다. 모든 자산의 정책을 미리 프로토콜에 넣지 않아도 개발자가 새로운 동작을 만들어 배포할 수 있다. 기존 컨트랙트를 호출해 그 기능을 다른 프로그램의 일부로 사용하는 것도 가능하다. Ethereum은 이런 조합 가능성을 스마트컨트랙트 개발의 중요한 특성으로 설명한다. 스마트컨트랙트의 조합 가능성
특히 Ethereum Virtual Machine, 즉 EVM을 사용하는 개발자는 함수와 상태 변수로 프로그램을
작성하고 다른 컨트랙트의 함수를 호출하는 방식에 익숙하다. 예를 들어 A가 transfer(B, 60)을
호출하면 토큰 컨트랙트가 자신의 규칙에 따라 잔액을 갱신한다. 호출자는 함수에 의도를 전달하고,
실행 환경은 그때의 상태를 읽어 결과를 계산한다.
입력과 출력을 미리 구성하는 방식에서는 개발자가 제안할 상태 전이를 더 직접 다룬다. 함수 호출로 동작을 표현하는 방식에서는 프로그램이 실행 중 필요한 상태를 읽고 다음 상태를 계산한다. 어느 쪽을 선택하느냐에 따라 클라이언트와 실행 환경이 나눠 맡는 역할이 달라진다.
UTXO에서도 스마트컨트랙트를 구현할 수 있다. 앞서 참고한 Cardano의 eUTXO는 출력에 데이터와 스크립트 검증을 결합한다. 따라서 니고프로토콜에서 풀고 싶었던 문제는 UTXO에 처음으로 프로그램을 붙이는 것이 아니었다. 명시적인 상태 전이를 사용할 수 있게 하면서, 익숙한 컨트랙트 실행 방식도 같은 원장 위에서 수용하는 것이었다.
자산 출력과 컨트랙트 상태를 담는 공통 단위
이 요구에서 출발한 최소 상태 단위가 StateCell이다. 셀은 원장 안에서 식별할 수 있는 하나의 상태이며, 식별자와 소유자, 값의 구조를 식별하는 정보, 실제 값을 함께 가진다.
니고의 Native 자산, 즉 원장이 자산으로 직접 다루는 영역에서는 앞의 UTXO 예제처럼 자산 출력을 셀로 표현할 수 있다. A의 100이 담긴 셀을 소비하고, B의 60과 A의 40을 담은 셀들을 생성하는 식이다. Cell 거래는 사용할 입력과 만들 출력을 명시하고, 원장과 필요한 프로그램이 그 전이를 검증한다.
EVM 컨트랙트의 상태도 같은 StateCell 기반 저장소에 기록한다. EVM은 상태를 슬롯이라는 저장 위치로 읽고 쓰는데, 니고는 최종 256비트 슬롯 하나를 하나의 셀로 표현한다. Solidity의 변수·배열·매핑을 어떤 슬롯에 배치할지는 기존 컴파일러와 EVM의 규칙을 따른다.
여기에는 자산 출력과 다른 점이 있다. A의 잔액을 한 슬롯에 저장하는 컨트랙트라면, 앞의
transfer(B, 60) 호출로 그 슬롯의 값이 100에서 40으로 바뀐다. 저장 계층은 기존 셀을 소비하고,
같은 ID에 값이 40인 셀을 생성하는 교체로 처리한다. B의 잔액 60도 B에게 해당하는 슬롯의 셀에
반영한다. 자산 UTXO처럼 매번 새로운 출력 ID를 부여하는 방식으로 모든 상태를 강제하지 않는다.
이렇게 하면 자산의 입력·출력 전이와 컨트랙트 실행 결과를 셀의 소비와 생성이라는 공통 구조로 반영할 수 있다. 상태를 변경하는 실행 방식은 각각 유지하면서, 그 결과를 저장하고 확정하는 기반을 공유하는 것이다.
공통 저장 단위를 사용하더라도 변경 권한까지 같아지는 것은 아니다. 사용자가 EVM 슬롯을 자산 출력처럼 임의로 소비하고 새 값으로 바꿀 수는 없다. 해당 컨트랙트의 실행과 검증 규칙을 거쳐야 한다. 무엇을 저장하는지와 누가 바꿀 수 있는지를 함께 설계해야 StateCell이 원장의 단위가 될 수 있다.
또한 EVM 상태를 셀로 저장한다고 일반 EVM 호출의 접근 범위가 제출 전에 모두 알려지는 것은 아니다. 현재 니고의 일반 EVM 거래는 순차 실행 경계로 처리한다. StateCell은 병렬성 분석에 사용할 공통 단위를 제공하지만, 각 실행 방식에서 독립성을 확인하는 일은 여전히 필요하다.
공통으로 다룰 상태와 프로그램에 맡길 규칙
소유기간 연구에서 시작한 질문으로 돌아오면, 원장에 기대했던 것은 여러 자산의 상태 변화를 공통 구조로 다룰 기반이었다. 어떤 상태가 소비되고 무엇이 새로 생겼는지 같은 형식으로 표현할 수 있으면, 각 프로그램의 서로 다른 내부 구조를 읽기 전에 원장 차원에서 변경 관계를 다룰 출발점이 생긴다.
그 위에서 보유기간을 어떤 권리로 바꿀지, 수익배분 구간을 어떻게 정할지는 프로그램과 정책의 문제로 남길 수 있다. 과거 이력을 얼마나 보존하고 어떻게 조회할지도 별도로 설계해야 한다. StateCell이라는 현재 상태 단위만으로 소유기간 계산이나 과거 상태의 증명이 자동 완성되지는 않는다.
StateCell을 선택하면서 정리한 방향은 이렇다. 자산과 컨트랙트 상태를 공통 단위로 표현하고, 명시할 수 있는 전이는 거래에 드러내며, 새로운 업무 규칙은 프로그램으로 확장한다. 그리고 각 영역의 변경 권한과 검증 책임을 그 위에 세운다.
다음 글에서는 이 책임을 구체적으로 살펴보려 한다. 셀의 owner는 무엇을 뜻하는지, 소유자가 있다는
사실과 상태를 바꿀 권한은 어떻게 연결되는지, 프로토콜 코어와 Native Program, EVM 컨트랙트는
무엇을 나누어 맡는지가 그 내용이다.
한 가지 질문도 남는다. 상태를 잘게 나눌 수 있어도 많은 거래가 결국 같은 상태를 갱신해야 한다면 어떻게 될까. AMM처럼 풀의 준비금 변화가 다음 거래의 가격에 영향을 주는 경우가 그렇다. 이 공유 상태의 경합은 StateCell의 구조를 설명한 뒤 다시 다룰 설계 과제다.
StateCell 설계 시리즈 1편. 예제 숫자는 상태 모델을 설명하기 위한 가정이며, 처리량이나 운영 성능을 제시한 것은 아니다.