簡介PNS(Polkadot Name Service) 是一個建立在 Polkadot 上的域名系統,它的主要功能是域名解析,即將一個例如 polka dot 這樣一個可讀性和可
簡介
PNS(Polkadot Name Service) 是一個建立在 Polkadot 上的域名系統,它的主要功能是域名解析,即將一個例如 “polka.dot” 這樣一個可讀性和可記憶性都非常好的字符串翻譯成 Polkadot 上一長串無實際意義的地址。
這樣我們就可以在轉賬、投票以及一些 dapp 操作中使用像 “polka.dot” 這樣簡單易懂的『域名』而不是冗長難記的『地址』。就好像現實生活中我們我們訪問網站使用的是例如 “google.com” 的『域名』,而不是谷歌機房的 ip 地址。
將 “google.com” 翻譯成谷歌主機 ip 的服務就是 DNS(Domain Name Service),而目前全球的 IPV4 根域名服務器只有13臺,其中9臺在美國,2臺在歐洲,1臺在亞洲,如此中心化的分布也導致了互聯網上有一個說法:攻擊整個因特網最有力、最直接,也是最致命的方法恐怕就是攻擊根域名服務器了。
而相比于 DNS,PNS 由于直接架構在 Polkadot 上,因此天然的擁有去中心化的特點,所以傳統的攻擊根域名服務器的方法自然無法奏效。
除了基礎的域名解析服務,PNS還提供了安全可靠的域名注冊、拍賣、轉讓以及交易等功能。
域名解析
域名注冊
eth-ens-namehash 這個 javascript 庫提供了 hash 和 normalize 方法,對域名進行前置處理,使用 UTS46來對域名進行標準化處理雖然支持utf-8 編碼的字符,但是同時也導致了一些釣魚域名可以注冊成功。例如 faceboоk.eth 和 facebook.eth 看起來似乎是兩個同樣的字符串,但是卻都可以在 ENS 上注冊成功,這是因為第一個 facebook 中的第二個 ο 是其實希臘字母 Ομικρον ,只是看起來一樣罷了,而如果允許這樣的情況繼續發生的話,那么在現代互聯網中屢見不鮮的『同形異義字』的釣魚域名攻擊在區塊鏈中依然無法幸免。
所以在 PNS 的域名規則里我們只允許這些字符:.abcdefghijklmnopqrstuvwxyz1234567890。雖然這樣會有不尊重少數語言的嫌疑,但是為了表面意義上的政治正確而增加用戶的資產風險顯然是個更加錯誤的決定。
.這個字符嚴格意義上并不屬于 PNS 域名規則中可以使用的字符,但是它確實會出現在域名中,例如:"polka.dot" 和 "chainx.polka.dot",從前面兩個例子可以看出來,這里的.和我們常見域名的作用是一樣的,即用來區分域名層級而并沒有實際的含義。
域名長度
· 短域名(3-6個字符,需要拍賣,示例:chainx.dot)
· 長域名(7-12字符,支付租用費選擇租用期限并注冊,示例:chainxpool.dot)
注冊步驟
1. 填寫想要注冊的域名(如 chainx)
2. 選擇域名時效(默認1年有效期,可續期,租用費用和租用市場相關,大于3年可給予一定優惠)
3. 支付費用提交交易,交易成功后獲取域名
4. 可選:默認綁定交易地址,可更改綁定地址
拍賣方式
· 英式拍賣,以一年期租用費用為起拍價,無保留價
· 拍賣系統定期放出一部分短域名進行競拍,在規定期限內,首次出價最高的用戶將會獲得域名。
如無人競拍,域名將以起拍價放置于代理交易系統,任何想獲得該域名的用戶都可以通過代理交易方式獲取該域名。
拍賣時長
· 5-6 個字符,4周
· 4 個字符,5周
· 3個字符,6周
域名屬性
開發者可以根據域名地址獲取到域名的所有屬性,并構建自己的應用
代理交易
用戶可以注冊域名,自然就會有賣出域名的需求。而賣出域名的過程具體如下:
Alice 想要賣掉自己的域名,只需要向『代理交易合約』提交一筆包含交易價格、傭金費率(傭金費率決定了你在 PNS 交易系統中的展示優先級)和時效的交易就可以了,交易成功之后該域名會自動進入『代理交易系統』中,而在時效過期之后該域名則會離開『代理交易系統』并返回到 Alice 的歸屬權下。在時效期內任何愿意購買該域名的用戶只需要購買并支付『代理交易合約』中標明的價格就可以獲得該域名。
這里會有一個潛在的問題,那就是如果 Alice 和 Bob 認識,并且他們兩個人已經商量好了交易價格,在 Alice 掛出域名之后則有可能出現兩個非預想的情況:
1. Bob 沒有及時完成購買操作,域名被其他人買了
2. Alice 在掛出之后就及時通知 Bob 進行購買,但是依然會被一些自動腳本或者惡意搶注的人先行一步的購買成功
針對這兩種情況 PNS 還提供了指定購買者地址的可選項,所以只要 Alice 在向『代理交易合約』提交請求的時候指定 Bob 提供的購買者地址就可以保證該域名只會被 Bob 購買成功了。
出價轉讓
當你發現你想要注冊的域名已經被別人注冊了的時候,你一定會非常沮喪。在現實世界中為了得到你心儀的域名,你可以通過域名管理網站聯系經紀人,然后經紀人去幫助你詢問域名擁有者是否愿意轉賣,如果對方愿意轉賣,在經歷過域名管理機構以及域名注冊商的轉讓操作之后,你就可以得到域名了。但是在區塊鏈世界中,似乎沒有人可以當你的經濟人,在賬戶匿名的情況下即使你想要付出高額的溢價,也有可能根本聯系不到對方。
所以 PNS 同時也提供了出價轉讓系統,那具體怎么操作呢?我們設想下面一個場景:
Alice 想要注冊一個名為 "polka.dot" 的域名,但是發現該域名已經被一個未知用戶注冊,且該域名既不在『拍賣系統』中,也不在『代理交易系統』中,那么 Alice 就可以向『出價轉讓合約』發送一個出價轉讓的申請交易并攜帶自己愿意支付的報價和一定比例的保證金。
當未知用戶登錄 PNS 或者任意支持 PNS 的應用時(我們會給所有支持 PNS 的應用提供『出價通知』的插件或工具包),他就會接收到出價轉讓通知,如果該未知用戶同意 Alice 的出價轉讓請求,則可以通過 PNS 提供的方法將自己的域名轉移到『代理交易系統』中,Alice 只需要在『代理交易系統』補足尾款即可獲得 "polka.dot" 這個域名。
如果域名擁有者不同意 Alice 的請求,那么無需任何操作,兩周之后該請求會自動作廢,并返還 Alice 的保證金。
如果 Alice 違約,在兩周之內沒有補足尾款,那么 "polka.dot" 會在『代理交易系統』中被釋放,并把之前支付的保證金分配給『代理交易合約』和域名持有者。
在幾乎所有的區塊鏈應用中都會強調例如去中心化、匿名、安全等關鍵字,但是對于真正需要交互的區塊鏈應用來說,匿名或許并不是一個值得稱道的點。比如在域名的轉讓過程中,不可能第一次出價就能夠讓雙方都滿意,那么彼此的討價還價就顯得很有必要了。
在智能合約里討價還價技術上確實是可行的,但是實際上是一種為了區塊鏈而區塊鏈的浪費資源且耽誤時間的行為。因此如果我們可以將用戶的聯系方式(Email)作為域名的一個屬性(如果能夠切實的對用戶提供便利,那么用戶可能并不介意填寫自己的電子郵箱),那么毫無關聯的兩個用戶完全可以通過更高效的方式完成域名價格的確定,然后再通過 PNS 提供的『代理交易合約』來安全的完成域名交易,這樣既兼顧用戶體驗又確保交易安全性的交互方式或許更加符合大部分用戶的真實需求。
域名管理
在注冊或者購買域名成功之后,還需要設置一些基本信息才能更好的使用
1. 更改映射地址
2. 添加子域名
3. 更改owner
4. renew
合約實現
目前官方提供的智能合約工具已經可以完成一些基礎的功能了,所以接下來我們會使用 ink 實現一個簡單的 PNS 。
在此之前,建議先閱讀 ink 相關的教程。
這里我們主要實現域名注冊、設置地址、域名轉移以及域名查詢這幾個功能。
創建合約
運行 cargo contract new simple-pns,新建一個合約項目。
定義合約結構
struct SimplePns {
/// A hashmap to store all name to addresses mapping
name_to_address: storage::HashMap
/// A hashmap to store all name to owners mapping
name_to_owner: storage::HashMap
default_address: storage::Value
}
其中 name_to_address 是一個存儲域名到映射地址的 hashmap,name_to_owner 是一個存儲域名到域名所有者的 hashmap,default_address 是一個類型為 AccountId 的空地址。
初始化合約
impl Deploy for SimplePns {
/// Initializes contract with default address.
fn deploy(&mut self) {
self.default_address.set(AccountId::from([0x0; 32]));
}
}
實現域名操作方法
impl SimplePns {
/// Register specific name with caller as owner
pub(external) fn register(&mut self, name: Hash) -> bool {
let caller = env.caller();
if self.is_name_exist_impl(name) {
return false
}
env.println(&format!("register name: {:?}, owner: {:?}", name, caller));
self.name_to_owner.insert(name, caller);
env.emit(Register {
name: name,
from: caller,
});
true
}
/// Set address for specific name
pub(external) fn set_address(&mut self, name: Hash, address: AccountId) -> bool {
let caller: AccountId = env.caller();
let owner: AccountId = self.get_owner_or_none(name);
env.println(&format!("set_address caller: {:?}, owner: {:?}", caller, owner));
if caller != owner {
return false
}
let old_address = self.name_to_address.insert(name, address);
env.emit(SetAddress {
name: name,
from: caller,
old_address: old_address,
new_address: address,
});
return true
}
/// Transfer owner to another address
pub(external) fn transfer(&mut self, name: Hash, to: AccountId) -> bool {
let caller: AccountId = env.caller();
let owner: AccountId = self.get_owner_or_none(name);
env.println(&format!("transfer caller: {:?}, owner: {:?}", caller, owner));
if caller != owner {
return false
}
let old_owner = self.name_to_owner.insert(name, to);
env.emit(Transfer {
name: name,
from: caller,
old_owner: old_owner,
new_owner: to,
});
return true
}
/// Get address for the specific name
pub(external) fn get_address(&self, name: Hash) -> AccountId {
let address: AccountId = self.get_address_or_none(name);
env.println(&format!("get_address name is {:?}, address is {:?}", name, address));
address
}
/// Check whether name is exist
pub(external) fn is_name_exist(&self, name: Hash) -> bool {
self.is_name_exist_impl(name)
}
}
/// Implement some private methods
impl SimplePns {
/// Returns an AccountId or default 0x00*32 if it is not set.
fn get_address_or_none(&self, name: Hash) -> AccountId {
let address = self.name_to_address.get(&name).unwrap_or(&self.default_address);
*address
}
/// Returns an AccountId or default 0x00*32 if it is not set.
fn get_owner_or_none(&self, name: Hash) -> AccountId {
let owner = self.name_to_owner.get(&name).unwrap_or(&self.default_address);
*owner
}
/// check whether name is exist
fn is_name_exist_impl(&self, name: Hash) -> bool {
let address = self.name_to_owner.get(&name);
if let None = address {
return false;
}
true
}
}
可以看到在上面具體的方法中我們使用 env.emit 觸發的一些事件,所以我們還需要定義這些事件:
event Register {
name: Hash,
from: AccountId,
}
event SetAddress {
name: Hash,
from: AccountId,
old_address: Option
new_address: AccountId,
}
event Transfer {
name: Hash,
from: AccountId,
old_owner: Option
new_owner: AccountId,
}
編寫測試函數
#[cfg(all(test, feature = "test-env"))]
mod tests {
use super::*;
use ink_core::env;
type Types = ink_core::env::DefaultSrmlTypes;
#[test]
fn register_works() {
let alice = AccountId::from([0x1; 32]);
// let bob: AccountId = AccountId::from([0x2; 32]);
let name = Hash::from([0x99; 32]);
let mut contract = SimplePns::deploy_mock();
env::test::set_caller::
assert_eq!(contract.register(name), true);
assert_eq!(contract.register(name), false);
}
#[test]
fn set_address_works() {
let alice = AccountId::from([0x1; 32]);
let bob: AccountId = AccountId::from([0x2; 32]);
let name = Hash::from([0x99; 32]);
let mut contract = SimplePns::deploy_mock();
env::test::set_caller::
assert_eq!(contract.register(name), true);
// caller is not owner, set_address will be failed
env::test::set_caller::
assert_eq!(contract.set_address(name, bob), false);
// caller is owner, set_address will be successful
env::test::set_caller::
assert_eq!(contract.set_address(name, bob), true);
assert_eq!(contract.get_address(name), bob);
}
#[test]
fn transfer_works() {
let alice = AccountId::from([0x1; 32]);
let bob = AccountId::from([0x2; 32]);
let name = Hash::from([0x99; 32]);
let mut contract = SimplePns::deploy_mock();
env::test::set_caller::
assert_eq!(contract.register(name), true);
// transfer owner
assert_eq!(contract.transfer(name, bob), true);
// now owner is bob, alice set_address will be failed
assert_eq!(contract.set_address(name, bob), false);
env::test::set_caller::
// now owner is bob, set_address will be successful
assert_eq!(contract.set_address(name, bob), true);
assert_eq!(contract.get_address(name), bob);
}
}
運行測試
使用命令 cargo +nightly test 來測試合約函數,如果得到下面的結果,證明測試通過。
編譯合約和 ABI
使用命令 cargo contract build 編譯合約,并使用命令 cargo +nightly build --features ink-generate-abi 編譯 ABI。
運行成功之后 target 目錄下會出現相應的 wasm 和 json 文件。
部署合約
在部署合約之前我們要使用 substrate --dev 在本地啟動一個 substrate 節點,然后克隆 polkadot-app 到本地,并連接到本地節點。
成功啟動之后,我們在 contracts 頁面上傳相應的文件。
上傳成功之后,我們還需要部署合約:
然后按照下圖輸入相應的數值,點擊部署:
部署成功后,就可以調用合約的具體函數了,由于目前 ink 以及相關的工具鏈還不是很完善,想要驗證數據只能在合約中使用 env.println 來在 substrate 節點的控制臺中輸出相關信息。
注意:env.println 只在 substrate --dev 模式下有效
現在讓我們測試一下注冊域名能否成功吧~
調用 register 函數:
在控制臺中查看調用日志:
可以看到控制臺中的 name 對應 0x9e9de23f4d89d086c74c9fa23e4f4ceff6f9b68165b60b70290d1e5820f4bf4d,調用成功!(楊文濤)