TON 트랜잭션 히스토리: lt, 해시, 그리고 아카이브 깊이
TON 트랜잭션 히스토리의 구조 — 논리 시간(lt), 해시, 마지막 트랜잭션 포인터. MCP로 에이전트가 히스토리를 읽는 법과 깊은 히스토리에 아카이브 노드가 필요한 이유를 정리했습니다.
밤늦게까지 작업하고 있는데 3시 47분에 메시지가 하나 날아옵니다. 사용자가 TON으로 주문 결제를 마쳤는데 봇이 그것을 인식하지 못했다는 내용입니다. 익스플로러를 열어보면 돈은 제자리에 있고 수신 송금도 그대로 보입니다. 그렇다면 문제는 블록체인이 아니라 여러분의 서비스가 트랜잭션 히스토리를 읽는 방식에 있습니다. 그리고 여기서 한 가지 사실이 드러납니다. TON의 트랜잭션 히스토리는 Ethereum에서 익숙해진 방식과 전혀 다르게 설계되어 있다는 것입니다.
TON에서 결제를 모니터링하는 일은 겉보기에 속을 만큼 단순합니다. 잔액을 폴링하다가 변화가 생기면 반응하면 된다는 식입니다. 하지만 실제로는 이 방식이 전부 무너집니다. 같은 금액의 결제가 1분 사이에 들어왔습니다. 한 건이었을까요, 두 건이었을까요? 사용자는 USDT를 보냈는데 GRAM 잔액은 꿈쩍도 하지 않습니다. 반년치 거래 내역이 필요한데 라이트서버는 최근 트랜잭션만 돌려주고 나머지에 대해서는 침묵합니다.
AI 에이전트를 TON에 연결하고 있거나 결제를 대조하는 백엔드를 작성하고 있다면 본론으로 들어가 보겠습니다. TON 트랜잭션 히스토리란 무엇인지, lt + hash 쌍이 왜 필요한지, 히스토리의 깊이가 왜 아카이브 노드에서 막히는지, 그리고 이 모든 것을 MCP로 에이전트가 어떻게 읽는지 살펴보겠습니다.
TON의 트랜잭션 히스토리란 무엇이고, 에이전트에게 왜 필요한가
TON에서는 지갑이든 컨트랙트든 제톤 지갑이든, 모든 계정이 자기만의 트랜잭션 체인을 가집니다. 각 트랜잭션은 GRAM 수령, 제톤 전송, 컨트랙트 메서드 호출처럼 수신 메시지를 처리한 결과입니다. 결제를 받거나 정산을 대조하는 에이전트(Claude, Cursor, 그 밖의 모든 MCP 클라이언트)에게 히스토리는 유일하게 신뢰할 수 있는 출처입니다. 잔액은 "지금 얼마인지"를 말해주지만, 히스토리는 "정확히 무엇이 언제, 누구로부터, 얼마나 일어났는지"를 말해줍니다.
에이전트에게 히스토리가 필요한 현실적인 작업들은 이렇습니다.
- 결제가 도착했는지 확인합니다("이 주소로 지난 한 시간 안에 송금이 들어왔는지 확인해줘").
- 지갑의 거래 내역을 뽑습니다.
- 식별자로 특정 트랜잭션을 추적합니다.
- 수신 제톤(예: USDT)을 예상 금액과 대조합니다.
문제는 TON에서 "100번부터 120번까지 트랜잭션을 달라"는 요청이 통하지 않는다는 점입니다. 여기에는 "N번 트랜잭션을 달라"고 말할 수 있는 전역의 연속된 블록 번호가 없습니다. 순서는 다른 방식으로, 즉 논리 시간으로 정해집니다.
논리 시간(lt): TON에 일반적인 블록 번호가 없는 이유
Ethereum에서는 모든 것이 단순합니다. 19,000,000번 블록이 있고 그 안에 트랜잭션이 순서대로 들어 있습니다. 전역 카운터 하나가 단조 증가하고, 모두가 동일한 번호를 기준으로 삼습니다.
TON은 멀티스레드 시스템입니다. 마스터체인, 워크체인, 그리고 부하에 따라 쪼개지고 합쳐지는 샤드들이 있습니다. 네트워크의 모든 이벤트를 선형으로 정렬할 수 있는 단일한 "블록 번호"가 여기에는 그냥 존재하지 않습니다. 그 대신 TON은 논리 시간(lt, logical time)을 사용합니다. 네트워크가 이벤트와 메시지, 트랜잭션의 순서를 정하는 단조 증가 카운터입니다. 이것이 보장하는 것은 하나입니다. 이벤트 A가 이벤트 B에 영향을 주었다면 lt(A) < lt(B)입니다.
여기서 실무적인 결론이 나옵니다. 트랜잭션의 위치는 블록 번호가 아니라 (lt, hash) 쌍으로 지정됩니다. lt는 순서(누가 먼저인지)를 담당하고, hash는 특정 트랜잭션을 명확히 식별하는 역할을 합니다. 둘을 따로 떼어놓으면 특정 트랜잭션을 집어내는 데 쓸 수 없습니다. 서로 다른 객체가 비슷한 lt를 가질 수 있고, hash만으로는 liteserver에게 어디서부터 읽기 시작해야 하는지 알려주지 못하기 때문입니다.
트랜잭션 해시와 lt + hash 쌍: 계정의 마지막 트랜잭션 포인터
트랜잭션 해시는 그 트랜잭션의 암호학적 지문입니다. TON에서는 base64 또는 hex 형태로 돌아옵니다. lt는 "순서상 언제인지"를, hash는 "정확히 어느 것인지"를 말해줍니다. 동일한 lt 단면에서는 이론적으로 모호함이 생길 수 있고, 해시가 없으면 liteserver가 트랜잭션을 내주지 않기 때문입니다. 이것만은 확실히 기억하십시오. (lt, hash) 쌍은 반드시 함께 있어야 합니다. lt만으로도, hash만으로도 부족합니다.
시작점은 어디서 얻을까요? 계정 상태는 가장 최근 트랜잭션을 가리키는 포인터를 보관합니다. 바로 last_transaction_id이며, 이것이 그 lt + hash 쌍입니다(필드는 last_trans_lt / last_trans_hash). 이것이 히스토리를 거슬러 순회할 때 출발점이 되는 목록의 "헤드"입니다.
TONNode의 MCP 도구 모음에서는 get_account_state가 이 일을 합니다. 계정의 상태와 플래그, 그리고 마지막 트랜잭션에 대한 포인터를 돌려줍니다.
get_transactions는 히스토리를 어떻게 넘기는가: lt + hash와 prev_trans 체인
이제 히스토리를 애초에 넘길 수 있게 해주는 핵심입니다. 모든 트랜잭션은 자신의 lt와 hash 외에 두 개의 필드를 더 가지고 있습니다. prev_trans_lt와 prev_trans_hash — 같은 계정의 이전 트랜잭션을 가리키는 참조입니다. 즉 트랜잭션들은 시간을 거슬러 올라가는 단방향 연결 리스트를 이루며, 그 헤드가 계정 상태의 last_transaction_id입니다.
get_transactions가 받는 인자는 이렇습니다.
account— 계정 주소- 시작점이 되는
lt와hash— 어디서부터 읽을지 지정 count— 배치 크기
이 도구는 지정한 지점에서 과거 방향으로 거슬러 올라가며 트랜잭션 배치를 돌려줍니다. 페이지 단위 순회 로직은 이렇습니다.
get_account_state에서last_trans_lt/last_trans_hash를 가져옵니다 — 이것이 헤드입니다.get_transactions(account, lt, hash, count)를 호출해 배치를 받습니다.- 배치의 마지막 트랜잭션에서
prev_trans_lt와prev_trans_hash를 꺼냅니다. - 그 쌍으로 다시
get_transactions를 호출합니다 — 이것이 다음 페이지입니다. prev_trans_lt가 0이 될 때까지(계정의 생애 시작점) 또는 필요한 깊이에 도달할 때까지 반복합니다.
TON의 페이지네이션은 이렇게 동작합니다. "2페이지"가 아니라 "이 (lt, hash) 쌍에서 이어서 계속"입니다.
최신 블록 대 깊은 히스토리: 아카이브 노드가 필요한 이유
바로 여기서 대부분이 걸려 넘어집니다. 일반적인 liteserver는 최신 블록과 계정의 최근 히스토리를 돌려줍니다. 제한된 상태 윈도우, 즉 최근 일정 개수의 블록만 보관하며, 네트워크가 커질수록 오래된 데이터는 서버에서 밀려납니다. prev_trans 체인을 충분히 깊이 따라가다 보면 어느 순간 liteserver는 그냥 트랜잭션을 돌려주지 않게 됩니다. 물리적으로 자기 윈도우 안에 더 이상 존재하지 않기 때문입니다. 방금 들어온 결제는 볼 수 있지만 반년 전 트랜잭션은 볼 수 없습니다.
이것은 버그가 아니라 설계입니다. 모든 계정의 전체 경로를 제네시스부터 보관하는 것은 비싸기 때문입니다. 깊은 히스토리를 보관하는 것은 아카이브 노드입니다. 오래된 블록을 버리지 않고 네트워크의 전체 역사에 걸친 완전한 상태를 유지하는 노드입니다.
실무적인 결론은 이렇습니다.
- 결제 감지, 최근 거래 내역, "지난 한 시간 안에 들어왔는지" — 일반 liteserver로 충분합니다.
- 첫날부터의 전체 지갑 감사 — 아카이브 노드가 필요합니다.
또 하나의 골칫거리는 TON 글로벌 설정에 들어 있는 공개 라이트서버입니다. 이들은 공용이고 한도가 걸려 있습니다. 부하가 걸리면 자주 not ready를 반환하거나 ADNL 타임아웃으로 빠지며, 깊은 히스토리도 갖고 있지 않습니다. 정확히 not ready를 만났다면 그것은 과부하된 공용 라이트서버의 증상입니다. 자세한 분석은 liteserver가 not ready를 반환하는 이유와 해결법에 정리했습니다.
TONNode에 대해 솔직하게 말씀드리겠습니다. 아카이브 노드는 로드맵에 있고 현재 동기화 중입니다 — 아카이브 깊이를 완성된 기능으로 약속드리지는 않겠습니다. 오늘 시점에 제공되는 것은 공개 라이트서버 복불복 없이, 관리되는 엔드포인트를 통해 최신 및 최근 히스토리를 읽는 것입니다. 제네시스부터의 완전한 아카이브는 별도 공지를 기다려 주십시오.
MCP로 AI 에이전트가 TON 히스토리를 읽는 법
TONNode는 TON용 hosted MCP 서버이며 도구는 정확히 16개입니다. get_transactions는 읽기 블록에 포함됩니다. MCP(Model Context Protocol)는 여러분이 손으로 ADNL 요청을 조립하지 않아도 에이전트가 스스로 도구를 호출하도록 정한 표준입니다. SDK를 설치하고 라이트서버를 띄우고 TL-B를 파싱할 필요가 없습니다. 에이전트가 도구를 직접 호출합니다.
무료 로컬 연결
읽기 도구 전체, 공개 설정, @tonnode/mcp 패키지 — 오픈소스이며 MIT 라이선스입니다.
{
"mcpServers": {
"ton": {
"command": "npx",
"args": ["-y", "@tonnode/mcp"]
}
}
}
이 패키지는 HTTP 중간 계층 없이 TON 네이티브 ADNL 프로토콜로 동작합니다.
전용 키를 쓰는 hosted 엔드포인트
처리량을 보장받으려면 전용 키가 있는 hosted 엔드포인트를 사용합니다.
{
"mcpServers": {
"ton": {
"type": "http",
"url": "https://mcp.tonnode.io/mcp",
"headers": { "Authorization": "Bearer tn_live_…" }
}
}
}
히스토리를 읽을 때 쓸모 있는 도구들입니다.
get_account_state— 순회의 시작점(last_trans_lt/last_trans_hash)과 계정의 상태, 플래그get_transactions— 트랜잭션 자체를(lt, hash)쌍 기준으로 페이지 단위 조회parse_address—EQ/UQ/raw사이의 오프라인 주소 변환get_jetton_info와get_jetton_balance— 제톤 히스토리를 다룰 때
실전: 결제 감지와 히스토리 페이지 순회
전형적인 시나리오를 만들어 보겠습니다 — "결제가 들어왔는지, 얼마인지".
에이전트에게 줄 프롬프트
ton도구로EQC…myshop에 대해get_account_state를 가져오고, 그다음 그last_trans_lt/last_trans_hash부터get_transactions를 count 20으로 호출해줘. 최근 10분 안에 들어온 5 GRAM 수신 송금을 찾아줘. 발신자 주소는 비교하기 전에parse_address로 EQ 형식에 맞춰줘. 더 깊은 히스토리가 필요하면 마지막 트랜잭션의prev_trans_lt/prev_trans_hash를 가져와get_transactions를 반복해줘.
에이전트가 알아서 체인의 헤드를 가져오고, 배치를 넘기고, 금액을 대조합니다.
페이지 순회 의사코드
같은 순회를 의사코드로 옮기면 이렇습니다(호출은 에이전트가 부르는 MCP 도구입니다).
state = get_account_state(account)
lt = state.last_trans_lt
hash = state.last_trans_hash
while lt != 0:
batch = get_transactions(account, lt, hash, count=20)
for tx in batch:
if matches_expected_payment(tx): # 원하는 금액/코멘트를 가진 수신 메시지
return tx
last = batch[-1]
lt = last.prev_trans_lt
hash = last.prev_trans_hash # hash 없이는 다음 페이지를 요청할 수 없습니다
디버깅 시간을 몇 시간 아껴주는 디테일
- 트랜잭션 필드의 주소는 raw 형태로 옵니다(
0:abcd…). 발신자나 수신자를 익숙한EQ…주소와 비교하기 전에parse_address로 양쪽을 같은 형식으로 맞추십시오. 그러지 않으면 같은 주소인데도 문자열 비교가 어긋납니다. 형식에 대한 자세한 내용은 TON 주소의 EQ, UQ, raw에 있습니다. - USDT 결제는 GRAM 지갑의 체인에서 보이지 않습니다. 제톤은 별도의 제톤 지갑에 보관되며, 그 주소는 온체인으로 계산됩니다(
get_jetton_balance가 이를 대신 해주고 현재 잔액도 돌려줍니다). 제톤 히스토리를 보려면 메인 지갑이 아니라 제톤 지갑의 트랜잭션을 순회하십시오. - raw 금액은 decimals로 환산하십시오. 제톤 금액은 최소 단위로 옵니다.
5000000을 사람이 읽는 5 USDT로 바꾸려면get_jetton_info에서decimals를 가져오십시오. USDT는 6이고 대부분의 제톤은 9입니다. 헷갈리면 1000배가 틀어집니다. 전체 분석은 TON에서 수신 USDT 결제를 잡아내는 법에 있습니다. - 중복 제거는 금액이 아니라
(lt, hash)로 하십시오. 동일한 결제 두 건은 오직 lt+hash 쌍으로만 구분됩니다. 이것이 곧 멱등성 키입니다.
히스토리 외에 컨트랙트의 현재 상태도 조회해야 한다면 run_get_method가 그 일을 합니다. SDK 없이 get 메서드를 호출하는 방법은 get 메서드로 SDK 없이 TON 읽기에 나와 있습니다.
요약
- TON에는 Ethereum 같은 블록 번호가 없습니다 — 트랜잭션의 위치는
(lt, hash)쌍으로 지정됩니다. lt는 순서, hash는 식별이며 둘은 반드시 함께 있어야 합니다. - 계정의 트랜잭션은
prev_trans_lt/prev_trans_hash로 이어진 연결 리스트입니다.get_account_state의last_transaction_id에서 시작해 거슬러 넘깁니다. - 일반 liteserver는 최신 및 최근 히스토리를 돌려줍니다. 전체 깊이는 아카이브 노드의 몫입니다.
- 제톤에서는
get_jetton_info의 decimals(USDT = 6)와,parse_address로 통과시켜야 하는 raw 주소를 잊지 마십시오.
not ready를 뱉는 공개 라이트서버와 씨름하고 싶지 않으신가요? get_transactions는 무료 Hobby 요금제에서 바로 사용할 수 있습니다 — 60 요청/분, 영구 무료, 카드 불필요, 로그인 직후 키 발급.
1분 만에 TON 히스토리 읽기를 연결하십시오 → tonnode.io/dashboard?plan=hobby
히스토리 읽기 말고 TON 위의 에이전트가 또 무엇을 할 수 있는지 궁금하다면 — 16개 도구 전체가 TONNode 도구 페이지에 있습니다.