域名解析
域名註冊
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<Hash, AccountId>,
/// A hashmap to store all name to owners mapping
name_to_owner: storage::HashMap<Hash, AccountId>,
default_address: storage::Value<AccountId>,
}
其中 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<AccountId>,
new_address: AccountId,
}
event Transfer {
name: Hash,
from: AccountId,
old_owner: Option<AccountId>,
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::<Types>(alice);
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::<Types>(alice);
assert_eq!(contract.register(name), true);
// caller is not owner, set_address will be failed
env::test::set_caller::<Types>(bob);
assert_eq!(contract.set_address(name, bob), false);
// caller is owner, set_address will be successful
env::test::set_caller::<Types>(alice);
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::<Types>(alice);
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::<Types>(bob);
// 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,呼叫成功!
以上就是使用 ink 開發、測試、編譯、部署以及呼叫合約的主要流程,具體程式碼見simple_pns(https://github.com/PolkaX/simple_pns)