콘텐츠와 해싱
Label 309가 레코드를 콘텐츠에 결합하는 방식 — hashes 맵의 구조, 다이제스트가 무엇에 커밋하는지, 그리고 다수의 항목을 하나의 루트 아래에 기록하기 위한 Merkle 커밋먼트.
콘텐츠 해시가 곧 주장입니다. Label 309 레코드가 존재에 관해 단언하는 모든 것은 콘텐츠 바이트의 암호학적 다이제스트에서 비롯되며, 이는 메타데이터 라벨 309 아래 온체인에 기록됩니다. 이 페이지에서는 그 다이제스트가 어떻게 담기는지, 정확히 무엇에 커밋하는지, 그리고 하나의 32바이트 루트가 어떻게 임의로 큰 항목 묶음을 대표할 수 있는지를 정의합니다.
hashes 맵
레코드의 각 항목은 hashes 맵을 담습니다. 이는 알고리즘 식별자에서 32바이트 원시 다이제스트로 이어지는 CBOR 맵입니다.
hashes = {
"sha2-256": h'…32 bytes…', ; key = algorithm id, value = raw digest
}키는 해시 레지스트리에서 가져온 텍스트 문자열 식별자이며, 값은 결코 16진수로 인코딩되지 않는 원시 바이트 문자열입니다. 이 맵은 적어도 하나의 항목을 포함해야 하며(MUST), 등록된 모든 해시 알고리즘은 정확히 32바이트를 생성합니다.
| 식별자 | 알고리즘 | 참조 | 다이제스트 |
|---|---|---|---|
sha2-256 | SHA-256 | FIPS 180-4 | 32 B |
blake2b-256 | BLAKE2b-256 | RFC 7693 | 32 B |
이 두 식별자는 모두 검증자가 구현해야 하는 필수 항목이므로, 둘 중 어느 하나만 사용한 단일 해시 레코드라도 어디서나 검증을 통과합니다. 인식할 수 없는 식별자를 만난 검증자는 해당 항목을 조용히 건너뛰는 대신 안정적인 오류 코드로 레코드를 거부합니다. 예약된 포스트 양자 슬롯을 포함한 전체 레지스트리는 알고리즘 레지스트리에 있습니다.
병렬 배열이나 {alg, digest} 하위 객체의 리스트가 아니라 CBOR 맵을 사용하는 것에는, 와이어 포맷 계약의 일부를 이루는 세 가지 귀결이 따릅니다. 첫째, CBOR 맵의 키는 고유하므로 알고리즘 중복은 구조적으로 불가능합니다. 둘째, 정규 CBOR은 키를 인코딩된 바이트로 정렬하므로 정규 순서가 자동으로 결정됩니다. 이에 따라 동일한 해시 집합을 표현하는 두 생산자는 바이트 단위로 동일한 맵을 출력하며, 그에 대한 레코드 수준 서명은 모두 안정적입니다. 셋째, 이 형태는 항목별 검증을 필요로 하지 않습니다. 구조 검증기는 각 키가 등록되어 있는지, 각 값이 해당 알고리즘의 다이제스트 길이를 갖는지만 확인합니다.
해시가 커밋하는 대상
다이제스트는 콘텐츠 바이트, 즉 생산자가 타임스탬프를 부여하려는 바로 그 정확한 바이트 시퀀스에 커밋합니다. hashes 맵의 모든 항목은 그 동일한 바이트 시퀀스를 명명된 알고리즘으로 해싱한 결과여야 합니다. 서로 다른 평문을 기술하는 항목을 담은 레코드는 비적합입니다. 검증자는 콘텐츠 바이트를 확보할 수 있는 경우 모든 다이제스트를 다시 계산해야 하며(MUST), 하나라도 일치하지 않으면 그 레코드를 거부합니다.
레코드가 암호화 봉투(enc)를 담고 있을 때, 해시는 암호문이 아니라 평문에 결합됩니다. 이는 의도적인 설계입니다. 존재 증명(Proof of Existence, PoE)은 작성자가 나중에 평문을 공개하여 그것이 특정 시점에 존재했음을 증명할 수 있도록 하기 위해 존재합니다. 암호문을 해싱하면 어떤 암호화된 데이터가 존재했다는 사실만 증명할 뿐, 그 기저의 콘텐츠에 대해서는 아무것도 말해 주지 않습니다. 따라서 봉인된 레코드도 어떤 평문에 타임스탬프가 찍혔는지를 정확히 증명합니다. 수신자는 복호화하여 평문 다이제스트를 다시 계산하고, 이를 온체인 커밋먼트와 대조합니다. 그러므로 enc를 담은 항목은 적어도 하나의 콘텐츠 해시 항목을 반드시 담아야 합니다. 그것이 없으면 다시 계산하여 대조할 평문 주장이 존재하지 않게 됩니다.
봉인되어 있더라도 결합 대상은 평문
봉인된 레코드의 온체인 다이제스트는 평문(클리어텍스트)의 다이제스트입니다. 암호문 자체는 콘텐츠
주소 지정 방식의 ar:// 또는 ipfs:// URI에 놓이므로, 스토리지 게이트웨이가 반환하는 바이트는
게이트웨이를 신뢰하지 않고도 그 주소에 비추어 위변조가 드러납니다. 수신자는 이를 복호화하고 평문
해시를 다시 계산하여, 온체인 주장까지 일관되게 결합합니다.
해시는 하나, 또는 여럿
단일 콘텐츠 해시로 완전히 적합합니다. 레지스트리에 있는 모든 256비트 해시에 대해, 현재 알려진 최선의 제2 역상 공격은 고전적 계산으로 2^256 부근에 위치합니다. 건전한 단일 256비트 해시 하나만으로도 레코드의 보관 수명 동안의 현실적인 위협 모델을 이미 충분히 다루며, 구조 검증기는 단일 항목 레코드에 대해 어떤 경고도 발생시키지 않습니다.
생산자는 선택적 심층 방어로서, 독립적인 설계 계열에 속하는 두 번째 항목을 추가할 수 있습니다(MAY). 예를 들어 sha2-256(SHA-2: Merkle–Damgård 구조)과 blake2b-256(BLAKE2: ChaCha에서 파생된 순열 위의 HAIFA 구조)을 함께 사용하는 식입니다. 이 두 계열은 구조적 계통을 전혀 공유하지 않으므로, 둘 다를 담은 레코드는 두 계열이 암호 분석으로 동시에 무너질 때에만 약화됩니다. 그 대가는 항목당 32바이트 다이제스트 하나와 그 짧은 식별자뿐입니다. 채택 여부는 생산자의 선택이며, 결코 강제되지 않습니다.
Merkle 배치 커밋먼트
단일 콘텐츠 해시는 단일 콘텐츠를 온체인에 기록합니다. 임의로 큰 모음 — 500개 파일의 CI 아티팩트 세트, IoT 이벤트 스트림, 감사 로그 배치 — 을 온체인에 기록하기 위해, Label 309는 최상위 merkle[] 배열을 정의합니다. 각 항목은 순서가 정해진 32바이트 리프 리스트에 커밋하고, 대응하는 32바이트 루트 하나를 온체인에 발행합니다. 순서가 정해진 리프 자체는 오프체인에 저장됩니다.
merkle = [
{
"alg": "rfc9162-sha256",
"root": h'…32 bytes…', ; canonical root over the ordered leaves
"leaf_count": 4, ; binds the on-chain root to the leaf-list size
"uris": [ … ], ; OPTIONAL — where the off-chain leaves list lives
},
]등록된 커밋먼트 알고리즘은 rfc9162-sha256입니다. 이는 RFC 9162 §2.1.1의 Merkle Tree Hash를, 기저 해시로 SHA-256을 사용하여 구성한 것입니다. 이는 리스트 커밋먼트 구조로서 콘텐츠 해시 레지스트리와는 구별됩니다. Merkle 루트는 리프 리스트 구조에 커밋하는 반면, sha2-256 다이제스트는 평문 바이트에 커밋합니다. 따라서 Merkle 루트는 hashes 내부가 아니라 자신만의 배열에 들어갑니다. 온체인 leaf_count는 루트를 오프체인 리스트의 크기에 결합하여, 동일한 루트를 공유하면서 어떤 리프 위치에서 크기가 다른 트리를 다시 구성하는 치환을 미리 차단합니다.
트리 구성
이 구조는 1바이트 도메인 분리 접두사로 리프와 내부 노드를 구별합니다. 리프에는 0x00을, 내부 노드에는 0x01을 사용합니다. 이로써 공격자가 리프와 충돌하는 내부 노드를 조작하는 것을 방지합니다. n ≥ 1인 32바이트 값의 순서 리스트 L = (d_0, …, d_{n-1})에 대한 Merkle Tree Hash는 다음과 같이 재귀적으로 정의됩니다.
MTH(L) = SHA-256(0x00 || d_0) when n == 1
MTH(L) = SHA-256(0x01 || MTH(L[0:k]) || MTH(L[k:n])) when n > 1
where k is the largest power of 2 strictly less than n여기서 중요한 귀결이 있습니다. 단일 리프는 리프 그 자체가 아니라 SHA-256(0x00 || d_0)으로 해싱됩니다. 따라서 리프가 하나뿐인 트리의 루트가 그 리프 자체와 같아지는 일은 결코 없습니다. 단일 콘텐츠에 타임스탬프를 찍으려는 생산자는 리프가 하나뿐인 Merkle 트리가 아니라 일반 sha2-256 또는 blake2b-256 항목을 직접 사용해야 합니다(MUST). 빈 트리(n == 0)는 금지됩니다.
이 구조는 순서에 민감합니다. 리프를 재배열하면 다른 루트가 나오므로, 생산자는 리프 리스트를 순서 있는 시퀀스로 다루고 발행, 보관, 그리고 이후의 어떤 증명 생성 과정에서도 그 순서를 보존해야 합니다.
오프체인 리프 리스트
루트는 리프 리스트가 없으면 쓸모가 없으므로, 생산자는 순서가 정해진 리프를 오프체인에 영속화합니다. 정규 아티팩트는 cardano-poe-merkle-leaves-v1 문서이며, 정규 CBOR(RFC 8949)로 인코딩됩니다. 내용은 32바이트 루트, 순서가 정해진 32바이트 리프 배열, 그리고 리프 수입니다.
leaves-list = {
"format": "cardano-poe-merkle-leaves-v1",
"tree_alg": tstr, ; registered list-commitment algorithm id
"root": bytes .size 32, ; raw 32 bytes, not hex
"leaves": [ + bytes .size 32 ], ; ordered raw 32-byte leaves
"leaf_count": 1..4294967295, ; 1 .. 2^32-1; MUST equal the length of `leaves`
? "leaf_alg": tstr, ; informative; no verification semantics
}검증자는 오프체인 리스트를 해석하고, 위의 구조에 따라 그 leaves에서 루트를 다시 계산한 뒤, 온체인 merkle[i].root와 바이트 단위로 대조합니다. 파일 내의 leaf_count는 온체인 leaf_count와 len(leaves) 둘 모두와 일치해야 합니다. 이 정규 CBOR 컨테이너가 유일한 규범적 리프 리스트 형식입니다. JSON 표현도 대체 직렬화도 존재하지 않으므로, 리프 리스트를 주고받는 두 구현은 언제나 바이트 단위로 비교 가능한 문서를 주고받게 됩니다.
포함 증명
배치화의 핵심은 선택적 공개입니다. 나머지를 다시 게시하지 않고도 — 심지어 공개하지조차 않고도 — 어떤 한 항목이 커밋된 리스트에 포함되어 있었음을 증명하는 것입니다. 한 리프에 대한 포함 증명이란, 그 리프에서 루트에 이르는 경로를 따라 늘어선 형제 노드 해시의 순서 리스트, 즉 O(log n)의 형제 경로입니다. 검증자는 RFC 9162에 따라 리프와 형제 노드를 트리 위쪽으로 접어 올리며, 재구성된 루트가 발행된 루트와 바이트 단위로 일치하는 경우에 한해서만 그 증명을 받아들입니다.
RFC 9162 트리는 2의 거듭제곱으로 패딩되지 않으므로, 불균형한 트리에서는 오른쪽 가장자리의 리프가 완전한 쪽에 있는 리프보다 짧은 경로를 가질 수 있습니다. 따라서 권위 있는 판정은 어디까지나 알고리즘적입니다. 즉, 접어 올린 결과가 루트를 재현하는지 여부이지, 결코 증명 길이의 비교가 아닙니다.
배치화가 중요한 이유
하나의 트랜잭션과 하나의 32바이트 루트가 수천, 수백만 개의 리프를 대표할 수 있습니다. O(log n)
증명을 가진 사람은 누구든 나중에 "이 항목은 내 리스트에 있었다"라고 보일 수 있는 한편, 공개되지
않은 모든 리프는 비공개로 남습니다. 루트는 그것이 커밋하는 리프에 대해 아무것도 드러내지 않습니다.