Secure and Decentralized Polkadot Domain Name System

ChainX
10 min readDec 2, 2019

--

Introduction

PNS (Polkadot Name Service) is a domain name system built on Polkadot. Its main function is domain name resolution which refers to the retrieval of numeric values corresponding to readable and easily memorable names such as “polka.dot” which can be used to facilitate transfers, voting and dapp-related operations instead of resorting to long IP addresses that are hard to remember. It also holds true for daily Internet usage: we use the domain name “google.com” other than its IP address.

The service that translates “google.com” into Google’s host IP is DNS (Domain Name Service). Currently, there are only 13 IPV4 root name servers worldwide, 9 of which are in the United States, 2 in Europe, and 1 in Asia, which qualifies as a highly centralized system over which one predominant concern has gone viral: the most powerful, direct, and probably the deadliest attack that could be mounted against the Internet is to wreak havoc on these root name servers.

Compared with DNS, PNS is immune to these malicious attacks against its name servers because it’s directly based on Polkadot, so it has a decentralized nature.

In addition to domain name resolution, PNS also provides secure and reliable domain name registration, auction, transfer, and transaction.

Domain name resolution

Domain name registration

eth-ens-namehash, a javascript library, provides hash and normalize to pre-process domain names and use UTS46 to standardize names. Although it supports UTF-8 encoded characters, some phishing names are hard to be sifted out. For example, faceboоk.eth and facebook.eth seem to be two identical strings, but both can be registered on ENS because the second ο in the first Facebook is actually the Greek letter Ομικρον, but it looks all the same. If this is allowed to exist, phishing-name attacks that are common in the Internet are still inevitable in blockchain.

That’s why we only allow the following characters in PNS: .abcdefghijklmnopqrstuvwxyz1234567890. Admittedly this comes with suspicions of disrespecting minority languages, but seeking political correctness at the expense of bringing undesirable asset risks to users is obviously not a sensible decision.

. this character is not strictly a character that can be used in PNS rules, but it does appear in domain names, for example: “polka.dot” and “chainx.polka.dot”, from which we can see . is the same as a domain name, distinguishing the hierarchies without actual meaning attached to it.

The length of domain name

• Short domain name (3–6 characters, auction required, example: chainx.dot)

• Long domain name (7–12 characters, rental fees for certain period and registration required, example: chainxpool.dot)

Registration steps

1 Fill in the domain name you want to register (such as chainx)

2 Select the expired date for the domain name (the default time setting is 1 year, but can be renewed with rental fees fluctuating according to the market, and certain discounts can be given to rental over 3 years)

3 Submit the application after paying the rental fee, and you’ll obtain the domain name after the transaction is done.

4 Set the default transaction address (optional) which can be changed.

Auction

• British-style auction, with a one-year rental fee as the starting price, no reserve price

• The system regularly releases short domain names for auctions. Within a specified period, users with the highest first-time bids will get the names.

If no one bids, the domain name will be placed at the starting price in the agency trading system through which users can buy the name.

Auction time

• 5–6 characters, 4 weeks

• 4 characters, 5 weeks

• 3 characters, 6 weeks

Domain attribute

Developers can access all the attributes of the domain name according to its address and build their own applications

Agency transaction

When users register domain names, it’s foreseeable that one day they want to sell these. The process of selling is as follows:

If Alice wants to sell her domain name, she needs to submit a transaction with the transaction price, commission rate (determined by the priorities you displayed in the PNS trading system), and time limit included to the “agency trading contract”. Her domain name will automatically enter the “agency trading system “ after the transaction is done and return to Alice’s account after the time limit expires. But before the expiring date, users can purchase the name via the “agency trading contract” if they wish.

However, there is one issue unsolved: if Alice and Bob know each other beforehand and they have negotiated the price, then there may be two possible scenarios after Alice posts about her name:

1 Bob did not complete the purchase in time, and the name was bought by someone else.

2 Alice informed Bob to purchase immediately after her posting, but it was still snatched up by some automatic scripts or malicious cybersquatters.

For these two cases, PNS provides the option to specify the purchaser’s address, so as long as Alice predetermines Bob’s address when submitting the request to the contract, it’s guaranteed that the name will only be purchased by Bob.

Bid transfer

You will be very frustrated when you find out the domain name you want has already been registered by others. In the real world, you may contact a broker who will help you ask if the owner willing to sell via the domain name management website and you will get the name after both sides agree. But in blockchain, it’s a different story. No one seems to be at hand even if you’re ready to pay a premium because of account anonymity.

Fortunately PNS also adopts a bid transfer system, so how does it work? Let’s imagine the following scenario:

Alice wants to register “polka.dot”, only to find out the name has been registered by an unknown user, which means the name is neither in the “auction system” or “agency trading system”, then Alice can submit a bid transfer application with her price offer and security deposit to the “bid transfer contract”.

When the unknown user logs in to PNS or any app that supports PNS (we will provide an “offer notification” plug-in or toolkit to all PNS-enabled applications), he or she will receive a transfer notification. The user takes the offer by submitting the domain to the “agency trading system” where Alice only needs pay the balance for “polka.dot”. If the offer is not accepted for two weeks, it will be automatically invalidated and Alice’s deposit will be returned. If Alice defaults, not paying the balance within two weeks, “polka.dot” will be released from the “agency trading system” and the previously paid deposit will be distributed to the “trading contract” and the name holder.

Features like decentralization, anonymity, and security are always put in the spotlight in almost all blockchain applications, but for those that really require interactions, anonymity may not be commendable. For example, haggling is inevitable as both sides could barely reach a consensus on the price in a transfer. Bargaining in smart contracts is technically feasible, but it is a waste of resources and time-consuming, to put it simply, applying only for the sake of the blockchain. What if we add users’ contact information (Email) as an attribute to domain names (real convenience may persuade users to fill in their e-mail addresses), so that two users who don’t know each other can negotiate the price more effectively and complete the transaction through the “agency trading contract” of PNS. This interactive method optimizing user experiences and ensuring transaction security may be more in line with most users’ demands.

Domain management

Basic information needs to be set after registering or purchasing a domain name

1 Changing mapping address

2 Adding a subdomain

3 changing owner

4 renewing

Contract implementation

At present, the official smart contract tools provided can already perform some basic functions, so we will demonstrate how to use ink to implement a simple PNS.

Prior to this, it is recommended to read the ink related tutorials first.

Here we mainly demonstrate domain registration, address setting, domain transfer, and domain name query.

Create a contract

Run cargo contract new simple-pns to create a new contract project.

Define the contract structure

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>,

}

Here name_to_address is a hashmap that stores domain names to mapping addresses, name_to_owner is a hashmap that stores domain names to domain name owners, and default_address is an empty address of AccountId type.

Initialize the contract

impl Deploy for SimplePns {

/// Initializes contract with default address.

fn deploy(&mut self) {

self.default_address.set(AccountId::from([0x0; 32]));

}

}

Implement the domain name

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

}

}

You can see some events are triggered by env.emit in the method above, so we also need to define these events:

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,

}

Write the test function

#[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);

}

}

Run the test

Use cargo + nightly test to run the contract function. If you get the following result, the function is working.

Compile the contract and ABI

Use cargo contract build to compile the contract, and cargo + nightly build — features ink-generate-abi to compile ABI. The corresponding wasm and json files will appear under target directory if running successfully.

Deploy the contract

Before deploying the contract, we need substrate — dev to start a substrate node locally, then clone polkadot-app and connect it to the local node.

Upload the corresponding files on the contracts page

Deploy the contract

Enter the following values ​​and click Deploy:

After successful deployment, you can call specific functions of the contract. But so far ink and the related tool chains are yet fully complete, env.println is the only available way to output the information via the console of substrate nodes so that we can verify data.

Note: env.println is only valid in substrate — dev mode

Now let’s test whether the domain registration is done properly~

Call the register function:

View the log in the console:

You can see that name in the console corresponds to 0x9e9de23f4d89d086c74c9fa23e4f4ceff6f9b68165b60b70290d1e5820f4bf4d. So the call is successful!

The above is the process of developing, testing, compiling, deploying, and calling contracts using ink. More information on codes, go to simple_pns (https://github.com/PolkaX/simple_pns)

About ChainX

--

--

ChainX
ChainX

Written by ChainX

ChainX is a BTC Layer 2 solution compatible with EVM that utilizes Bitcoin as a gas fee, serving as the predecessor network of BEVM.

No responses yet