체크포인트를 받는 형태 — .chk · .binpb.zst · gRPC

이 글은 Sui 블록체인 데이터 뜯어보기 시리즈의 2번째 글입니다.

Sui 로고

개요

1편에서는 Sui의 원본 데이터 단위인 체크포인트가 무엇이고, 공개 HTTPS 스토어·버킷·full node gRPC 중 어디서 받을 수 있는지 살펴봤습니다. 이번 편에서는 그렇게 받은 체크포인트가 실제로 어떤 바이트로 이루어져 있는지 열어 봅니다.

같은 체크포인트라도 받는 방법에 따라 형태가 다릅니다.

  • .chk: 스토어에 올라가던 옛 파일 포맷
  • .binpb.zst: 스토어의 현재 권장 파일 포맷
  • gRPC 응답: full node에 요청하거나 구독해서 받는 메시지

이 글에서는 세 형태를 차례로 열어 보고, 무엇이 같고 무엇이 다른지 비교합니다. 미리 결론을 말하면, 셋은 결국 같은 체크포인트를 다른 봉투에 담은 것이고, 어느 형태로 받든 원본 내용을 그대로 읽으려면 BCS(Binary Canonical Serialization, Sui의 직렬화 규칙)를 다룰 줄 알아야 합니다.

이 글의 사실과 수치는 2026-10-03 기준입니다. 이후에 확인한 사실과 측정값은 날짜를 함께 적었습니다.


1. 스토어의 파일 포맷 세 가지


공식 문서의 포맷 안내

체크포인트 스토어는 Sui의 sui-checkpoint-blob-indexer라는 프로그램이 채웁니다. 공식 문서(Running a Remote Store)는 이 프로그램이 만드는 파일을 이렇게 안내합니다.

파일내용문서의 평가
{seq}.binpb.zstzstd로 압축한 protobuf 체크포인트"This is the expected format."
{seq}.binpb압축하지 않은 protobuf 체크포인트"Not recommended."
{seq}.chkBCS로 인코딩한 체크포인트"Deprecated and being decommissioned. Do not enable this pipeline."

.binpb와 .binpb.zst는 압축 여부만 다릅니다. 압축 옵션 없이 실행하면 .binpb가 만들어지는데, 문서는 이를 권하지 않습니다. 인덱서 프레임워크는 스토어에서 .binpb.zst만 찾기 때문에, 압축하지 않은 파일은 아예 읽히지 않습니다. 그래서 이 글에서는 .binpb.zst와 .chk 두 가지만 다룹니다.


.chk 발행 중단

문서는 .chk가 "폐지 중"이라고만 하고 날짜는 밝히지 않습니다. 실제 공개 스토어를 확인해 보면 .chk는 이미 새로 올라오지 않습니다(2026-10-08 확인).

  • 마지막 .chk 파일은 330625964(2026-10-05 17:25 UTC 업로드)이고, 그다음 번호부터는 .chk 요청이 404를 돌려줍니다.
  • 같은 번호의 .binpb.zst는 계속 올라오고 있습니다.
  • 이미 올라간 .chk 파일은 스토어의 30일 보관 기간이 끝날 때까지 받을 수 있습니다.

스토어의 진행 상황을 알려 주는 watermark 파일(_metadata/watermarks/checkpoint_bcs.json)은 330625694에서 멈춰 있습니다. 실제 마지막 파일보다 270개 앞선 번호입니다. watermark는 파일을 올릴 때마다 갱신되는 것이 아니라 주기적으로 기록되기 때문입니다. 그래서 watermark는 "적어도 여기까지는 올라왔다"는 하한으로 읽어야 합니다.

이 글의 기준일(2026-10-03)에는 .chk가 아직 발행되고 있었습니다. 앞으로 새로 받는 체크포인트는 .binpb.zst뿐이지만, 과거 데이터를 다룬 자료나 도구에는 .chk가 계속 등장하므로 구조는 알아 둘 만합니다.


2. .chk — BCS 그대로


바이트 구조

.chk 파일의 구조는 아주 단순합니다. Sui 소스의 sui-storage/src/blob.rs를 보면, .chk는 다음 두 부분을 그대로 이어 붙인 것입니다.

[ 1바이트: 인코딩 태그 0x01 ] ++ [ BCS로 직렬화한 CheckpointData ]
  • **첫 바이트 0x01**은 "뒤의 데이터가 BCS로 인코딩되어 있다"는 표시입니다(BlobEncoding::Bcs = 1). 체크포인트 내용의 일부가 아니므로, 파싱할 때는 이 1바이트를 건너뛰고 시작합니다.
  • 압축도, 길이 정보도 없습니다. 파일 전체가 곧 CheckpointData 하나입니다.

실제 파일의 앞부분을 보면 이 구조가 그대로 드러납니다.

$ curl -s https://checkpoints.mainnet.sui.io/330625964.chk | head -c 17 | xxd
00000000: 01f7 0400 0000 0000 00ac f3b4 1300 0000  ................
00000010: 00                                       .
바이트값의미
011인코딩 태그 (BCS)
f7 04 00 00 00 00 00 001271epoch (u64, 리틀엔디안)
ac f3 b4 13 00 00 00 00330625964체크포인트 번호 (u64, 리틀엔디안)

태그 바로 뒤에 epoch과 체크포인트 번호가 8바이트씩 나옵니다. BCS는 정수를 고정 길이 리틀엔디안으로 쓰기 때문에, 파일 전체를 파싱하지 않고도 이런 값을 바로 읽을 수 있습니다. BCS 규칙과 나머지 필드는 3편에서 자세히 다룹니다.


CheckpointData의 구성

태그 뒤의 CheckpointData는 Sui 소스(sui-types/src/full_checkpoint_content.rs)에 정의된 구조체입니다. 크게 세 부분으로 나뉩니다.

CheckpointData
├─ checkpoint_summary      체크포인트 요약 + 집계 서명 (CertifiedCheckpointSummary)
│    ├─ summary            epoch, 번호, 누적 트랜잭션 수, 이전 digest, timestamp …
│    └─ 서명               BLS 집계 서명 + 서명자 bitmap
├─ checkpoint_contents     포함된 트랜잭션·effects digest 목록 (CheckpointContents)
└─ transactions[]          트랜잭션마다 하나씩 (CheckpointTransaction)
     ├─ transaction        트랜잭션 본문 + 사용자 서명 (Transaction)
     ├─ effects            실행 결과
     ├─ events             발생한 이벤트 (없을 수 있음)
     ├─ input_objects      실행 전 Object들
     └─ output_objects     실행 후 Object들

1편에서 본 개념이 그대로 대응됩니다. checkpoint_summary가 validator들이 서명하는 요약이고, checkpoint_contents는 그 요약이 digest로 보증하는 트랜잭션 목록입니다. transactions에는 각 트랜잭션의 실제 내용이 들어 있습니다.

이 구조에서 눈여겨볼 점은 두 가지입니다. 다음 장의 .binpb.zst와 비교할 때 차이가 나는 부분입니다.

  • 트랜잭션에 사용자 서명이 함께 붙어 있습니다. transaction은 트랜잭션 데이터와 사용자 서명을 묶은 형태(SenderSignedData)입니다.
  • Object가 트랜잭션마다 따로 들어 있습니다. 같은 Object를 여러 트랜잭션이 건드리면, 그 Object는 트랜잭션마다 중복해서 저장됩니다.

3. .binpb.zst — protobuf 봉투 + zstd


바이트 구조

.binpb.zst는 이름 그대로 두 겹입니다. 안쪽은 protobuf(Protocol Buffers, Google의 바이너리 메시지 포맷)로 인코딩한 메시지이고, 바깥은 그것을 zstd로 압축한 것입니다. 같은 체크포인트 330625964를 받아 열어 보겠습니다.

$ curl -s -O https://checkpoints.mainnet.sui.io/330625964.binpb.zst
$ xxd 330625964.binpb.zst | head -1
00000000: 28b5 2ffd 0058 3443 047e f0c6 221b 3310  (./..X4C.~..".3.

첫 4바이트 28 b5 2f fd는 zstd 파일임을 나타내는 magic number입니다. 압축을 풀면 protobuf 메시지가 나옵니다.

$ zstd -d 330625964.binpb.zst -o 330625964.binpb
$ xxd 330625964.binpb | head -2
00000000: 08ac e7d3 9d01 1ac3 010a c001 0a11 4368  ..............Ch
00000010: 6563 6b70 6f69 6e74 5375 6d6d 6172 7912  eckpointSummary.

이 메시지의 타입은 Sui gRPC API에 정의된 sui.rpc.v2.Checkpoint입니다. 최상위 필드는 7개입니다.

번호필드330625964에서
1sequence_number330625964
2digest(비어 있음)
3summary195 B
4signature228 B
5contents4,176 B
6transactions (반복)22건, 61,701 B
7objects153,252 B

크기를 보면 objects가 전체의 3분의 2 이상을 차지합니다. 이 필드의 정체는 뒤에서 .chk와 비교할 때 다시 보겠습니다. 위 hex 덤프에 CheckpointSummary라는 글자가 그대로 보이는데, 이것이 다음 섹션의 주제입니다.


protobuf는 봉투일 뿐

protobuf는 원래 값마다 이름 붙은 필드를 두는 형식입니다. 스키마에 summary.epoch라는 필드가 있다면, 메시지를 디코딩하는 것만으로 checkpoint.summary.epoch처럼 값을 바로 꺼내 쓸 수 있어야 합니다. 실제로 sui.rpc.v2.Checkpoint 스키마에는 summary.epoch, summary.timestamp, transactions.balance_changes 같은 필드가 모두 정의되어 있습니다. 그런데 .binpb.zst를 디코딩해 보면 이 필드들은 비어 있습니다. 대신 각 부분이 이런 모양입니다.

summary      { bcs { name: "CheckpointSummary",  value: <BCS 바이트> } }
contents     { bcs { name: "CheckpointContents", value: <BCS 바이트> } }
transactions { transaction { bcs { name: "TransactionData",    value: … } }
               effects     { bcs { name: "TransactionEffects", value: … } }
               events      { bcs { … } } }
objects      { objects { bcs { name: "Object", value: … }, object_id, version } … }

즉 protobuf는 BCS 바이트를 담는 봉투로만 쓰입니다. 스토어를 채우는 sui-checkpoint-blob-indexer가 FieldMask(받을 필드를 고르는 목록)로 각 부분의 bcs 필드만 골라 담기 때문입니다. 예외는 몇 가지뿐입니다.

  • 최상위 sequence_number
  • 체크포인트 집계 서명(signature: epoch, 48바이트 BLS 서명, 서명자 bitmap)
  • Object를 찾기 위한 object_id와 version

결론은 분명합니다. .binpb.zst로 받아도 체크포인트 내용을 읽으려면 결국 BCS 파서가 필요합니다. protobuf를 풀면 BCS 덩어리가 나오고, epoch이나 timestamp는 그 BCS 안에 있습니다. 이 점에서 .binpb.zst와 .chk는 생각보다 가깝습니다. 둘 다 내용물은 BCS이고, 담는 방식만 다릅니다.


.chk와 무엇이 다른가

.chk와 .binpb.zst의 내용물은 모두 BCS지만, 담는 방식에서 세 가지가 다릅니다.

1. 사용자 서명의 위치

.chk는 트랜잭션마다 데이터와 사용자 서명을 묶어서(SenderSignedData) 둡니다. .binpb.zst의 transactions에는 서명 없는 트랜잭션 데이터(TransactionData)만 들어 있고, 트랜잭션별 서명 필드는 비어 있습니다. 서명이 사라진 것은 아닙니다. 사용자 서명은 contents의 BCS 안에 함께 들어 있고, 공식 인덱서 프레임워크도 디코딩할 때 거기서 서명을 꺼내 각 트랜잭션에 다시 붙입니다. 서명이 있는 위치가 다를 뿐입니다.

2. Object를 담는 방식

.chk는 트랜잭션마다 실행 전·후 Object(input_objects, output_objects)를 따로 둡니다. 여러 트랜잭션이 같은 Object를 건드리면 같은 Object가 여러 번 저장됩니다. .binpb.zst는 체크포인트 전체의 Object를 objects 한곳에 모으고 (object_id, version)으로 찾게 합니다. proto 정의도 같은 Object가 여러 트랜잭션에 등장할 때 중복을 없애려는 설계라고 설명합니다.

3. 담긴 정보의 범위

.chk로 변환할 때 일부 정보가 빠집니다. 트랜잭션이 실행 중에 읽기만 하고 바꾸지 않은 Object 목록(unchanged_loaded_runtime_objects)이 그렇습니다. Sui 소스에서도 이 변환을 "lossy"라고 주석으로 밝혀 두었습니다. .binpb.zst는 이 정보도 보존합니다.

크기는 왜 1/4일까

1편에서 .binpb.zst가 .chk의 약 1/4이라고 했습니다. Object 중복 제거 덕분일 것 같지만, 압축을 풀어 보면 대부분은 zstd 압축의 효과입니다.

체크포인트.chk.binpb.zst압축 해제한 .binpb
330625964222,158 B48,058 B219,637 B
32560000065,291 B22,018 B74,450 B

330625964는 2026-10-08, 325600000은 2026-09-24에 받아 잰 값입니다.

압축을 푼 protobuf 메시지는 .chk와 크기가 비슷하고, 오히려 더 큰 경우도 있습니다. 봉투(protobuf 필드 태그, bcs.name 같은 타입 이름)가 붙는 만큼 늘어나기 때문입니다. 결국 두 파일의 크기 차이는 거의 전부 압축 여부에서 나옵니다. .chk는 압축하지 않은 파일이기 때문입니다.


4. gRPC로 받기


같은 Checkpoint 메시지

full node의 gRPC API로 체크포인트를 받는 방법은 세 가지입니다.

  • LedgerService.GetCheckpoint: 번호나 digest로 하나를 받습니다.
  • LedgerService.ListCheckpoints: 범위를 지정해 여러 개를 받습니다.
  • SubscriptionService.SubscribeCheckpoints: 새 체크포인트를 실시간으로 구독합니다.

세 메서드 모두 앞에서 본 것과 같은 sui.rpc.v2.Checkpoint 메시지를 돌려줍니다. 정말 같은지 확인하려고, 체크포인트 329671000을 .binpb.zst와 GetCheckpoint 두 경로로 받아 필드 단위로 비교해 봤습니다(2026-10-06). gRPC 요청에는 공식 인덱서 프레임워크가 쓰는 것과 같은 필드 목록을 지정했습니다.

.binpb.zst 압축 해제gRPC GetCheckpoint 응답
크기318,529 B318,525 B
sequence_number있음없음 (요청 필드에 넣지 않음)
summary · contents · transactions · objects바이트까지 같음바이트까지 같음
signature서명자 83명서명자 82명

체크포인트의 내용은 바이트 하나까지 같았습니다. 그런데 서명이 달랐습니다. 체크포인트의 서명은 여러 validator의 서명을 합친 집계 서명이고, 누구의 서명을 합쳤는지가 함께 기록됩니다. 두 인증서는 그 명단이 달랐습니다(83명과 82명, 겹치는 validator는 72명). 1편에서 본 대로 집계는 validator마다 각자 하고, 서명을 모으다가 stake 2/3를 넘는 순간 그때까지 모인 서명으로 완성됩니다. 서명이 도착하는 순서는 validator마다 다르므로 완성된 명단도 조금씩 다를 수 있습니다. full node는 이렇게 만들어진 인증서 중 하나를 받아 저장하므로, 스토어 파일과 gRPC 응답이 서로 다른 인증서를 담게 된 것입니다. 둘 다 stake 2/3를 넘는 유효한 인증서입니다.

여기서 실무적인 교훈이 하나 나옵니다. 같은 체크포인트인지 판단할 때 파일 전체의 해시를 비교하면 안 됩니다. 서명이 다르면 해시도 달라지기 때문입니다. 체크포인트의 동일성은 summary의 digest로 판단해야 합니다. 체크포인트 digest는 서명을 빼고 summary만으로 계산하므로, 어느 인증서가 붙어 있든 같은 값이 나옵니다.


read_mask로 필드 고르기

gRPC로 받을 때의 가장 큰 장점은 받을 필드를 고를 수 있다는 점입니다. 요청에 read_mask(앞에서 본 FieldMask)로 필드 경로 목록을 넘기면, 서버가 그 필드만 채워 돌려줍니다.

buf curl --protocol grpc https://<FULL_NODE_URL>/sui.rpc.v2.LedgerService/GetCheckpoint \
  -d '{ "sequenceNumber": 330625964, "readMask": "sequenceNumber,summary.epoch,summary.timestamp" }'

여기서 두 가지를 알아 두면 좋습니다.

  • read_mask를 생략하면 최소한의 필드만 옵니다. GetCheckpoint의 기본값은 sequence_number,digest뿐입니다. protobuf의 일반적인 관례(mask가 없으면 전부)와 반대이므로, 전부 받으려면 *를 명시해야 합니다.
  • 구조화 필드를 요청하면 서버가 채워 줍니다. .binpb.zst에서는 비어 있던 summary.epoch, summary.timestamp 같은 필드를 gRPC로 요청하면 값이 들어옵니다. 필요한 값이 이런 필드로 충분하다면 BCS 파서 없이도 쓸 수 있습니다.

고르는 필드에 따라 전송량은 크게 달라집니다. 체크포인트 329671000 근처에서 잰 체크포인트 하나당 크기입니다(2026-10-03).

요청 필드크기
최소 필드 (번호, summary, 트랜잭션 digest·성공 여부·가스, 잔고 변동) — 스트림약 4.4 KB
.binpb.zst와 같은 필드 — 스트림약 331 KB
.binpb.zst와 같은 필드 — 단건 요청(압축 응답)약 91 KB
전체(*) — 단건 요청(압축 응답)약 176 KB (압축 전 817 KB)

같은 스트림이라도 최소 필드와 전체 원본은 약 75배 차이가 납니다. 하루 체크포인트 수(37~39만 개)를 곱하면 약 1.7 GB와 약 124 GB의 차이입니다. 무엇이 필요한지 알고 있다면, 원본 전체를 받는 대신 필요한 필드만 구독하는 편이 훨씬 가볍습니다.


스트림 구독과 빈틈 메우기

실시간으로 받을 때는 SubscriptionService.SubscribeCheckpoints로 스트림을 엽니다. 새 체크포인트가 만들어지는 대로 서버가 계속 보내 줍니다. proto 정의는 필터를 걸지 않은 스트림이라면 매 응답에 체크포인트가 순서대로, 빈틈없이(in order and without gaps) 담긴다고 설명합니다.

다만 이 스트림에는 중요한 제약이 있습니다. 이어 받기(resume)가 안 됩니다.

Subscriptions do not support resumption. A new subscription always begins at the current tip of the chain as seen by the server.

(구독은 이어 받기를 지원하지 않습니다. 새 구독은 항상 서버가 보는 체인의 현재 머리에서 시작합니다.)

연결이 끊겼다가 다시 구독하면, 끊긴 동안 만들어진 체크포인트는 건너뛰고 지금 시점부터 받게 됩니다. 그래서 빈틈은 LedgerService.ListCheckpoints로 따로 메워야 합니다. 공식 문서가 권하는 순서는 이렇습니다.

  1. 구독을 먼저 엽니다.
  2. 첫 응답에 담긴 체크포인트 번호를 기억합니다.
  3. 마지막으로 받았던 번호 다음부터 그 번호 직전까지를 ListCheckpoints로 받습니다.
  4. 백필(backfill, 지나간 구간을 채워 받는 작업)이 끝나면 이미 열어 둔 스트림을 이어서 읽습니다.

구독을 먼저 여는 이유는, 백필하는 동안 새로 생긴 체크포인트를 스트림이 받아 두게 하기 위해서입니다. 백필을 먼저 하면 그 사이에 생긴 체크포인트가 또 빈틈이 됩니다.

하나 더 알아 둘 점은 구독 스트림은 압축되지 않는다는 것입니다. Sui의 gRPC 서버 코드를 보면 LedgerService 같은 서비스는 압축 응답을 지원하지만, SubscriptionService에는 압축 설정이 없습니다. 앞의 표에서 같은 필드를 스트림으로 받으면 331 KB, 단건 요청으로 받으면 91 KB였던 차이가 여기서 나옵니다.

공식 인덱서가 이 제약을 실제로 어떻게 다루는지는 시리즈 마지막 편에서 다룹니다.


서버가 계산해 주는 값

gRPC 응답에는 .binpb.zst에 아예 없는 값도 담을 수 있습니다. 원본 체크포인트에는 없지만, 요청하면 서버가 그 자리에서 계산해 채워 주는 값입니다. Sui의 gRPC 서버 코드(GetCheckpoint 기준)에서 확인한 대표적인 것은 다음과 같습니다.

필드내용서버가 하는 일
transactions.balance_changes트랜잭션별 잔고 변동 (주소·코인 종류·금액)실행 전후 Object를 비교해 계산
transactions.events.events.json이벤트 내용을 읽을 수 있는 JSON으로이벤트의 BCS 바이트를 해석해 렌더링
transactions.checkpoint, transactions.timestamp트랜잭션이 속한 체크포인트 번호와 시각체크포인트 정보에서 채움

잔고 변동처럼 데이터를 분석할 때 가장 먼저 필요한 값이 원본에는 없다는 점이 흥미롭습니다. 원본 체크포인트만 가지고 이 값을 얻으려면 직접 계산해야 하고, 그 방법은 4편에서 다룹니다.

이 구분은 앞으로 중요합니다. 같은 체크포인트라도 원본에 있는 값과 서버가 만들어 준 값이 섞여 있고, 서버가 만든 값은 서버의 계산 방식에 따라 원본과 다르게 보일 수 있습니다. GraphQL·gRPC가 돌려주는 값을 원본과 직접 대조하는 일은 5편에서 해 보겠습니다.


5. 세 형태 정리


한눈에 비교

지금까지 본 세 형태를 표 하나로 정리하면 다음과 같습니다.

.chk.binpb.zstgRPC 응답
받는 곳공개 스토어·버킷공개 스토어·버킷full node, Archival Service
겉 포맷태그 1바이트 + BCSzstd + protobufprotobuf
내용물BCS (CheckpointData)protobuf 안의 BCS 덩어리요청한 필드 (BCS 덩어리 또는 구조화 필드)
압축없음zstd단건 요청은 가능, 구독 스트림은 없음
평균 크기 (1편 측정)약 307 KB약 76 KB요청 필드에 따라 4.4 KB ~ 수백 KB
사용자 서명트랜잭션마다 붙어 있음contents 안에요청한 필드에 따라
Object트랜잭션마다 따로 (중복)체크포인트 전체에 한 번.binpb.zst와 같음
서버 계산 값없음없음요청하면 있음 (잔고 변동, 이벤트 JSON 등)
상태폐지 (2026-10-05 발행 중단)현재 권장상시 수신의 공식 권장 경로

정리하면 세 형태는 같은 Checkpoint를 담는 서로 다른 봉투입니다.

  • .chk는 BCS를 그대로 담은 옛 봉투입니다.
  • .binpb.zst는 BCS를 protobuf로 한 번 감싸고 zstd로 압축한 현재의 봉투입니다.
  • gRPC는 필요한 필드만 골라 담고, 원하면 서버가 계산한 값까지 넣어 주는 봉투입니다.

누가 만들고 누가 읽나

마지막으로, 이 파일들을 실제로 만들고 읽는 공식 코드를 짚어 두겠습니다. 파서를 직접 만들 때는 이 코드를 참고하는 것이 가장 좋습니다.

만드는 쪽: sui-checkpoint-blob-indexer

체크포인트 스토어를 채우는 프로그램입니다. full node의 gRPC나 기존 스토어에서 체크포인트를 받아 세 가지 파이프라인으로 기록합니다.

  • checkpoint_blob: FieldMask로 필드를 골라 protobuf로 인코딩하고 zstd로 압축한 뒤 {seq}.binpb.zst로 저장합니다.
  • epochs: epoch마다 마지막 체크포인트 번호를 epochs.json에 기록합니다.
  • checkpoint_bcs: {seq}.chk를 저장하는 파이프라인입니다. 이미 폐지되었고, 공식 문서도 켜지 말라고 안내합니다.

읽는 쪽: sui-indexer-alt-framework

Sui의 공식 인덱서 프레임워크입니다. 스토어에서는 {seq}.binpb.zst만 요청합니다. .chk나 압축하지 않은 .binpb를 읽는 경로는 없습니다. 받은 파일은 다음과 같이 디코딩합니다(ingestion/decode.rs).

let decompressed = zstd::decode_all(bytes)?;                         // 1. zstd 해제
let proto_checkpoint = proto::Checkpoint::decode(&decompressed[..])?; // 2. protobuf 디코딩
Ok(Checkpoint::try_from(&proto_checkpoint)?)                          // 3. 안의 BCS를 풀어 Rust 타입으로

이 세 줄이 곧 이 글의 요약입니다. 압축을 풀고, protobuf 봉투를 열고, 안에 든 BCS를 해석합니다. 3번 단계에서는 contents 안의 사용자 서명을 꺼내 각 트랜잭션에 다시 붙이는 작업도 함께 일어납니다.

메시지 정의(sui.rpc.v2.Checkpoint)는 MystenLabs/sui-apis 저장소의 proto 파일에 있습니다.


마무리

이번 편에서는 같은 체크포인트를 세 가지 형태로 받아 열어 봤습니다.

  • .chk: 태그 1바이트 뒤에 CheckpointData의 BCS가 바로 이어지는 단순한 포맷입니다. 2026-10-05 이후 더는 발행되지 않습니다.
  • .binpb.zst: zstd로 압축한 protobuf Checkpoint입니다. protobuf는 필드를 구분하는 봉투 역할만 하고, 실제 내용은 그 안에 BCS 덩어리로 들어 있습니다. 공식 인덱서가 읽는 포맷도 이것 하나입니다.
  • gRPC 응답: 같은 Checkpoint 메시지입니다. read_mask로 필요한 필드만 고를 수 있고, 잔고 변동이나 이벤트 JSON처럼 서버가 계산한 값도 받을 수 있습니다.

결국 원본을 그대로 다루려면 어느 형태로 받든 마지막에는 BCS를 만나게 됩니다. 압축을 풀고 protobuf 봉투를 열면 그 안에 BCS 바이트가 있습니다.

다음 편에서는 그 BCS를 다룹니다. BCS 인코딩 규칙을 정리하고, CheckpointData를 바이트 오프셋 단위로 따라가며 필드를 읽습니다. 이어서 digest를 직접 계산해 원본을 검산하고, 전체를 파싱하지 않고 필요한 값만 꺼내는 방법도 살펴봅니다.


References

전체 목차:

  1. Sui 체크포인트 데이터 — 무엇이고 어디서 얻나
  2. 체크포인트를 받는 형태 — .chk · .binpb.zst · gRPC