簡(jiǎn)介PNS(Polkadot Name Service) 是一個(gè)建立在 Polkadot 上的域名系統(tǒng),它的主要功能是域名解析,即將一個(gè)例如 polka dot 這樣一個(gè)可讀性和可
簡(jiǎn)介
PNS(Polkadot Name Service) 是一個(gè)建立在 Polkadot 上的域名系統(tǒng),它的主要功能是域名解析,即將一個(gè)例如 “polka.dot” 這樣一個(gè)可讀性和可記憶性都非常好的字符串翻譯成 Polkadot 上一長(zhǎng)串無實(shí)際意義的地址。
這樣我們就可以在轉(zhuǎn)賬、投票以及一些 dapp 操作中使用像 “polka.dot” 這樣簡(jiǎn)單易懂的『域名』而不是冗長(zhǎng)難記的『地址』。就好像現(xiàn)實(shí)生活中我們我們?cè)L問網(wǎng)站使用的是例如 “google.com” 的『域名』,而不是谷歌機(jī)房的 ip 地址。
將 “google.com” 翻譯成谷歌主機(jī) ip 的服務(wù)就是 DNS(Domain Name Service),而目前全球的 IPV4 根域名服務(wù)器只有13臺(tái),其中9臺(tái)在美國(guó),2臺(tái)在歐洲,1臺(tái)在亞洲,如此中心化的分布也導(dǎo)致了互聯(lián)網(wǎng)上有一個(gè)說法:攻擊整個(gè)因特網(wǎng)最有力、最直接,也是最致命的方法恐怕就是攻擊根域名服務(wù)器了。
而相比于 DNS,PNS 由于直接架構(gòu)在 Polkadot 上,因此天然的擁有去中心化的特點(diǎn),所以傳統(tǒng)的攻擊根域名服務(wù)器的方法自然無法奏效。
除了基礎(chǔ)的域名解析服務(wù),PNS還提供了安全可靠的域名注冊(cè)、拍賣、轉(zhuǎn)讓以及交易等功能。
域名解析
域名注冊(cè)
eth-ens-namehash 這個(gè) javascript 庫(kù)提供了 hash 和 normalize 方法,對(duì)域名進(jìn)行前置處理,使用 UTS46來對(duì)域名進(jìn)行標(biāo)準(zhǔn)化處理雖然支持utf-8 編碼的字符,但是同時(shí)也導(dǎo)致了一些釣魚域名可以注冊(cè)成功。例如 faceboоk.eth 和 facebook.eth 看起來似乎是兩個(gè)同樣的字符串,但是卻都可以在 ENS 上注冊(cè)成功,這是因?yàn)榈谝粋€(gè) facebook 中的第二個(gè) ο 是其實(shí)希臘字母 Ομικρον ,只是看起來一樣罷了,而如果允許這樣的情況繼續(xù)發(fā)生的話,那么在現(xiàn)代互聯(lián)網(wǎng)中屢見不鮮的『同形異義字』的釣魚域名攻擊在區(qū)塊鏈中依然無法幸免。
所以在 PNS 的域名規(guī)則里我們只允許這些字符:.abcdefghijklmnopqrstuvwxyz1234567890。雖然這樣會(huì)有不尊重少數(shù)語言的嫌疑,但是為了表面意義上的政治正確而增加用戶的資產(chǎn)風(fēng)險(xiǎn)顯然是個(gè)更加錯(cuò)誤的決定。
.這個(gè)字符嚴(yán)格意義上并不屬于 PNS 域名規(guī)則中可以使用的字符,但是它確實(shí)會(huì)出現(xiàn)在域名中,例如:"polka.dot" 和 "chainx.polka.dot",從前面兩個(gè)例子可以看出來,這里的.和我們常見域名的作用是一樣的,即用來區(qū)分域名層級(jí)而并沒有實(shí)際的含義。
域名長(zhǎng)度
· 短域名(3-6個(gè)字符,需要拍賣,示例:chainx.dot)
· 長(zhǎng)域名(7-12字符,支付租用費(fèi)選擇租用期限并注冊(cè),示例:chainxpool.dot)
注冊(cè)步驟
1. 填寫想要注冊(cè)的域名(如 chainx)
2. 選擇域名時(shí)效(默認(rèn)1年有效期,可續(xù)期,租用費(fèi)用和租用市場(chǎng)相關(guān),大于3年可給予一定優(yōu)惠)
3. 支付費(fèi)用提交交易,交易成功后獲取域名
4. 可選:默認(rèn)綁定交易地址,可更改綁定地址
拍賣方式
· 英式拍賣,以一年期租用費(fèi)用為起拍價(jià),無保留價(jià)
· 拍賣系統(tǒng)定期放出一部分短域名進(jìn)行競(jìng)拍,在規(guī)定期限內(nèi),首次出價(jià)最高的用戶將會(huì)獲得域名。
如無人競(jìng)拍,域名將以起拍價(jià)放置于代理交易系統(tǒng),任何想獲得該域名的用戶都可以通過代理交易方式獲取該域名。
拍賣時(shí)長(zhǎng)
· 5-6 個(gè)字符,4周
· 4 個(gè)字符,5周
· 3個(gè)字符,6周
域名屬性
開發(fā)者可以根據(jù)域名地址獲取到域名的所有屬性,并構(gòu)建自己的應(yīng)用
代理交易
用戶可以注冊(cè)域名,自然就會(huì)有賣出域名的需求。而賣出域名的過程具體如下:
Alice 想要賣掉自己的域名,只需要向『代理交易合約』提交一筆包含交易價(jià)格、傭金費(fèi)率(傭金費(fèi)率決定了你在 PNS 交易系統(tǒng)中的展示優(yōu)先級(jí))和時(shí)效的交易就可以了,交易成功之后該域名會(huì)自動(dòng)進(jìn)入『代理交易系統(tǒng)』中,而在時(shí)效過期之后該域名則會(huì)離開『代理交易系統(tǒng)』并返回到 Alice 的歸屬權(quán)下。在時(shí)效期內(nèi)任何愿意購(gòu)買該域名的用戶只需要購(gòu)買并支付『代理交易合約』中標(biāo)明的價(jià)格就可以獲得該域名。
這里會(huì)有一個(gè)潛在的問題,那就是如果 Alice 和 Bob 認(rèn)識(shí),并且他們兩個(gè)人已經(jīng)商量好了交易價(jià)格,在 Alice 掛出域名之后則有可能出現(xiàn)兩個(gè)非預(yù)想的情況:
1. Bob 沒有及時(shí)完成購(gòu)買操作,域名被其他人買了
2. Alice 在掛出之后就及時(shí)通知 Bob 進(jìn)行購(gòu)買,但是依然會(huì)被一些自動(dòng)腳本或者惡意搶注的人先行一步的購(gòu)買成功
針對(duì)這兩種情況 PNS 還提供了指定購(gòu)買者地址的可選項(xiàng),所以只要 Alice 在向『代理交易合約』提交請(qǐng)求的時(shí)候指定 Bob 提供的購(gòu)買者地址就可以保證該域名只會(huì)被 Bob 購(gòu)買成功了。
出價(jià)轉(zhuǎn)讓
當(dāng)你發(fā)現(xiàn)你想要注冊(cè)的域名已經(jīng)被別人注冊(cè)了的時(shí)候,你一定會(huì)非常沮喪。在現(xiàn)實(shí)世界中為了得到你心儀的域名,你可以通過域名管理網(wǎng)站聯(lián)系經(jīng)紀(jì)人,然后經(jīng)紀(jì)人去幫助你詢問域名擁有者是否愿意轉(zhuǎn)賣,如果對(duì)方愿意轉(zhuǎn)賣,在經(jīng)歷過域名管理機(jī)構(gòu)以及域名注冊(cè)商的轉(zhuǎn)讓操作之后,你就可以得到域名了。但是在區(qū)塊鏈?zhǔn)澜缰?,似乎沒有人可以當(dāng)你的經(jīng)濟(jì)人,在賬戶匿名的情況下即使你想要付出高額的溢價(jià),也有可能根本聯(lián)系不到對(duì)方。
所以 PNS 同時(shí)也提供了出價(jià)轉(zhuǎn)讓系統(tǒng),那具體怎么操作呢?我們?cè)O(shè)想下面一個(gè)場(chǎng)景:
Alice 想要注冊(cè)一個(gè)名為 "polka.dot" 的域名,但是發(fā)現(xiàn)該域名已經(jīng)被一個(gè)未知用戶注冊(cè),且該域名既不在『拍賣系統(tǒng)』中,也不在『代理交易系統(tǒng)』中,那么 Alice 就可以向『出價(jià)轉(zhuǎn)讓合約』發(fā)送一個(gè)出價(jià)轉(zhuǎn)讓的申請(qǐng)交易并攜帶自己愿意支付的報(bào)價(jià)和一定比例的保證金。
當(dāng)未知用戶登錄 PNS 或者任意支持 PNS 的應(yīng)用時(shí)(我們會(huì)給所有支持 PNS 的應(yīng)用提供『出價(jià)通知』的插件或工具包),他就會(huì)接收到出價(jià)轉(zhuǎn)讓通知,如果該未知用戶同意 Alice 的出價(jià)轉(zhuǎn)讓請(qǐng)求,則可以通過 PNS 提供的方法將自己的域名轉(zhuǎn)移到『代理交易系統(tǒng)』中,Alice 只需要在『代理交易系統(tǒng)』補(bǔ)足尾款即可獲得 "polka.dot" 這個(gè)域名。
如果域名擁有者不同意 Alice 的請(qǐng)求,那么無需任何操作,兩周之后該請(qǐng)求會(huì)自動(dòng)作廢,并返還 Alice 的保證金。
如果 Alice 違約,在兩周之內(nèi)沒有補(bǔ)足尾款,那么 "polka.dot" 會(huì)在『代理交易系統(tǒng)』中被釋放,并把之前支付的保證金分配給『代理交易合約』和域名持有者。
在幾乎所有的區(qū)塊鏈應(yīng)用中都會(huì)強(qiáng)調(diào)例如去中心化、匿名、安全等關(guān)鍵字,但是對(duì)于真正需要交互的區(qū)塊鏈應(yīng)用來說,匿名或許并不是一個(gè)值得稱道的點(diǎn)。比如在域名的轉(zhuǎn)讓過程中,不可能第一次出價(jià)就能夠讓雙方都滿意,那么彼此的討價(jià)還價(jià)就顯得很有必要了。
在智能合約里討價(jià)還價(jià)技術(shù)上確實(shí)是可行的,但是實(shí)際上是一種為了區(qū)塊鏈而區(qū)塊鏈的浪費(fèi)資源且耽誤時(shí)間的行為。因此如果我們可以將用戶的聯(lián)系方式(Email)作為域名的一個(gè)屬性(如果能夠切實(shí)的對(duì)用戶提供便利,那么用戶可能并不介意填寫自己的電子郵箱),那么毫無關(guān)聯(lián)的兩個(gè)用戶完全可以通過更高效的方式完成域名價(jià)格的確定,然后再通過 PNS 提供的『代理交易合約』來安全的完成域名交易,這樣既兼顧用戶體驗(yàn)又確保交易安全性的交互方式或許更加符合大部分用戶的真實(shí)需求。
域名管理
在注冊(cè)或者購(gòu)買域名成功之后,還需要設(shè)置一些基本信息才能更好的使用
1. 更改映射地址
2. 添加子域名
3. 更改owner
4. renew
合約實(shí)現(xiàn)
目前官方提供的智能合約工具已經(jīng)可以完成一些基礎(chǔ)的功能了,所以接下來我們會(huì)使用 ink 實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 PNS 。
在此之前,建議先閱讀 ink 相關(guān)的教程。
這里我們主要實(shí)現(xiàn)域名注冊(cè)、設(shè)置地址、域名轉(zhuǎn)移以及域名查詢這幾個(gè)功能。
創(chuàng)建合約
運(yùn)行 cargo contract new simple-pns,新建一個(gè)合約項(xiàng)目。
定義合約結(jié)構(gòu)
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 是一個(gè)存儲(chǔ)域名到映射地址的 hashmap,name_to_owner 是一個(gè)存儲(chǔ)域名到域名所有者的 hashmap,default_address 是一個(gè)類型為 AccountId 的空地址。
初始化合約
impl Deploy for SimplePns {
/// Initializes contract with default address.
fn deploy(&mut self) {
self.default_address.set(AccountId::from([0x0; 32]));
}
}
實(shí)現(xiàn)域名操作方法
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 觸發(fā)的一些事件,所以我們還需要定義這些事件:
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,
}
編寫測(cè)試函數(shù)
#[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);
}
}
運(yùn)行測(cè)試
使用命令 cargo +nightly test 來測(cè)試合約函數(shù),如果得到下面的結(jié)果,證明測(cè)試通過。
編譯合約和 ABI
使用命令 cargo contract build 編譯合約,并使用命令 cargo +nightly build --features ink-generate-abi 編譯 ABI。
運(yùn)行成功之后 target 目錄下會(huì)出現(xiàn)相應(yīng)的 wasm 和 json 文件。
部署合約
在部署合約之前我們要使用 substrate --dev 在本地啟動(dòng)一個(gè) substrate 節(jié)點(diǎn),然后克隆 polkadot-app 到本地,并連接到本地節(jié)點(diǎn)。
成功啟動(dòng)之后,我們?cè)?contracts 頁面上傳相應(yīng)的文件。
上傳成功之后,我們還需要部署合約:
然后按照下圖輸入相應(yīng)的數(shù)值,點(diǎn)擊部署:
部署成功后,就可以調(diào)用合約的具體函數(shù)了,由于目前 ink 以及相關(guān)的工具鏈還不是很完善,想要驗(yàn)證數(shù)據(jù)只能在合約中使用 env.println 來在 substrate 節(jié)點(diǎn)的控制臺(tái)中輸出相關(guān)信息。
注意:env.println 只在 substrate --dev 模式下有效
現(xiàn)在讓我們測(cè)試一下注冊(cè)域名能否成功吧~
調(diào)用 register 函數(shù):
在控制臺(tái)中查看調(diào)用日志:
可以看到控制臺(tái)中的 name 對(duì)應(yīng) 0x9e9de23f4d89d086c74c9fa23e4f4ceff6f9b68165b60b70290d1e5820f4bf4d,調(diào)用成功!(楊文濤)
關(guān)鍵詞: PNS Polkadot 域名系統(tǒng)