はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

今回は、ENSでEthereumだけでなく、複数のブロックチェーンのアドレスも同じ仕組みで登録・取得できるようにする仕組みを提案しているENSIP9についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

他にも様々なEIP・BIP・SLIP・CAIP・ENSIPについてまとめています。

概要

ENSIP9は、ENS(Ethereum Name Service)のリゾルバにおける addr フィールドを拡張し、新しいオーバーロード(同じ名前の関数に異なる使い方を追加する仕組み)を導入するものです。 これにより、ENS上でEthereumアドレスだけでなく、他のブロックチェーンのアドレスも解決(名前からアドレスを取得)できるようになります。 もともとはERC2304として提案されていた機能です。

ERC2304については以下の記事を参考にしてください。

つまり、ENSを使って「名前からアドレスを探す」時に、Ethereum以外のチェーン(例:BitcoinやLitecoinなど)のアドレスも一貫した方法で取得できるようにするのが目的です。

動機

ENSはEthereumのエコシステム内で広く使われてきましたが、最近ではマルチコインウォレット(複数のチェーンの資産を管理できるウォレット)が増えてきています。 こうしたウォレットの開発者からは「ENSを使ってEthereum以外のチェーンのアドレスも解決できるようにしてほしい」という要望が寄せられていました。

ENSIP9は、そのニーズに応えるために作られたものです。 ENSで複数のチェーンに対応したアドレス管理を標準化し、どのクライアント(ウォレットやアプリケーション)でも共通の方法で利用できるようにすることで、ユーザーと開発者の利便性を高める狙いがあります。

仕様

ENSIP9は、ENS(Ethereum Name Service)のリゾルバに新しい関数とイベントを追加することで、Ethereum以外のブロックチェーンのアドレスも解決できるようにする仕組みを定めています。 ENSを通じて複数のチェーンのアドレスを共通の方法で扱えるようにすることで、ウォレットやアプリケーション間での互換性が向上します。

新しい関数

ENSリゾルバに次の関数が追加されます。

javascript
function addr(bytes32 node, uint coinType) external view returns(bytes memory);

この関数は、指定されたノード(namehashで表される名前)とコインタイプに対応する暗号資産(トークンやコイン)のアドレスを返します。 もしそのコインタイプに対応するアドレスが存在しなければ、空のバイト列を返します。 コインタイプ(coinType)はSLIP44という標準に基づいており、例えばEthereumは60、Bitcoinは0という識別子が割り当てられています。 戻り値は各チェーンの「ネイティブなバイナリ形式」で保存されるため、文字列で表されるチェックサムやハイフンなどの修飾は含まれません。

SLIP44については以下の記事を参考にしてください。

この関数のEIP165インターフェースIDは 0xf1cb7e06 です。

新しいイベント

リゾルバは、アドレス情報が変更されるたびに以下のイベントを発行します。

javascript
event AddressChanged(bytes32 indexed node, uint coinType, bytes newAddress);

これにより、アプリケーションはENS上でアドレスが更新されたことを検知でき、ウォレットなどの同期が容易になります。

推奨される設定関数

アドレスを登録・更新するための推奨インターフェースとして以下の関数が用意されています。

javascript
function setAddr(bytes32 node, uint coinType, bytes calldata addr);

この関数は、指定されたノードとコインタイプに対するアドレスを追加または置き換えます。 変更時には必ず AddressChanged イベントが発行されます。 実装によっては異なるインターフェースを用いても構いませんが、この形式を採用することが推奨されています。

アドレスのエンコード方式

ENSに保存されるアドレスは「各チェーンのネイティブなバイナリ形式」で統一されます。 つまり、ユーザーが普段目にするテキスト形式のアドレスは、一旦デコードされてから保存される仕組みです。 ここでは代表的な方式を紹介します。

  • P2PKH(Pay to Public Key Hash) Bitcoinの古典的な形式で、Base58Checkエンコードされたアドレスをデコードし、バージョンバイト+公開鍵ハッシュを保存します。 スクリプト形式は OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG です。

  • P2SH(Pay to Script Hash) スクリプトのハッシュを使う形式で、デコード後はバージョンバイト+スクリプトハッシュを保存します。 スクリプト形式は OP_HASH160 <scriptHash> OP_EQUAL です。

  • SegWit(Bech32) bc(Bitcoinメインネット)などの人間可読部分を含む形式で、witness version と witness program に変換されます。 誤った変換をすると資金が利用できなくなるため注意が必要です。

  • ChecksummedHex(Ethereumなど) EthereumやEthereum Classicなどは、EIP55のチェックサム付き16進数形式を採用しています。 ENSには 0x を除いた16進数バイト列が保存され、読み出すときは必ずチェックサムを付与して返されます。

EIP55については以下の記事を参考にしてください。

  • Ripple Rippleアドレスは独自のBase58Checkエンコードを使用し、デコード後のバイト列を保存します。 通常の r- アドレスやタグ付きの X- アドレスが存在します。

  • CashAddr(Bitcoin Cash) Bech32の派生形式を使い、bitcoincash: プレフィックス付きのアドレスをデコードして保存します。

  • Bech32(Binanceなど) Binanceの bnb アドレスなど、チェーンごとに決められた人間可読部分を含む形式で、デコード後のバイト列が保存されます。

参考実装

js
pragma solidity ^0.5.8;
 
contract AddrResolver is ResolverBase {
    bytes4 constant private ADDR_INTERFACE_ID = 0x3b3b57de;
    bytes4 constant private ADDRESS_INTERFACE_ID = 0xf1cb7e06;
    uint constant private COIN_TYPE_ETH = 60;
 
    event AddrChanged(bytes32 indexed node, address a);
    event AddressChanged(bytes32 indexed node, uint coinType, bytes newAddress);
 
    mapping(bytes32=>mapping(uint=>bytes)) _addresses;
 
    /**
     * Sets the address associated with an ENS node.
     * May only be called by the owner of that node in the ENS registry.
     * @param node The node to update.
     * @param a The address to set.
     */
    function setAddr(bytes32 node, address a) external authorised(node) {
        setAddr(node, COIN_TYPE_ETH, addressToBytes(a));
    }
 
    /**
     * Returns the address associated with an ENS node.
     * @param node The ENS node to query.
     * @return The associated address.
     */
    function addr(bytes32 node) public view returns (address) {
        bytes memory a = addr(node, COIN_TYPE_ETH);
        if(a.length == 0) {
            return address(0);
        }
        return bytesToAddress(a);
    }
 
    function setAddr(bytes32 node, uint coinType, bytes memory a) public authorised(node) {
        emit AddressChanged(node, coinType, a);
        if(coinType == COIN_TYPE_ETH) {
            emit AddrChanged(node, bytesToAddress(a));
        }
        _addresses[node][coinType] = a;
    }
 
    function addr(bytes32 node, uint coinType) public view returns(bytes memory) {
        return _addresses[node][coinType];
    }
 
    function supportsInterface(bytes4 interfaceID) public pure returns(bool) {
        return interfaceID == ADDR_INTERFACE_ID || interfaceID == ADDRESS_INTERFACE_ID || super.supportsInterface(interfaceID);
    }
}

変数

ADDR_INTERFACE_ID

javascript
bytes4 constant private ADDR_INTERFACE_ID = 0x3b3b57de;

従来の addr(bytes32) インターフェース(EIP55チェックサムのEthereumアドレス取得)のEIP165用インターフェースID。

EIP165については以下の記事を参考にしてください。

addr(bytes32) を実装していることをEIP165の仕組みで表明するための定数です。 ウォレットやDAppが supportsInterface を通じて、このコントラクトが従来の addr(bytes32) をサポートしているかを判定できます。

ADDRESS_INTERFACE_ID

javascript
bytes4 constant private ADDRESS_INTERFACE_ID = 0xf1cb7e06;

拡張版の addr(bytes32,uint) インターフェース(マルチチェーン対応アドレス取得)のEIP165用インターフェースID。 addr(bytes32,uint) を実装していることをEIP165で示す定数です。 これにより、クライアントはEthereum以外のチェーンのアドレス解決に対応しているかどうかを supportsInterface で判定できます。

COIN_TYPE_ETH

javascript
uint constant private COIN_TYPE_ETH = 60;

SLIP44におけるEthereumのコインタイプ(60)を示す定数。 ENSのマルチチェーン対応により、複数のブロックチェーンのアドレスを同じ仕組みで扱えるようになりました。 その時に、それぞれのチェーンを区別するための数値としてコインタイプ(coinType)が使われます。 Ethereumの場合、この定数 60 が用いられます。

ここで、ENSがすでに多くのクライアントやウォレットに利用されているため、以下のように従来のEthereum専用の関数も引き続き提供されていることです。

js
function setAddr(bytes32 node, address a) external authorised(node) {
    setAddr(node, COIN_TYPE_ETH, addressToBytes(a));
}

一見するとEthereum専用の関数のように見えますが、内部では新しい仕組みである setAddr(node, uint coinType, bytes addr) を呼び出し、coinType = 60 を指定しています。 これにより、古いインターフェースを利用するアプリケーションもそのまま動作しつつ、内部ではマルチチェーン対応の仕組みに統一して保存できます。

このように、setAddr(bytes32,address) や addr(bytes32) といったEthereum専用の関数は、互換性維持のためのショートカットとして残されており、内部では常に COIN_TYPE_ETH を利用するよう設計されています。

AddrChanged

javascript
event AddrChanged(bytes32 indexed node, address a);

指定ノードのEthereumアドレスが更新された時に発行されるイベント。 レガシーな addr(bytes32)/setAddr(bytes32,address) の互換性を保つためのイベントです。 マルチチェーン対応の更新がEthereum(コインタイプ 60)に対して行われた場合にも、従来クライアントとの互換のためにこのイベントを併せて発行します。

パラメータ

  • node
    • namehash されたENSノード。
    • namehash とは、ENSのドメイン名(例: alice.eth)を階層的にハッシュ計算して得られる固定長の識別子(bytes32 値)のこと。
  • a
    • 新しいEthereumアドレス(address 型)。

AddressChanged

javascript
event AddressChanged(bytes32 indexed node, uint coinType, bytes newAddress);

指定ノードの特定コインタイプのアドレスが更新された時に発行されるイベント。 マルチチェーン対応の標準イベントです。 coinType ごとにバイナリ形式のアドレスを bytes で保存・更新するため、任意チェーンの更新を一意に追跡できます。 リゾルバは各更新時に必ずこのイベントを発行します。

パラメータ

  • node
    • namehash されたENSノード。
    • namehash とは、ENSのドメイン名(例: alice.eth)を階層的にハッシュ計算して得られる固定長の識別子(bytes32 値)のこと。
  • coinType
    • SLIP44に基づくコインタイプ番号(例:ETHは60)。
  • newAddress
    • 新しいアドレスのネイティブバイナリ表現(bytes)。

_addresses

javascript
mapping(bytes32=>mapping(uint=>bytes)) _addresses;

ノードとコインタイプごとのバイナリアドレスを保持するストレージ。 第1キーに bytes32 のノード(namehash)、第2キーにSLIP44の coinType を取り、値として当該チェーンのネイティブなバイナリアドレス(bytes)を格納します。 文字列表現のチェックサム等は除去した状態で保存します。

関数

setAddr

javascript
function setAddr(bytes32 node, address a) external authorised(node) {
    setAddr(node, COIN_TYPE_ETH, addressToBytes(a));
}

Ethereumアドレスを設定する関数。 従来の setAddr(bytes32,address) 互換の関数です。 内部ではマルチチェーン対応の setAddr(bytes32,uint,bytes) に処理を委譲し、coinType=60(Ethereum)として保存します。 authorised(node) により、引数で渡されたノードの更新権限があるかを確認します。

引数

  • node
    • namehash されたENSノード。
  • a
    • 設定するEthereumアドレス(address 型)。

addr

javascript
function addr(bytes32 node) public view returns (address) {
    bytes memory a = addr(node, COIN_TYPE_ETH);
    if(a.length == 0) {
        return address(0);
    }
    return bytesToAddress(a);
}

Ethereumアドレスを取得する関数。 マルチチェーン版 addr(bytes32,uint) を coinType=60 で呼び出し、保存済みのEthereumアドレスを返します。 未設定の場合はゼロアドレスを返します。 内部の保存形式は bytes ですが、呼び出し側には address 型で提供します。

引数

  • node
    • namehash されたENSノード。

戻り値

  • address
    • 設定済みのEthereumアドレス。
    • 未設定時は address(0)。

setAddr

javascript
function setAddr(bytes32 node, uint coinType, bytes memory a) public authorised(node) {
    emit AddressChanged(node, coinType, a);
    if(coinType == COIN_TYPE_ETH) {
        emit AddrChanged(node, bytesToAddress(a));
    }
    _addresses[node][coinType] = a;
}

任意コインタイプのアドレスを設定する関数。 SLIP44の coinType とネイティブバイナリ表現のアドレス a を受け取り、対応するストレージに保存します。 必ず AddressChanged イベントを発行し、coinType=60 の場合は従来互換の AddrChanged イベントも発行します。。 authorised(node) により更新権限を確認します。

引数

  • node
    • namehash されたENSノード。
  • coinType
    • SLIP44のコインタイプ番号(例:BTCは0、ETHは60)。
  • a
    • アドレスのネイティブバイナリ表現(bytes)。
    • 文字列表現のチェックサムは含めません。

addr

javascript
function addr(bytes32 node, uint coinType) public view returns(bytes memory) {
    return _addresses[node][coinType];
}

任意コインタイプのアドレス(バイナリ)を取得する関数。 指定ノードとコインタイプに対応する保存済みアドレスのネイティブバイナリ(bytes)を返します。 未設定の場合は長さ0の bytes を返します。 呼び出し側は必要に応じて特定のチェーンの規則に従い文字列表現へエンコードします。

引数

  • node
    • namehash されたENSノード。
  • coinType
    • SLIP44のコインタイプ番号。

戻り値

  • bytes
    • 保存されたアドレスのネイティブバイナリ。
    • 未設定時は空バイト列。

supportsInterface

javascript
function supportsInterface(bytes4 interfaceID) public pure returns(bool) {
    return interfaceID == ADDR_INTERFACE_ID || interfaceID == ADDRESS_INTERFACE_ID || super.supportsInterface(interfaceID);
}

このコントラクトがサポートするEIP165インターフェースを判定する関数。 従来の addr(bytes32)(ADDR_INTERFACE_ID)と拡張の addr(bytes32,uint)(ADDRESS_INTERFACE_ID)をサポートしているかをbool値で返します。 その他のインターフェースIDは継承元コントラクトの supportsInterface 関数を実行します。

引数

  • interfaceID
    • EIP165のインターフェースID(bytes4)。

戻り値

  • bool
    • 対応していれば true、非対応なら false。

互換性

ENSでは、新しいマルチチェーン対応仕様を導入しましたが、従来の仕組みを利用しているアプリケーションやウォレットが動作しなくならないように互換性を確保することが求められています。 具体的には、ENSIP1で定義された addr(bytes32) と、ENSIP9で導入された addr(bytes32,uint) が矛盾しないように扱われます。

ENSIP1については以下の記事を参考にしてください。

Ethereumアドレスの扱い

従来の addr(bytes32) はEthereumアドレス専用でした。 新しい仕様ではEthereumのコインタイプが 60 に定義されているため、addr(node) の結果は必ず addr(node, 60) と一致しなければなりません。 これにより、古い実装と新しい実装が同じEthereumアドレスを返すように統一されます。

javascript
// ENSIP1 の呼び出し
addr(node)

// ENSIP9の呼び出し(coinType = 60 は Ethereum)
addr(node, 60)

両者は必ず同じ結果を返す必要があります。

イベントの扱い

イベントについても同じ考え方が適用されます。 従来の AddrChanged イベントと新しい AddressChanged イベントは、Ethereumアドレスに関する変更であれば必ず両方同時に発行されなければなりません。

javascript
event AddrChanged(bytes32 indexed node, address a);
event AddressChanged(bytes32 indexed node, uint coinType, bytes newAddress);

例えば、Ethereumのアドレスが更新された場合には AddrChanged が発行されると同時に、coinType=60 を指定した AddressChanged も発行されます。 その逆も同様です。

最後に

今回は「ENSでEthereumだけでなく、複数のブロックチェーンのアドレスも同じ仕組みで登録・取得できるようにする仕組みを提案しているENSIP9」についてまとめてきました! いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

他の媒体でも情報発信しているのでぜひ他も見ていってください!