KNS 인테그레이션

KNS의 Resolution을 직접 적용하기 위한 가이드입니다.

기본 구조

바탕이 되는 큰 구조는 Ethereum Name Service를 참고하였습니다.

  1. Registry는 각 도메인의 owner와 resolver에 대한 정보를 가지고 있습니다.

  2. Resolver는 각 도메인에 대응되는 주소와 부가 정보들(이메일 주소, 아바타 등)을 담고 있습니다.

Registry 상의 owner는 도메인 NFT를 소유하고 있는 주소이며, 실제로 해당 도메인을 resolve한 결과는 resolver에 addr를 호출해야만 알아낼 수 있습니다. (위 그림 참조) 즉, Registry 상의 owner는 도메인을 resolve한 결과와 다를 수 있습니다.

등록된 도메인의 resolver는 PublicResolver인 것이 기본 상태입니다. PublicResolver는 addr 메소드를 가지고 있어, 적절한 주소로 resolve가 가능합니다. KNS 프론트엔드를 사용하면 resolver의 주소가 언제나 PublicResolver의 주소로 유지됩니다.

Forward Resolution (도메인 주소)

etherseth-ens-namehash 라이브러리를 활용한 forward resolution 구현입니다.

const namehash = require("eth-ens-namehash");
const ethers = require("ethers");

/********************************************
    Declare provider and ABI to use here
********************************************/

async function forwardResolve(domain) {
    const EMPTY_ADDRESS = "0x0000000000000000000000000000000000000000";
    let registry = new ethers.Contract(
        REGISTRY_ADDRESS,
        REGISTRY_ABI,
        provider
    );
    let node = namehash.hash(domain);
    let resolverAddress = await registry.resolver(node);
    if (parseInt(resolverAddress, 16) === 0) {
        return EMPTY_ADDRESS;
    }
    let resolver = new ethers.Contract(
        resolverAddress, 
        PUBLIC_RESOLVER_ABI,
        provider
    );
    try {
        let address = await resolver.addr(node);
        return address;
    } catch(e) {
        console.error(e);
        return EMPTY_ADDRESS;
    }
}

forwardResolve("foo.klay").then(console.log);

컨트랙트 상에서 도메인은 raw string으로 관리되지 않고, bytes32 형의 namehash 값으로 관리됩니다. eth-ens-namehash 라이브러리는 이 namehash 값을 계산하는 함수를 제공합니다. 물론, 위 링크를 참고하여 직접 구현하셔도 무방합니다.

계산된 namehash값을 사용해 우선 Registry 컨트랙트에 도메인의 resolver address를 쿼리합니다. 얻은 resolver address가 0인 경우는 등록되지 않은 도메인이거나 resolver를 0 주소로 세팅한 경우이므로 forward resolution이 불가능하고 이 경우 빈 주소를 반환합니다.

그 외의 경우에는 반환된 resolver address에 addr 메소드를 호출해 주소를 얻어내면 됩니다. addr 메소드를 구현하지 않은 resolver로 설정될 가능성이 있으므로 위와 비슷한 형태의 예외 처리를 해 주시는 것을 권장드립니다.

0 주소가 반환되는 경우에는 반환값을 무시하도록 구현해야 합니다.

const REGISTRY_ABI = ["function resolver(bytes32) view returns (address)"]
const PUBLIC_RESOLVER_ABI = ["function addr(bytes32) view returns (address)"]

Forward resolution을 위해 필요한 최소한의 ABI는 위와 같습니다.

const REGISTRY_ADDRESS = "0x0892ed3424851d2Bab4aC1091fA93C9851Eb5d7D"
const PUBLIC_RESOLVER_ADDRESS = "0xe2AE210c9b8601E00edE4aE5b9B23a80dCD12e3C"

또한, 현재 Cypress에 deploy된 Registry와 Public Resolver의 주소는 위와 같습니다. (Public Resolver의 주소는 아래에서 사용됩니다.)

이벤트

주소가 바뀌는 것을 트래킹하기 위해서 PublicResolver의 주소에서 다음 이벤트를 구독하면 됩니다. 물론, PublicResolver가 아닌 custom resolver가 설정된 경우에는 트래킹이 불가능합니다.

event AddrChanged(bytes32 indexed node, address addr);

_node_는 도메인의 namehash 값이며, 그 도메인에 해당되는 주소가 _addr_로 바뀌었다는 의미입니다.

Registry 상에서 도메인의 owner나 resolver가 바뀐 경우에도 이전에 맵핑된 주소를 invalidate하고 새롭게 바뀐 주소를 쿼리해야 합니다. 이를 위해서는 Registry 주소에서 다음 이벤트들을 구독하면 됩니다.

event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

_tokenId_는 도메인의 namehash 값을 bytes32에서 uint256으로 단순히 캐스팅한 값이며, 해당 _tokenId_의 소유자가 _from_에서 _to_로 바뀌었다는 의미입니다. Registry는 IKIP17을 구현하고 있으며, 이는 IKIP17의 Transfer 이벤트와 동일합니다.

event NewResolver(bytes32 indexed node, address resolver);

_node_는 도메인의 namehash 값이며, 해당 도메인의 resolver 주소가 _resolver_로 바뀌었다는 의미입니다.

종합하여, 이벤트를 구독해 캐시를 관리하는 버전의 forward resolution을 의사코드로 나타내면 다음과 같습니다.

// Forward resolution with caching
func forwardResolve(domain):
	namehash <- getNamehash(domain)
	if (namehash in cache):
		return cache[namehash]
	else:
		resolver <- getResolver(namehash)
		result <- resolver.addr(namehash)
		if (resolver == PUBLIC_RESOLVER):
			cache[namehash] <- result
		return result

// Daemon for listening events
func daemon():
	while (true):
		event <- listenEventFrom([REGISTRY, PUBLIC_RESOLVER])
		if (event.type == "AddrChanged"): // from PUBLIC_RESOLVER
			cache[event.node] <- event.addr
		elif (event.type == "Transfer"): // from REGISTRY
			invalidate cache[event.tokenId]
		elif (event.type == "NewResolver"): // from REGISTRY
			invalidate cache[event.node]

Reverse resolution (주소 → 도메인)

Reverse resolution의 작동 방식은 ENS와 유사하기 때문에 ENS reverse resolution document를 참고하시면 이해에 도움이 될 수 있습니다.

Reverse resolution이란 하나의 주소에 매핑된 여러 개의 도메인 중 하나를, 그 주소의 “대표 도메인”으로 지정하는 기능입니다. 이를 위해 “addr.reverse”라는 특별한 도메인을 사용합니다. “addr.reverse” 도메인의 Registry 상 owner는 ReverseRegistrar라는 컨트랙트이며, 사용자들은 이 컨트랙트를 통해 서브도메인 “(주소).addr.reverse”의 소유권을 획득할 수 있습니다. “(주소).addr.reverse” 도메인의 resolver에 name(bytes32 namehash) 함수를 호출한 결과가 이 주소의 대표 도메인이 됩니다.

예를 들어, “0x314159265dd8dbb310642f98f50c066173c1259b” 주소는 ReverseRegistrar 컨트랙트를 통해 “314159265dd8dbb310642f98f50c066173c1259b.addr.reverse” 도메인의 소유권을 claim할 수 있으며, 해당 도메인의 resolver에 name(“0x9f3f…17e6”) 값을 “hello.klay”로 설정함으로써 “0x3141...259b”의 대표 도메인을 “hello.klay”로 설정할 수 있습니다. (“0x9f3f…17e6”는 “3141…259b.addr.reverse”의 namehash 값)

“(주소).addr.reverse” 도메인의 resolver는 일반적인 도메인과 다르게 기본적으로 DefaultReverseResolver로 설정되며, DefaultReverseResolver는 name(bytes32)를 지원합니다.

대표 도메인을 설정할 때에 실제로 해당 주소로 resolve되지 않는 도메인이 설정되거나, 대표 도메인으로 설정했던 도메인의 소유권이 변경되는 경우가 발생할 수 있으므로 reverse resolution을 실제로 구현할 때에는 몇 가지를 추가로 확인해 줘야 합니다. 구체적으로는 다음 과정을 거쳐야 합니다.

  1. Registry에 “(주소).addr.reverse”의 resolver 주소를 쿼리합니다. (이 때 주소의 알파벳(a-f)는 모두 소문자로 입력해야 하며, 0x prefix는 입력하지 않아야 함에 유의.) 쿼리 결과가 0 주소를 반환한 경우 실패. (종료)

  2. 1번의 결과로 나온 resolver에 name(bytes32 namehash)를 호출합니다. namehash는 “(주소).addr.reverse”의 namehash값입니다. 쿼리 결과가 빈 문자열을 반환한 경우 실패. (종료)

  3. 2번의 결과로 나온 도메인의 namehash 값을 사용해 Registry에 resolver 주소를 쿼리합니다. 쿼리 결과가 0 주소를 반환한 경우 실패. (종료)

  4. 3번의 결과로 나온 resolver 주소에 addr(bytes32 namehash)를 호출합니다. namehash는 2번의 결과로 나온 도메인의 namehash 값입니다. 쿼리 결과로 반환된 주소 값이 처음 입력된 주소 값과 다르면 실패. (종료)

  5. 1~4 과정을 실패 없이 모두 통과했다면 입력된 주소의 대표 도메인은 2번의 결과로 나온 도메인이 됩니다. 만약 중간에 실패했다면 대표 도메인이 설정되지 않았거나 잘못 설정된 경우입니다.

위에서 설명한 예시를 다시 살펴보면, 1번 과정에서는 Registry에 resolver(“0x9f3f…17e6”)를 호출해 “3141…259b.addr.reverse”에 해당되는 resolver의 주소를 얻을 것이며 (default는 DefaultReverseResolver 주소), 2번 과정에서는 해당 resolver 주소에 name(“0x9f3f…17e6”)를 호출해 “hello.klay”를 반환받고, 3번 과정에서는 Registry에 “hello.klay”의 namehash 값인 “0x6f37…b595”를 사용해 resolver(“0x6f37…b595”)를 호출해 “hello.klay”의 resolver 주소를 얻고 (default는 PublicResolver 주소), 4번 과정에서는 해당 resolver 주소에 addr(“0x6f37…b595”)를 호출해 실제로 “0x3141…259b” 주소로 resolve된다면 성공, 그렇지 않다면 실패합니다.

위 모든 과정은 ReverseRecords 컨트랙트의 getName(address) 함수에 구현되어 있습니다. 따라서 가장 간단하게는 다음과 같이 reverse resolution을 구현할 수 있습니다.

import "ethers";

async function reverseResolve(address: any) {
  let reverseRecords = new ethers.Contract(
    REVERSE_RECORDS_ADDRESS,
    REVERSE_RECORDS_ABI,
    provider
  );
  let name = await reverseRecords.getName(address);
  return name;
}

reverseResolve("0x314159265dd8dbb310642f98f50c066173c1259b").then(console.log);

Reverse resolution을 쿼리하는 데에 필요한 최소한의 ABI는 다음과 같습니다.

const REVERSE_RECORDS_ADDRESS = "0x87f4483E4157a6592dd1d1546f145B5EE22c790a"
const REVERSE_RECORDS_ABI = ["function getName(address) view returns (string)"]

이벤트

Reverse resolution에 대해 캐싱을 수행하기 위해서는 event를 구독해야 합니다. 기본적으로 위에서 설명한 1~4의 과정 중 하나라도 결과가 바뀐 경우에는 새롭게 쿼리를 해서 결과를 얻어야 합니다. 1~4 과정 중 3~4 과정은 2번 과정의 결과로 나온 도메인에 대해 forward resolution을 수행해 입력된 주소와 같은지를 확인하는 과정이므로 Forward Resolution에서 설명한 event들을 구독하는 것이 기본적으로 필요합니다.

추가로 1~2 과정에서 DefaultReverseResolver에 설정된 name(대표 도메인)이 바뀌는 것을 확인하기 위해 다음 이벤트를 구독해야 합니다.

event NameChanged(bytes32 indexed node, string name);

_node_는 “(주소).addr.reverse”의 namehash 값이며, 그 주소의 이름이 _name_으로 바뀌었다는 뜻입니다.

Last updated