soobook
CRYPTO

TON 101

무한 확장이라는 한 가지 출발점을 잡으면 단일 world state도, 동기 호출도, EOA도, 영구 무료 storage도 모두 성립하지 않는다. TON은 그 가정 위에서 다시 설계한 체인이다.

블록체인을 처음 배우면 거의 모든 그림이 EVM의 어휘로 그려진다. EOA와 컨트랙트, 동기 함수 호출, 단일 world state, linear chain.

이 어휘에 박힌 가정을 의식하지 못한 채 받아들이게 된다. TON은 그 가정 대부분을 다른 결정으로 바꾼 체인이다.

계정은 모두 컨트랙트이고, 컨트랙트끼리는 동기로 호출할 수 없고, 데이터는 머클 DAG로 관리되고, 주소는 코드 해시로 결정되고, 체인은 계정 단위까지 분할되고, 영구 저장에는 임대료가 붙는다.

이 결정들은 서로 떨어져 있지 않다. 한 결정이 다음 결정으로 이어진다. 무한 확장을 출발점으로 받아들이면 단일 world state를 포기해야 하고, 단일 state를 포기하면 동기 호출을 못 쓰게 되고, 동기를 포기하면 메시지 모델로 가야 하고, 메시지 모델 위에서는 actor가 가장 깔끔한 추상이 된다. 이 글은 그 결정 연쇄를 따라간다.

 

모든 계정이 actor, 모든 호출이 메시지\textbf{\text{모든 계정이 actor, 모든 호출이 메시지}}

 

Actor Model: 비동기 메시지가 1급 시민

TON에는 EOA가 없다. 모든 주소가 컨트랙트로 뒷받침된다.

사용자의 personal wallet도 컨트랙트, 토큰을 받기만 하는 임시 주소도 컨트랙트, jetton minter도 DEX 풀도 elector도 모두 컨트랙트다.

사람이 키를 들고 있는 형태의 단순 계정은 protocol 차원에 존재하지 않는다.

사용자는 ed25519 keypair를 갖지만 그 키의 역할은 자기 wallet contract에 들어오는 외부 메시지에 시그니처를 다는 것뿐이다.

여기서 중요한 점은 단지 명명의 문제가 아니라 모든 계정을 actor로 본다는 실행 모델 결정이라는 사실이다.

actor 모델의 본질은 세 가지다.

  1. 자기 state는 자기만 변경한다
  2. 다른 actor와는 메시지로만 통신한다
  3. 메시지를 받으면 새 메시지를 보내거나, 자기 state를 바꾸거나, 새 actor를 생성한다

TVM은 동기 호출 instruction을 아예 갖고 있지 않다. 컨트랙트가 다른 컨트랙트의 함수를 호출해 return value를 받는 패턴이 protocol 차원에 없다. message-passing이 유일한 통신 수단이다.

그럼 Finalize 는? 연쇄적으로 호출 필요할 때에는? Upgrade 는?

sequenceDiagram
    participant A as Contract A
    participant Net as TON network
    participant B as Contract B

    Note over A,B: tx 1 (block N)
    A->>Net: send_internal_message(B, payload)
    Net-->>A: tx end, state committed

    Note over A,B: tx 2 (block N+1+)
    Net->>B: deliver message
    B->>Net: send_internal_message(A, response)
    Net-->>B: tx end

    Note over A,B: tx 3 (block N+2+)
    Net->>A: deliver response
    A-->>Net: tx end

이 그림이 EVM 사용자에게 가장 낯선 부분이다. A → B → A 흐름이 한 트랜잭션이 아니라 세 개의 트랜잭션 으로 쪼개진다. 각 단계 사이에 블록이 끼어들 수 있고, B가 A에 응답하기 전에 다른 메시지가 끼어들 수도 있다.

approve 다음 swap을 atomic하게 묶는 EVM 패턴이 자연스럽게 작동하지 않는다. 두 호출이 두 트랜잭션이고, 그 사이에 state가 변할 수 있기 때문이다. 그래서 jetton(TON의 ERC-20 대응) 표준은 transfer를 두 단계 commit + 응답 패턴으로 설계해 비동기 환경에서의 일관성을 컨트랙트 수준에서 명시한다.

Atomicity가 약해진 대신 reentrancy가 EVM 같은 형태로는 발생하지 않는다. 한 메시지의 처리가 끝나야 다음 메시지가 들어오므로, 호출 도중 외부에서 자기 state가 다시 진입하는 시나리오 자체가 protocol에서 차단된다. 다른 종류의 race condition이 그 자리에서 발생한다.

Cells: 상태 자체가 머클 트리

TON의 데이터 단위는 byte도 word도 아닌 cell 이다.

한 cell은 최대 1023 bits의 data와 최대 4개의 다른 cell에 대한 reference를 담는다. cell이 다른 cell을 가리키며 만드는 그래프는 circular reference가 금지된 DAG다. 모든 데이터, 컨트랙트 코드, 블록의 트랜잭션 리스트, 큐, 계정 storage가 모두 이 cell DAG로 표현된다.

graph TD
    R[Root cell<br/>data + up to 4 refs]
    R --> C1[Cell A]
    R --> C2[Cell B]
    C1 --> C3[Cell C]
    C1 --> C4[Cell D]
    C4 --> C5[Cell E]

각 cell은 자기 data와 자식 cell들의 hash를 합친 값을 자기 hash로 갖는다. cell DAG의 root hash 하나가 그 아래 모든 cell의 머클 무결성을 봉인한다.

이 점이 protocol 차원에 박혀 있다는 사실이 핵심이다. EVM에서는 storage가 key→value 매핑이고, 머클 트리는 클라이언트 측에서 root를 계산하는 응용 단계의 일이다. TON은 데이터 모델 자체가 머클 DAG라서 다음 세 가지가 자연스럽게 따라온다.

우선 content addressing이 무료다. 동일한 hash의 cell은 storage에서 한 번만 카운트된다. 같은 sub-tree를 공유하는 컨트랙트가 많아도 dedup이 자동으로 이루어진다.

또 하나는 부분 증명이 무료라는 점. 큰 state 중 한 leaf cell만 머클 증명으로 노출 가능하다. light client나 cross-shard 메시지 인증이 별도 머클 라이브러리 없이 protocol primitive로 가능하다.

마지막으로 storage 회계의 단위가 명확하다. 계정 하나가 차지하는 비용은 그 계정 cell DAG의 총 bits와 cell 개수로 정확히 계산된다.

bit 1023이라는 어색한 숫자는 cell 자체에 1 bit의 metadata 여유를 남긴 결과다. 한 byte 단위가 아니라 bit 단위로 압축되도록 설계해 storage 효율을 잘게 통제한다.

cells와 BoC(Bag of Cells, cell DAG의 직렬화 포맷) 위에 TON의 모든 데이터가 올라간다. 컨트랙트 코드도 cells, 컨트랙트의 storage도 cells, 메시지 페이로드도 cells, 블록도 cells. 데이터 모델이 한 종류로 통일된다.

Address = hash(stateInit)

TON의 주소 생성 규칙은 이더리움과 다른 결정을 명시적으로 보여준다.

address=workchain_idhash256(stateInit)\text{address} = \text{workchain\_id} \,\|\, \text{hash}_{256}(\text{stateInit})

여기서 stateInit는 컨트랙트의 초기 코드 cell과 초기 data cell을 묶은 cell 구조다. 즉 주소가 곧 코드+초기상태의 머클 hash 다.

사용자 입장에서 wallet 생성이 어떻게 굴러가는지 보면 이 결정의 의미가 또렷해진다.

TON에서 “wallet”은 사용자가 직접 deploy하는 표준 컨트랙트의 버전 시리즈다. TON Foundation이 v1, v2, v3, v4, v5(W5)로 wallet 컨트랙트를 발전시켜 왔고, 버전마다 plugin 지원, gasless transaction, multi-signature 같은 기능 차이를 갖는다. 사용자가 새 지갑을 만든다는 행위는 이 표준 컨트랙트 중 하나를 자기 주소에 deploy하는 일이다.

흐름은 이렇다. wallet 앱이 새 ed25519 keypair를 만들고, 선택한 wallet 버전의 컴파일된 코드 cell을 가져온다. 거기에 사용자의 public key와 초기 seqno를 data cell로 만들어 둘을 묶으면 stateInit가 된다. 이 stateInit의 hash가 그대로 사용자의 TON 주소가 된다. 주소는 계산된 값이지 어디 등록하는 식별자가 아니다.

이 시점에 wallet은 아직 체인에 올라가 있지 않다. 누군가 그 주소로 TON을 처음 송금하면 잔액만 생기고 코드는 비어 있는 uninit 상태로 머문다. 사용자가 첫 트랜잭션을 보내기 위해 외부 메시지에 stateInit를 첨부하는 순간, 노드가 hash를 검증한 뒤 그 자리에 코드와 data를 deploy한다. wallet 활성화는 별도 deploy 트랜잭션이 아니라 첫 사용 트랜잭션과 한 몸이다.

이 결정이 만드는 효과가 세 가지다.

(1) Counterfactual deploy. 컨트랙트를 배포하지 않은 채 주소를 미리 계산할 수 있다. 누군가 그 주소로 송금해 두면, 첫 외부 메시지가 들어올 때 stateInit을 첨부해 그 자리에서 deploy하면 된다. wallet의 첫 활성화 패턴이 이걸 그대로 쓴다.

(2) 코드 무결성이 주소에 박힘. 어떤 주소에 deploy하려면 stateInit의 hash가 그 주소와 일치해야 한다. 즉 사용자가 주소만 보면 어떤 코드가 deploy됐는지를 protocol이 보장한다. EVM이 EIP-1167 minimal proxy나 EIP-2470 deterministic deployer로 사후 보강하던 일이 protocol primitive로 들어와 있다.

(3) 업그레이드의 근본적 어려움. 코드를 바꾸면 stateInit가 바뀌고 주소가 바뀐다. wallet을 v4에서 v5로 올리려면 새 stateInit으로 새 주소를 계산하고 거기에 새 wallet을 deploy한 뒤 옛 wallet의 자산을 새 wallet으로 송금해 옮겨야 한다. 옛 주소는 사라지지 않고 그대로 살아 있어, 외부에 공유해 둔 옛 주소로 들어오는 송금은 옛 wallet의 잔액에 그대로 쌓인다. 거래소·dapp·연락처에 알려둔 주소를 일일이 새 주소로 갱신하지 않으면 자산이 두 주소로 갈라지는 셈이다.

flowchart LR
    SI[stateInit<br/>code + initial data] -->|hash256| H[address]
    H -.->|deploy| C[contract<br/>at address]
    C -->|새 stateInit로 재배포| H2[NEW address]

이 비대칭을 우회하는 길이 두 갈래로 발전했다.

하나는 plug-in / extension 모델. wallet contract 본체는 그대로 두고, 추가 권한을 별도 cell로 위임한다. wallet v4가 plugin slot을 도입하고 v5(W5)가 extension을 일반화한 게 이 방향이다. 본체 코드는 안 바뀌므로 주소도 그대로다.

다른 하나는 SETCODE opcode. TVM 명령어 중 하나로, 컨트랙트가 실행 중에 자기 cell DAG의 코드 부분을 다른 cell로 교체하도록 허용한다. 주소는 deploy 시점의 hash 그대로 유지되고 코드만 갈아치워진다. EVM의 transparent proxy나 UUPS proxy가 별도 storage slot에 implementation 주소를 두고 delegatecall로 우회하던 업그레이드 패턴과 같은 효과를, TON은 protocol primitive 한 줄로 표현하는 셈이다.

단 이때 주소는 deploy 시점의 코드를 가리키는 historical claim일 뿐, 현재 실행되는 코드와는 다를 수 있다. address-as-codehash의 무결성 모델이 살짝 어긋난다. 그래서 SETCODE는 사용자 wallet에는 거의 쓰이지 않고, governance/admin 권한이 명확한 시스템 컨트랙트(elector, config 등)에 한정하는 것이 표준 관행이다.

address-as-codehash와 업그레이드 가능성은 본질적으로 trade-off다. EVM은 mutable address + extra integrity layer를, TON은 integrity-baked address + harder upgrade를 선택했다.

Infinite Sharding Paradigm

TON의 확장 모델은 한 줄로 요약된다.

각 계정이 자기만의 shard에 있다고 가정해도 protocol 의미는 같다.

이걸 가능하게 하는 것이 actor model이다. 컨트랙트가 다른 컨트랙트와 동기 호출할 일이 없으니, 두 컨트랙트가 같은 shard에 있을 필요도 없다. 메시지가 shard 경계를 넘어 routing되기만 하면 된다.

체인은 세 층으로 나뉜다.

graph TD
    M[Masterchain<br/>config + finalized state]
    M --> W0[Workchain 0<br/>basechain]
    M --> WN[Workchain ...<br/>up to 2^32]
    W0 --> S0[Shardchain<br/>prefix 0...]
    W0 --> S1[Shardchain<br/>prefix 1...]
    S0 --> S00[Shard 00...]
    S0 --> S01[Shard 01...]
  • Masterchain: 네트워크 config, 활성 validator 셋과 stake, 모든 workchain/shardchain의 최신 블록 hash를 보관하는 메타 체인. 단일 source of truth.
  • Workchain: 최대 2³² 개. 각 workchain은 자기 가상머신과 규칙을 가질 수 있는 논리 체인. 실제로는 basechain(workchain 0)이 거의 모든 트래픽을 처리.
  • Shardchain: 한 workchain이 부하에 따라 동적으로 분할한 하위 체인. 각 shard는 (workchain_id, shard_prefix)로 식별되고, account_id가 그 prefix로 시작하는 계정만 담는다. 한 workchain은 최대 2⁶⁰ shard로 분할 가능.

Workchain은 사용자가 임의로 만들 수 없다. masterchain의 config 컨트랙트가 활성 workchain의 정의(가상머신, gas 정책, 시작 블록 등)를 보관하고 있고, 새 workchain을 추가하거나 기존 workchain의 규칙을 바꾸려면 config 컨트랙트에 변경 제안을 올린 뒤 활성 validator들의 stake-가중 투표(3/4 다수)를 통과해야 한다.

현재 메인넷에 활성된 workchain은 사실상 두 개다. basechain(workchain 0)과 masterchain(workchain -1, 음수 ID 사용). 2³² 라는 상한은 이론적 여지일 뿐 추가된 적은 없다. 반면 shardchain은 protocol이 자동으로 split/merge하므로 거버넌스가 끼지 않는다.

shard의 동학은 단순한 규칙을 따른다.

부하가 임계 초과하면 prefix p의 shard가 p0, p1 두 절반으로 split된다. 부하가 임계 미만이면 인접한 두 shard가 다시 merge된다. validator는 매 블록마다 자기 shard의 부하를 보고 split/merge 결정을 발표하고, 결정 후 일정 시간(split_merge_delay, 기본 100초)이 지나면 적용된다.

shard 사이 메시지는 hyper-cube routing으로 전달된다. 송신 shard가 메시지를 자기 outbound queue에 넣으면, 다음 블록 단계에서 가까운 shard들을 거쳐 수신 shard에 도달한다. 모든 라우팅이 머클 증명 위에서 일어나기 때문에 shard 간 메시지의 위변조는 불가능하다.

노드와 Validator

TON에서 노드를 운영한다는 건 세 갈래로 나뉜다.

  • Full node: 전체 블록체인 state를 로컬에 보관하고 검증한다. validator나 liteserver의 기반이지만 자기가 합의에 참여하지는 않는다.
  • Liteserver: full node에 RPC 기능을 켠 형태. wallet 앱이나 dapp 백엔드가 트랜잭션을 보내고 state를 조회할 때 접속하는 엔드포인트다.
  • Validator: full node에 stake를 묶고 합의에 참여한다. 새 블록을 제안하고 다른 validator의 블록을 검증한다.

Validator로 등록하려면 masterchain에 주소를 두고 elector라는 시스템 컨트랙트에 일정량의 TON을 stake로 보낸다. elector가 매 election cycle(validators_elected_for, 약 18시간)마다 다음 라운드 validator 셋을 뽑고, 각 validator는 cycle 동안 자기에게 할당된 shard들의 블록을 만들어 낸다. stake가 클수록 더 많은 shard를 배정받지만, 담합 방지를 위해 한 shard를 담당하는 validator subset은 정기적으로 무작위 rotate된다.

masterchain의 합의에는 활성 validator 전원이 참여하고, 각 shardchain의 합의에는 그 cycle에 그 shard로 할당된 일부만 참여한다. 그래서 shard 수가 늘어나도 한 validator가 들고 있어야 할 부담은 일정 수준에 머문다. validator 운영 도구로는 TON Foundation이 배포한 mytonctrl이 사실상 표준이다.

무한 sharding의 핵심은 sharding 자체가 아니라, 각 메시지의 처리가 자기 shard 안에서 닫힌다는 가정 이다. EVM처럼 한 트랜잭션이 여러 컨트랙트를 동기로 호출하면 sharding이 의미를 잃는다. actor model이 sharding의 전제 조건이다.

Storage Rent: 영구 저장의 비용을 누가 내는가

이더리움은 deploy 한 번에 가스를 받고, 그 후로는 컨트랙트가 영원히 무료로 storage를 점유한다. State bloat이 protocol-level 부채로 누적되는 구조다.

TON은 다르게 푼다. 모든 컨트랙트는 시간이 가면서 자기 storage 비용을 자기 잔액에서 결제한다.

공식은 단순하다.

storage_fee=(bitsbit_price+cellscell_price)Δt216\text{storage\_fee} = \left\lceil \frac{(\text{bits} \cdot \text{bit\_price} + \text{cells} \cdot \text{cell\_price}) \cdot \Delta t}{2^{16}} \right\rceil

bit_pricecell_price는 network config의 param 18에서 가져오는 동적 값이다. bitscells는 그 계정 cell DAG의 누적 크기. Δt는 마지막 storage 결제 이후 흐른 시간.

매 트랜잭션의 storage phase에서 위 식만큼 잔액에서 차감된다. 트랜잭션이 한동안 없으면 다음 트랜잭션에서 누적분이 한꺼번에 빠진다. 잔액이 freeze_due_limit 아래로 떨어지면 컨트랙트가 frozen 상태로 전이한다.

stateDiagram-v2
    [*] --> Active: deploy
    Active --> Active: 메시지 처리,<br/>gas + storage 결제
    Active --> Frozen: 잔액 < freeze_due_limit
    Frozen --> Active: TON 송금 + 동일 stateInit 첨부
    Frozen --> [*]: 충분히 오래 frozen 후 deletion

frozen 상태의 메커니즘은 머클 hash 모델 덕분에 깔끔하게 풀린다. 동결되는 순간 컨트랙트의 code cell과 data cell이 storage에서 삭제 되고, 그 두 cell의 hash만 계정 정보에 남는다. 누군가 충분한 TON을 송금하고 동일한 cell들을 함께 첨부해 외부 메시지를 보내면, 첨부된 cell의 hash가 남아 있는 hash와 일치하는지 검증한 뒤 자료를 복원한다.

즉 storage에서 데이터를 지웠어도 무결성은 hash로 봉인되어 있다. cell-DAG 모델 위에서 storage rent의 마지막 한 조각까지 일관성 있게 처리된다.

Storage rent는 단순히 비용 회수 메커니즘이 아니라 block-space 정책의 명시화 다. 누가, 얼마나, 언제까지 영구 저장 자원을 쓸 권리가 있는지가 protocol에 박혀 있다. 이더리움 진영이 EIP-7444, EIP-7736 같은 state expiry 제안으로 뒤늦게 풀고 있는 문제를 TON은 출발점에서 푼 셈이다.

정리

TON의 디자인은 한 결정이 다음 결정으로 이어지는 연쇄다.

결정따라오는 결정
단일 world state 포기동기 호출 불가, 메시지 모델 필수
메시지 모델actor model, 모든 계정이 컨트랙트
Actor model계정 단위 sharding 가능 (Infinite Sharding)
Cells DAG머클 무결성, dedup, 부분 증명이 protocol primitive
Address = hash(stateInit)코드 무결성 자동, counterfactual deploy, 업그레이드 어려움
영구 storage 비용 명시Storage rent, freeze, hash 봉인 후 데이터 삭제

EVM이 원래 동기적이었던 모델에 비동기를 끼워 넣는 작업(rollup, account abstraction, intent)을 하고 있다면, TON은 처음부터 비동기로 짠 모델에 동기 호출의 편의를 끼워 넣는 작업(jetton 두 단계 transfer, atomic-like 패턴)을 하고 있다.

같은 분산 시스템 문제를 어느 쪽에서 출발해 풀 것인가의 차이.

TON을 EVM 어휘로 읽으면 자꾸 어색하다. “지갑이 컨트랙트라고? 가스를 누가 내? 두 호출을 어떻게 묶어?” 같은 질문이 끝없이 나온다. 어휘를 바꾸면 모든 게 깔끔해진다. “actor가 메시지를 받아 자기 state를 업데이트한다” 한 문장으로 위 모든 것이 같이 설명된다.

actor model을 블록체인의 1급 추상으로 끌어올린 결과가 TON이다. 설계 원리는 그 한 가지 결정에서 모인다.

References