과거에 없던 상태와, 지금 꺼낼 수 없는 상태
원장에 “그때 이 자산이 있었나요?”라고 물었는데, 기록을 찾을 수 없다는 답이 돌아왔다. 그 시점에 정말 없었다는 뜻일까? 아니면 답한 노드가 그때의 자료를 갖고 있지 않다는 뜻일까?
현재 잔액을 확인할 때는 잘 드러나지 않는 차이다. 하지만 과거의 소유를 확인하거나 거래 결과를 감사하려면 두 답을 구분해야 한다. 자료를 꺼낼 수 없다는 이유로 “없었다”고 판단하면, 원장의 사실이 조회한 노드의 보관 사정에 따라 달라진다.
병렬 실행의 정확성에서는 계산이 끝나는 순서가 달라도 같은 원장 결과에 도달해야 한다는 조건을 살펴봤다. 이번에는 그 결과를 만든 뒤의 질문이다. 시간이 지난 다음에도, 그때 원장에 무엇이 있었는지 확인할 수 있을까?
지금 A에게 80이 있다는 사실만으로는 부족하다
A가 자산 100을 갖고 있다가 B에게 20을 보냈다고 하자. 니고는 자산의 소유자와 수량을 StateCell이라는 기록에 담을 수 있다. 이 예제에서는 기존 기록을 소비하고 새로운 기록을 만드는 방식으로 자산을 이전한다.
블록은 여러 거래의 결과를 순서대로 담는 단위이고, 높이는 그 블록의 순번이다.
높이 10의 블록이 끝났을 때 A의 100이 u0라는 기록에 담겨 있었다.
높이 11부터 19까지 이 자산에는 변화가 없고, 높이 20의 블록에서 이전이 일어났다고 하자.
거래는 u0를 소비하고 A의 80을 담은 u1, B의 20을 담은 u2를 만든다.
u0·u1·u2는 서로 다른 기록을 구분하는 설명용 이름이다.
수수료 금액과 추가 발행·소각은 없고, A와 B에게 이 자산의 다른 기록도 없다고 가정한다.
높이 20 종료 시점에는 u1과 u2가 남으며 합계는 100이다. u0의 수량을 80으로 덮어쓴 것이 아니다. u0는 현재 사용할 수 있는 상태에서 빠지고, 과거에 존재했다는 사실은 별도로 확인해야 한다.
현재의 상태만 읽으면 A의 80과 B의 20을 찾을 수 있다.
그러나 “높이 10이 끝났을 때 A가 100을 갖고 있었는가?”라는 질문에는 그때의 자료가 필요하다.
지금 u0가 없다는 답을 열 블록 전에도 없었다는 뜻으로 사용할 수는 없다.
먼저 질문을 한 기록으로 좁혀보자. “높이 10 종료 시점에 u0가 있었고, 그 내용이 A의 100이었는가?”
예제의 전제를 알고 있는 우리는 이것을 A의 잔액과 연결할 수 있다.
실제 원장에서 한 기록의 존재를 확인하는 일과, A에게 속한 모든 기록을 빠짐없이 찾아 합산하는 일은 별개다.
과거를 묻기 전에 기준 상태를 고정한다
“높이 10”이라고만 말하면 검증의 기준이 충분하지 않다. 어떤 블록을 가리키는지, 그 블록을 처리한 뒤 어떤 상태가 남았는지도 함께 고정해야 한다.
니고의 과거 상태 조회는 블록 높이, 블록 해시, 상태 루트를 함께 지정한다. 해시는 내용을 정해진 길이의 요약값으로 바꾸는 계산으로, 내용의 변화를 대조하는 데 쓴다. 블록 해시는 해당 블록을 식별하는 값이고, 상태 루트(state root)는 그 시점의 원장 상태를 해시로 묶어 만든 요약값이다. 조회한 기록이 그 요약값에 맞는지를 대조하는 데 사용한다.
이후 “높이 10의 상태”라고 쓸 때는 이 세 값이 정해진 요청을 뜻한다. 노드가 그 높이의 이력을 확보했다면, 요청한 블록 해시와 상태 루트가 보관한 기준과 일치하는지 확인한다. 다르면 조회를 거절한다. 그 높이의 이력 자체가 없다면 먼저 자료를 확보하지 못했다고 답해야 한다. 기준이 다른 요청을 기록이 없다는 답으로 바꾸면 엉뚱한 상태에 대한 질문을 정상 처리한 셈이 된다. 반대로 여러 블록에서 상태가 바뀌지 않아 같은 루트가 반복되더라도, 그것만으로 블록의 시점까지 같아지지는 않는다.
시점의 단위도 중요하다. 여기서 보관하고 확인하는 것은 확정된 블록이 끝난 뒤의 상태다.
같은 블록 안의 거래들이 중간에 만들었다가 소비한 모든 상태를 그 블록 종료 상태에서 찾을 수 있다는 뜻은 아니다.
u0가 높이 10에서 어느 거래 직후 잠깐 존재했는지와, 높이 10이 끝났을 때 남아 있었는지는 다른 질문이다.
변경 경로, 과거 값, 증명은 맡은 일이 다르다
이전 거래의 기록을 보면 u0를 소비하고 u1과 u2를 만들었다는 연결을 따라갈 수 있다.
StateCell의 소유와 변경 권한에서 설명한
리니지(lineage)는 이렇게 상태가 어떤 거래를 거쳐 만들어지고 바뀌었는지 나타내는 관계다.
이 연결은 “어떻게 여기까지 왔는가?”에 답하는 데 유용하다. 하지만 거래의 입출력 기록을 찾았다는 것만으로, 임의의 과거 시점에 그 기록이 활성 상태였다는 증명까지 자동으로 생기지는 않는다. 어느 블록이 끝난 상태인지에 맞춰 확인해야 한다.
과거 조회에는 서로 다른 역할의 자료가 필요하다.
- 그때의 Cell 원문은 소유자·수량을 비롯해 실제로 무엇이 기록돼 있었는지 알려준다.
- 그때의 상태 루트와 검증 경로는 그 원문이 요청한 상태에 속하는지 대조할 근거가 된다.
- 노드가 확보한 이력의 범위는 그 시점에 대해 답할 자료를 갖췄는지 알려준다.
상태 루트는 원문을 다시 펼칠 수 있는 압축 파일이 아니다. 요약값만 보관했다고 과거의 소유자와 수량을 복원할 수 있는 것도 아니다. 반대로 원문만 남아 있어도 그것이 내가 묻는 시점의 상태에 포함됐는지 확인할 근거가 필요하다.
니고의 영속 저장소를 사용하는 노드는 과거 Cell 원문과 확보 범위를 보관한다.
u0가 현재 상태에서 소비되더라도 이 보관 원문이 함께 없어지는 것은 아니다.
새 블록을 반영할 때 현재 상태와 그 블록의 이력 자료를 같은 저장 작업에서 함께 반영해,
현재 상태만 갱신됐는데 그 시점의 이력을 확보했다고 잘못 표시하는 상황을 막는다.
다만 모든 노드가 처음부터 모든 이력을 받은 것은 아니다. 특정 높이의 상태 묶음인 스냅샷으로
참여한 노드는 그 시점부터 출발할 수 있다. 높이 20의 상태로 시작했다면 현재의 u1과 u2를 갖고 있어도,
그보다 앞선 높이 10의 u0 원문과 검증 자료를 받았다고 주장할 수는 없다.
“없었다”와 “제공할 수 없다”를 나눈다
다시 u0를 조회해 보자. 같은 기록에 대한 질문이라도 요청 시점과 노드의 보관 조건에 따라
답의 의미가 달라진다. 아래는 실제 API 호출 결과가 아니라, 차이를 설명하기 위한 경우들이다.
| 요청과 노드의 조건 | 답이 뜻하는 것 |
|---|---|
| 높이 10의 u0. 필요한 자료를 확보했고 조회를 허용한다 | 그때 u0가 있었으며 내용은 A의 100이다 |
| 높이 20의 u0. 필요한 자료를 확보했고 조회를 허용한다 | 그때 u0는 활성 상태에 없었다 |
| 높이 10의 u0. 노드는 높이 20의 스냅샷부터 시작했다 | 그때의 자료를 확보하지 못해 답할 수 없다 |
| 높이 10의 u0. 이력은 확보했지만 현재 보존 정책상 조회할 수 없다 | 지금 이 노드가 해당 시점의 자료를 제공하지 않는다 |
첫 두 답은 원장의 상태를 말한다. 뒤의 두 답은 응답하는 노드가 자료를 제공할 수 있는가를 말한다. 현재 같은 원장을 처리하는 노드끼리도 출발 시점과 보존 정책이 다르면 과거 질문에 답할 수 있는 범위가 달라진다.
니고는 이를 각각 FOUND, ABSENT, NOT_CAPTURED, NOT_RETAINED로 구분한다.
이름보다 중요한 것은 ABSENT의 의미다. 단순히 데이터베이스 검색 결과가 비었다는 뜻이 아니라,
요청한 상태 루트에서 그 Cell ID가 없음을 검증했다는 뜻이다.
높이 20의 u0가 없다는 증명은 과거에도 없었다거나, 그것을 소비한 거래의 기록까지 삭제됐다는 증명이 아니다.
NOT_RETAINED도 “디스크에서 이미 지웠다”로 읽어서는 안 된다.
자료가 물리적으로 남아 있어도 현재의 보존 정책상 그 상태에 대한 조회를 허용하지 않을 수 있다.
조회 가능 여부와 물리적인 삭제 완료는 서로 다른 판단이다.
어느 범주에도 끼워 넣어서는 안 되는 상황이 있다. 해당 시점의 이력을 확보했고 현재 보존 대상인데도
필요한 검증 자료나 Cell 원문이 손상되거나 누락된 경우다. 이때는 조회·검증 실패로 처리해야 한다.
손상된 자료 때문에 찾지 못한 것을 ABSENT로 바꾸면, 저장소의 문제를 원장의 사실로 위장하게 된다.
현재 값이나 다른 시점의 거래 기록으로 조용히 대신 답해서도 안 된다.
서버의 답을 받아서 직접 확인하려면
자료를 갖춘 서버가 “높이 10의 u0는 A의 100입니다”라고 답했다고 하자.
응답 내용을 읽을 수 있다는 것과 그 답을 검증할 수 있다는 것은 다르다.
니고의 단일 Cell 증명은 상태를 해시로 묶는 머클 트리(Merkle tree)의 경로를 사용한다. 각 기록의 식별자로 위치를 정하고, 기록 내용을 해시한 값들을 아래에서 위로 묶어 상태 루트를 만든다. 검증자는 전체 원장을 받는 대신, 관심 있는 기록과 그 위치에서 루트까지 계산하는 데 필요한 옆 갈래의 해시들을 받는다.
높이 10의 u0가 있다는 응답은 다음 순서로 확인할 수 있다.
- 응답이 내가 요청한 블록과 상태 루트,
u0를 가리키는지 확인한다. - 받은 Cell 원문을 해석해 ID와 내용을 확인하고, 정해진 형식으로 그 값의 해시를 계산한다.
u0의 ID가 정하는 위치에서 검증 경로의 해시들을 결합한다.- 계산한 루트가 확인하려던 상태 루트와 같은지 대조한다.
이렇게 기록이 해당 상태에 포함된다는 것을 확인하는 것이 포함 증명(membership proof)이다. 예제에서는 그 기록의 소유자가 A이고 수량이 100이라는 내용까지 원문에 묶여 있다. 원문의 수량만 바꿔 같은 경로를 내놓으면, 바뀐 해시로는 기준 루트에 맞는 증명을 만들 수 없다.
높이 20의 u0에 대해서는 반대로 그 ID의 위치가 비어 있음을 루트까지 대조한다.
이를 부재 증명(non-membership proof)이라고 한다.
“찾지 못했다”는 서버의 말에 그치지 않고, 지정한 상태에 포함되지 않는다는 검증 자료를 주는 것이다.
니고는 존재와 부재가 확인된 응답에 증명을 제공하며, 자료를 확보하지 못했다는 응답을 부재 증명으로 포장하지 않는다.
이 검증에는 서버의 데이터베이스에 다시 접속할 필요가 없다. 하지만 무엇과 대조할지 정하는 상태 루트는 따로 신뢰할 수 있어야 한다. 누군가 임의의 원장을 만들고 그 원장에 맞는 루트와 증명을 함께 내놓아도, 둘의 계산은 일치할 수 있기 때문이다. 그 블록과 루트가 내가 확인하려는 원장의 확정 결과인지 판단하는 것은 별도의 블록·합의 검증 책임이다. Cell 증명 자체가 블록의 확정성까지 보증하지는 않는다.
니고는 보관한 원문과 검증 경로를 함께 전달하는 단일 Cell 증명 기능을 제공한다. 이 글이 다루는 단위는 특정 상태의 특정 Cell 하나다. A가 소유한 모든 자산의 합계나, 일정 기간 내내 같은 수량을 보유했다는 증명으로 확대해서는 안 된다.
받은 증명을 검증하는 일과 다시 받는 일
높이 10의 u0 원문과 증명을 받아 기준 루트와 함께 보관했다고 하자.
이후 노드가 높이 10을 조회 대상에서 제외해도, 이미 받은 자료를 같은 루트에 대조하는 계산은 달라지지 않는다.
다시 검증할 때 필요한 것은 보관한 증명과 기준이지, 그 노드가 지금도 같은 데이터를 제공한다는 약속이 아니다.
반면 며칠 뒤 다른 이용자가 처음으로 같은 자료를 요청한다면 이야기가 다르다. 누군가는 Cell 원문과 증명을 만드는 재료를 계속 보관하고 제공해야 한다. 응답을 한 번 받았다고 그 상태가 자동으로 영구 보존되는 것도 아니다. 과거의 사실을 검증할 수 있다는 성질과, 필요할 때 자료를 계속 구할 수 있다는 성질은 분리된다.
자료를 만드는 동안의 보호도 필요하다. 증명 생성 도중 정리 작업이 필요한 트리 노드를 지워버리면 원문과 검증 경로를 온전히 읽을 수 없다. 니고는 허용된 조회가 보존 기준을 확인하고 자료를 읽어 검증하는 동안, 정리 작업이 필요한 자료를 지우지 못하도록 조회와 정리를 조정한다. 자료의 확보·검증을 마치면 읽기 보호도 끝나며, 확보한 자료로 응답을 만든다. 이 보호가 같은 상태를 이후에도 계속 제공한다는 약속은 아니다.
여기서부터는 증명 알고리즘만으로 답할 수 없다. 어느 시점까지 제공할지, 특별히 보존할 상태는 무엇인지, 자료를 제공하지 못할 때 어떤 답을 할지를 저장·운영 정책으로 정해야 한다.
소유기간을 묻던 질문으로 돌아가면
소유기간 컨트랙트를 검토한 글에서는 현재 잔액뿐 아니라 누가 어느 기간 동안 보유했는지를 기록하려 했다. 자산마다 반복해서 만드는 이력 관리 기능을 원장이 공통으로 제공하면 어떨까 하는 질문도 거기에서 나왔다.
과거 Cell 조회와 증명은 그 질문에 필요한 기반 하나를 제공한다.
예제에서는 높이 10의 u0와 높이 20의 u1·u2를 확인해 두 시점의 기록을 설명할 수 있다.
하지만 두 시점의 증명만으로 그 사이에 다른 변화가 없었다는 사실까지 확인한 것은 아니다.
높이 11부터 19까지 변화가 없다는 것은 이번 예제에서 준 가정이었다.
실제로 보유기간을 계산하려면 중간 변화와 누락 없는 기록 범위, 기간의 시작·끝을 해석하는 규칙이 더 필요하다. 여러 Cell에 나뉜 수량을 합산하는 일도 남는다. 단일 Cell 증명이 이 계산을 대신하지는 않지만, 계산에 사용하는 과거 값 하나가 어떤 상태에 속했는지 다시 확인할 수 있게 해준다.
처음의 질문에 답하려면, “기록이 없다”는 말부터 나누어야 했다. 그때 원장에 없었던 것인지, 노드가 그때의 자료를 받지 않은 것인지, 지금 제공하지 않는 것인지가 달랐다. 원장은 과거의 값을 보관하는 것과 함께 어떤 근거로 답하고, 어디까지 답할 수 있는지를 드러내야 한다.
그 다음에는 보관 비용의 질문이 남는다. 제공해야 할 증명 재료를 보호하면서, 더 이상 필요하지 않은 것은 어떻게 구분해 지울 수 있을까? 오래된 상태들이 검증 자료의 일부를 공유한다면, 한 상태를 보존 대상에서 뺐다는 이유만으로 관련 자료를 모두 지워도 될까? 이 질문은 과거 상태의 조회에서 불필요해진 저장 자료를 회수하는 GC(garbage collection)의 설계로 이어진다.