ERC-137 이더리움 네임 서비스(ENS) 명세 (Ethereum Domain Name Service - Specification)
Final · 이름·메타데이터 · 2016-04-04 작성
'vitalik.wallet' 같이 사람이 읽기 쉬운 이름을 컨트랙트 주소나 콘텐츠 해시 같은 자원으로 바꿔 주는 이름 서비스의 규칙을 정합니다. 자원이 바뀌어도 이름은 그대로 둡니다.
왜 필요한가
이름 풀이 기능이 없으면 사용자는 긴 16진수 주소를 외우거나 복사해 써야 하고 지갑이나 호스트가 바뀔 때마다 새 주소를 다시 알려야 합니다. 원문은 기존 이더리움 이름 풀이 명세와 구현에도 한계가 있다고 지적합니다. 모든 이름이 하나의 전역 이름 공간에 들어가고 중앙 리졸버 하나가 풀이를 맡습니다. 하위 이름(서브도메인)을 위임하는 기능이 거의 없고 기록 종류가 하나뿐이라 같은 이름에 여러 자원을 달 수 없습니다. 또 전역 구현이 하나라 서로 다른 이름 배분 방식을 쓸 수 없고 이름 풀이와 등록, 소유자 조회(whois)가 한 곳에 뒤섞여 있습니다. 이 때문에 live.mysite.tld 같은 하위 이름, 한 이름 아래 스웜 앱과 위스퍼 주소와 메일 서버를 함께 두는 일, 기존 DNS 기록을 블록체인에 올리는 일이 어렵습니다.
구현해야 하는 것
function owner(bytes32 node) constant returns (address);: node의 소유자(레지스트라) 주소를 돌려줍니다.function resolver(bytes32 node) constant returns (address);: node를 담당하는 리졸버 주소를 돌려줍니다.function ttl(bytes32 node) constant returns (uint64);: node 정보를 캐시해도 되는 최대 시간(TTL)을 돌려줍니다.function setOwner(bytes32 node, address owner);: node 소유권을 다른 레지스트라로 넘깁니다. 현재 소유자만 부를 수 있고 Transfer 이벤트를 남깁니다.function setSubnodeOwner(bytes32 node, bytes32 label, address owner);: sha3(node, label)로 하위 node를 만들어 소유자를 정하거나 이미 있으면 소유자를 갱신합니다. NewOwner 이벤트를 남깁니다.function setResolver(bytes32 node, address resolver);: node의 리졸버 주소를 정합니다. 소유자만 부를 수 있고 NewResolver 이벤트를 남깁니다.function setTTL(bytes32 node, uint64 ttl);: node의 TTL을 정합니다. 레지스트리 기록과 리졸버가 돌려주는 정보 모두에 적용됩니다.function supportsInterface(bytes4 interfaceID) constant returns (bool): 리졸버의 유일한 필수 함수입니다. 주어진 4바이트 인터페이스 ID를 구현하면 true를 돌려줍니다.function addr(bytes32 node) constant returns (address);: node에 연결된 컨트랙트 주소를 돌려줍니다. 기록이 없으면 0 주소를 돌려줍니다.event AddrChanged(bytes32 indexed node, address a);: node의 주소 기록이 바뀔 때 반드시 발생시키는 이벤트입니다.
심층 설명
레지스트리, 리졸버, 레지스트라 세 부분
ENS는 세 부분으로 이뤄집니다. 레지스트리는 단 하나의 컨트랙트로, 등록된 모든 이름을 담당 리졸버에 연결하고 이름 소유자가 리졸버 주소를 바꾸거나 하위 이름을 만들 수 있게 합니다. 하위 이름은 상위 이름과 다른 소유자를 둘 수 있습니다. 리졸버는 이름에 대한 실제 자원 조회를 맡아 컨트랙트 주소, 콘텐츠 해시, IP 주소 등을 돌려줍니다. 레지스트라는 사용자에게 이름을 배분하며 ENS를 갱신할 수 있는 유일한 주체입니다. 레지스트리에서 어떤 node의 소유자가 곧 그 node의 레지스트라입니다. 레지스트라는 컨트랙트일 수도 외부 소유 계정일 수도 있지만 루트와 최상위 레지스트라는 최소한 컨트랙트로 구현될 것으로 봅니다. 이름 풀이는 두 단계입니다. 먼저 이름을 해시해 레지스트리에서 리졸버 주소를 얻고 그다음 리졸버에서 자원 종류에 맞는 함수를 호출합니다. namehash는 이름만으로 정해지므로 미리 계산해 컨트랙트에 넣을 수 있고 이름의 구성 요소 수와 무관하게 O(1)로 조회합니다.
이름 문법과 길이 권장
이름은 점으로 나뉜 레이블의 나열입니다. 각 레이블은 UTS46 기준의 정규화된 레이블이어야 하며 옵션은 transitional=false, useSTD3AsciiRules=true 입니다. 자바스크립트에서는 idna-uts46 라이브러리로 정규화와 검사를 할 수 있습니다. 대문자와 소문자 모두 쓸 수 있지만 UTS46 정규화가 해시 전에 대소문자를 접으므로 철자가 같고 대소문자만 다른 두 이름은 같은 namehash를 냅니다. 레이블과 도메인 길이에는 제한이 없지만 기존 DNS와의 호환을 위해 레이블은 64자 이하, 전체 ENS 이름은 255자 이하를 권장합니다. 같은 이유로 레이블이 하이픈으로 시작하거나 끝나지 않고 숫자로 시작하지 않기를 권장합니다.
namehash 알고리즘과 테스트 벡터
이름은 ENS에서 쓰기 전에 namehash로 해시하며 결과를 node라고 부릅니다. 빈 이름은 32바이트 0입니다. 그 외에는 이름을 첫 점에서 label과 나머지로 나눈 뒤 sha3(namehash(나머지) + sha3(label))를 재귀적으로 계산합니다. 즉 마지막 구성 요소부터 시작해 이전 출력과 레이블 해시를 이어 붙여 다시 해시합니다. 'mysite.swarm'은 node = 0 32바이트, node = sha3(node + sha3('swarm')), node = sha3(node + sha3('mysite')) 순서로 처리합니다. 구현은 다음 테스트 벡터를 만족해야 합니다. namehash('')는 0x0000000000000000000000000000000000000000000000000000000000000000, namehash('eth')는 0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae, namehash('foo.eth')는 0xde9b09fd7c5f901e23a3f19fecc54828e9c848539801e86591bd9801b019f84f 입니다.
레지스트리 함수와 이벤트
owner(node)는 node의 소유자(레지스트라)를, resolver(node)는 리졸버 주소를, ttl(node)은 node 정보를 캐시해도 되는 최대 시간인 TTL을 돌려줍니다. setOwner(node, owner)는 소유권을 다른 레지스트라로 넘기며 현재 소유자만 부를 수 있고 성공하면 Transfer(bytes32 indexed, address) 이벤트를 남깁니다. setSubnodeOwner(node, label, owner)는 sha3(node, label)로 새 node를 만들어 소유자를 정하거나 이미 있으면 소유자를 갱신합니다. 역시 node의 현재 소유자만 부를 수 있고 NewOwner(bytes32 indexed, bytes32 indexed, address)를 남깁니다. setResolver(node, resolver)는 리졸버 주소를 정하며 소유자만 부를 수 있고 NewResolver(bytes32 indexed, address)를 남깁니다. setTTL(node, ttl)은 TTL을 정합니다. TTL은 레지스트리의 owner, resolver 기록뿐 아니라 연결된 리졸버가 돌려주는 모든 정보에 적용됩니다.
리졸버 공통 규칙과 supportsInterface
리졸버는 여기 정의된 기록 종류 중 어떤 부분집합이든 구현할 수 있습니다. 한 기록 종류가 여러 함수를 요구하면 전부 구현하거나 하나도 구현하지 않아야 합니다. 리졸버는 throw하는 fallback 함수를 반드시 둬야 합니다. 필수 함수는 supportsInterface(bytes4 interfaceID) 하나입니다. EIP-165에 정의된 이 함수는 주어진 4바이트 식별자의 인터페이스를 구현하면 true를 돌려줍니다. 인터페이스 식별자는 그 인터페이스에 속한 함수 시그니처 해시들의 XOR이며 함수가 하나뿐이면 그 함수의 시그니처 해시와 같습니다. true를 돌려줬다면 해당 함수들을 실제로 구현해야 합니다. supportsInterface 자체의 ID인 0x01ffc9a7에는 항상 true를 돌려줘야 합니다. 표준화된 인터페이스는 addr(0x3b3b57de, 이 문서), name(0x691f3431, #181), ABI(0x2203ab56, #205), pubkey(0xc8690233, #619)이며 다른 EIP가 새 인터페이스를 추가할 수 있습니다.
addr 인터페이스와 0 주소 처리
컨트랙트 주소 자원을 지원하는 리졸버는 addr(bytes32 node)를 제공해야 합니다. addr 조회를 지원하지만 요청한 node에 addr 기록이 없으면 반드시 0 주소를 돌려줍니다. addr 기록을 푸는 클라이언트는 반드시 0 반환값을 확인하고 리졸버가 지정되지 않은 이름과 똑같이 취급해 그 주소로 송금하거나 상호작용하기를 거부해야 합니다. 이를 지키지 않으면 사용자가 실수로 0 주소에 자금을 보낼 수 있습니다. 주소가 바뀌면 반드시 AddrChanged(bytes32 indexed node, address a) 이벤트를 발생시켜야 합니다.
이 문서가 정하지 않는 것과 부록 예시
이 문서의 규범 부분은 특정 구현을 정하지 않습니다. 서로 다른 리졸버 구현이 일관되게 이름을 풀 수 있도록 프로토콜을 기록하는 것이 목적입니다. 도메인을 어떻게 등록하거나 갱신하는지, 도메인 책임자를 어떻게 찾는지도 다루지 않습니다. 등록은 레지스트라의 몫이며 최상위 도메인마다 다른 거버넌스 문제입니다. 기록 갱신도 풀이와 별도로 다룰 수 있으며 스웜처럼 갱신 인터페이스가 필요한 시스템을 위해 별도 표준이 나올 것으로 예상합니다. 부록은 예시일 뿐입니다. 부록 A는 레지스트리 구현, 부록 B는 자기 자신을 리졸버로 쓰는 내장 리졸버, 소유자만 갱신하는 독립 리졸버, 레지스트리의 owner로 갱신 권한을 정하는 공개 리졸버, 부록 C는 먼저 요청한 사람에게 무료로 이름을 주는 FIFSRegistrar를 보여 줍니다. 내장 리졸버는 알 수 없는 호출에 throw해야 해서 일부 컨트랙트의 정상 동작을 방해할 수 있습니다.