ERC-8326 문서 묶음 해시 앵커 표준 (Canonical Document Bundle Anchor)
Final · 기타 · 2026-07-05 작성
여러 오프체인 문서를 정해진 규칙으로 정렬해 하나의 해시(번들 해시)로 묶고 이를 주제·역할별로 온체인에 기록하며 교체 이력까지 보존하는 표준입니다.
왜 필요한가
계약서, 증빙, 인증서처럼 체인 밖에 있는 문서 여러 개를 컨트랙트가 참조하려면 보통 문서들의 해시를 묶어 하나의 값으로 기록합니다. 그런데 공통 규칙이 없으면 구현마다 문서 항목을 적는 방식, 정렬 순서, 버전 구분, 교체 처리 방식이 제각각이 됩니다. 그 결과 두 시스템이 완전히 같은 문서를 들고 있어도 서로 다른 묶음 해시를 계산해 호환이 깨집니다. 또 원본 파일을 표준 형태의 바이트로 바꾸는 정규화(normalization) 단계와, 그 결과를 나열해 해시를 만드는 매니페스트(manifest) 단계가 뒤섞이면 형식별 처리 방식이 바뀔 때마다 전체 규칙이 흔들립니다. 이 표준은 두 단계를 분리하고 매니페스트 작성과 온체인 기록 방법을 고정해 같은 문서 집합이면 누구나 같은 해시를 얻도록 합니다.
구현해야 하는 것
function computeCanonicalBundleHash(DocumentEntry[] memory entries) internal pure returns (bytes32);: 항목들을 표준 순서로 정렬한 뒤 번들 해시를 계산합니다. 빈 묶음이면 되돌립니다.function computeBundleHash(DocumentEntry[] memory entries) internal pure returns (bytes32);: 이미 정렬된 항목을 요구하며 정렬되지 않은 입력이나 빈 묶음은 되돌립니다.
심층 설명
문서 엔트리의 다섯 필드
각 문서는 bytes32 다섯 개로 된 고정 폭 구조체 DocumentEntry로 표현합니다. contentHash는 정규화 프로파일이 출력한 바이트의 keccak256이어야 합니다. role은 묶음 안에서 문서가 맡는 기능이며 LEGAL_BASIS, EVIDENCE, CERTIFICATION, AGREEMENT, AMENDMENT, SUPPORTING 여섯 개가 정의되어 있고 응용이 추가할 수 있습니다. mimeTypeHash는 매개변수를 뺀 소문자 ASCII의 표준 IANA 미디어 타입을 keccak256한 값입니다. filenameHash는 /와 \를 경로 구분자로 보고 마지막 구분자 뒤만 남긴 뒤, ASCII 대문자를 소문자로 바꾸고 유니코드 NFC 정규화와 UTF-8 인코딩을 거쳐 keccak256한 값입니다. normProfileId는 어떤 변환으로 바이트를 만들었는지 식별하며 소비자는 이 값 없이 contentHash를 해석해서는 안 됩니다.
정규화 프로파일 세 가지
PROFILE_RAW는 입력 바이트를 변환 없이 그대로 씁니다. PROFILE_JSON_RFC8785는 RFC 8785 표준 직렬화의 UTF-8 출력을 쓰며 중복 키나 잘못된 유니코드, 홀로 있는 서러게이트, 상호운용 범위를 벗어난 숫자 등 RFC 8785와 I-JSON 제약을 어기는 입력은 거부해야 합니다. JSON 문자열 값에는 유니코드 정규화를 적용하지 않습니다. PROFILE_XML_C14N11은 주석 없는 Canonical XML 1.1 출력을 쓰고 외부 엔티티 해석을 반드시 꺼야 합니다. PDF, 이미지, 서명된 PDF, 일반 텍스트의 정규화는 이 표준이 정의하지 않으므로 별도 합의가 없으면 PROFILE_RAW를 쓰는 것이 권장됩니다. 커스텀 프로파일은 NORM:CUSTOM:<namespace>:<format>:<version> 형식을 권장하며 정확한 변환 규칙과 테스트 픽스처를 정의해야 합니다.
정렬 규칙과 번들 해시 유도
항목들은 role, filenameHash, contentHash, mimeTypeHash, normProfileId 순서의 다섯 키를 각각 오름차순으로 비교해 사전순으로 정렬해야 하며 앞 키가 모두 같을 때만 다음 키로 넘어갑니다. 같은 항목의 중복은 유지되어 결과 해시에 영향을 줍니다. 각 항목은 다섯 필드를 abi.encodePacked로 이어 keccak256한 leaf가 되고 번들 해시는 SCHEMA_V1(keccak256("ERC-8326:BUNDLE:V1"))과 모든 leaf를 이어 붙여 keccak256한 값입니다. 모든 요소가 고정 폭 bytes32이므로 packed 인코딩에 경계 모호성이 없습니다. 묶음에는 최소 한 개의 항목이 있어야 하고 호환되지 않는 미래 스키마는 SCHEMA_V1을 재사용하지 말고 새 식별자를 써야 합니다.
앵커링 규칙과 슬롯
기록은 (bundleHash, subjectId, role) 세 값으로 키가 정해지며 같은 번들 해시를 다른 주제나 역할 아래 독립적으로 기록할 수 있습니다. (subjectId, role) 조합인 슬롯에는 유효한 묶음이 최대 하나만 존재합니다. anchorBundle은 해시, 주제, 역할, 문서 수가 0이면 거부하고 이미 있는 트리플이나 유효한 묶음이 있는 슬롯도 거부합니다. 점유된 슬롯의 교체는 supersedeBundle만 쓸 수 있습니다. 성공 시 anchoredBy에 msg.sender, anchoredAt에 uint64(block.timestamp)를 기록하고 BundleAnchored를 발생시킵니다. metadataURI는 비워 둘 수 있으며 빈 값은 온체인 조회 포인터가 없다는 뜻입니다. 앵커링은 승인된 호출자로 제한해야 하고 그 권한 방식은 구현이 정하되 문서화해야 합니다.
교체와 추가 전용 이력
supersedeBundle은 새 해시, 주제, 역할, 문서 수가 0이거나 옛 해시와 새 해시가 같으면 거부합니다. 옛 기록은 존재해야 하고 아직 교체되지 않았으며 해당 슬롯의 유효한 묶음이어야 하고 새 트리플은 아직 없어야 합니다. 교체는 원자적으로 옛 기록의 superseded를 true로, supersededBy를 새 해시로 바꾸고 새 유효 기록을 만들고 슬롯을 갱신하며 BundleSuperseded와 새 기록의 BundleAnchored를 함께 발생시킵니다. 옛 기록의 나머지 필드는 바뀌지 않고 계속 조회할 수 있어 감사 추적이 보존됩니다. 권한 없는 다른 앵커 권한자가 남의 슬롯을 가로채서는 안 됩니다.
선택 사항인 복구 확장
앵커 키가 유출되거나 슬롯이 무단 점유될 수 있는데, 관리자가 직접 교체를 시도하면 현재 주인이 먼저 교체해 버리는 선행매매(front-running)에 당할 수 있습니다. 그래서 복구 확장은 슬롯 주인을 원자적으로 재지정하는 방식을 씁니다. 이 확장을 구현하면 첫 앵커링과 교체 성공 시 슬롯 주인이 msg.sender로 설정되어야 합니다. assignSlotPrincipal은 승인된 복구 관리자만 부를 수 있고 재지정 후 이전 주인은 일반 앵커 권한만으로는 그 슬롯을 교체할 수 없어야 합니다. 주인 지정 자체가 앵커 권한을 주지는 않으며 관리자 신뢰가 필요하므로 핵심 인터페이스와 분리되어 있어 불변 권한을 원하는 배포는 생략할 수 있습니다.
소비자 검증 절차와 ERC-165
유효한 묶음에 의존하는 소비자는 여섯 단계를 지켜야 합니다. activeBundle을 조회해 bytes32(0)이면 거부하고 getAnchor로 기록을 받아 필드가 요청한 해시·주제·역할과 일치하는지, anchoredAt과 documentCount가 0이 아닌지, superseded가 false인지 확인한 뒤, 매니페스트와 정렬, 번들 해시를 오프체인에서 재현합니다. documentCount는 신고 값일 뿐 컨트랙트가 검증한 값이 아닙니다. 준수 레지스트리는 ERC-165를 구현해 IDocumentBundleAnchor 인터페이스 ID에 true를 돌려줘야 하고 복구를 구현하면 IDocumentBundleAnchorRecovery에도 true를 돌려줘야 합니다. ERC-165는 인터페이스 지원만 알려줄 뿐 해시의 올바른 유도나 문서의 진위를 보장하지 않습니다. 이 표준은 Final 상태로 확정되었습니다.
쓸 때 주의할 점
- 정규화 프로파일 신뢰: 결정성 보장은 프로파일 구현만큼만 강합니다. PROFILE_RAW는 아무 정규화도 하지 않고 커스텀 프로파일은 모호하거나 악의적이거나 불완전할 수 있으므로 소비자는 선택된 프로파일을 확인하고 변환을 직접 재현해야 합니다.
- 온체인은 정규화를 검증하지 않음: 해시 라이브러리는 항목 필드만 받을 뿐 JSON이나 XML 등을 파싱하거나 정규화하지 않습니다. 호출자가 프로파일을 따랐다고 거짓 주장하며 해시를 제출할 수 있으므로 소비자는 정규화와 항목 유도를 오프체인에서 재현해야 합니다.
- 문서 가용성과 프라이버시: 앵커된 해시가 원본 문서를 제공해 주지는 않습니다. metadataURI는 사라지거나 내용이 바뀌거나 민감 정보를 노출할 수 있으므로, 개인·기밀·법적 제한 정보를 공개 URI에 직접 넣어서는 안 됩니다.
- documentCount는 신고 값: 컨트랙트는 documentCount가 실제 매니페스트와 일치하는지 검증할 수 없습니다. 소비자는 저장된 값을 그대로 믿지 말고 재현한 항목 수를 직접 세어야 합니다.
- 슬롯 선점과 교체 선행매매: 앵커 권한이 있는 호출자는 주인이 없는 슬롯을 먼저 차지할 수 있으므로 권한 범위를 좁히거나 보호할 슬롯에 주인을 미리 지정해야 합니다. 분쟁 슬롯은 직접 교체 대신 원자적 주인 재지정을 써야 선행매매를 막을 수 있으며 복구 관리자에게는 강한 운영 통제가 필요합니다.