Skip to content

Commit

Permalink
feat: Start a minimal CNS implementation. (#66)
Browse files Browse the repository at this point in the history
Start an implementation of a minimal CNS. This PR adds a very minimal
CNS root canister with basic tests.

---------

Co-authored-by: Nathan Mc Grath <[email protected]>
  • Loading branch information
przydatek and nathanosdev authored Nov 29, 2024
1 parent 0b1f707 commit 20efb14
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 18 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,9 @@ node_modules/
## generate output
out
dist

## dfx
.dfx/

## IDEs
.idea/
36 changes: 18 additions & 18 deletions canisters/name-registry/spec.did
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type DomainRecord = record {
// system, e.g. "CID", "A", "CNAME", "TXT", "MX", "AAAA", "NC", "NS", "DNSKEY", "NSEC".
//
// Also, "ANY" is a reserved type that can only be used in lookups to retrieve all records of a domain.
type : text;
record_type : text;
// The Time to Live (TTL) is a parameter in a record that specifies the amount of time for
// which the record should be cached before being refreshed from the authoritative naming canister.
//
Expand Down Expand Up @@ -48,15 +48,15 @@ type PaginationInfo = record {
limit : nat64;
// The offset of the first record in the result set.
start : nat64;
}
};

// Specify the pagination options for a result set.
type PaginationOptions = record {
// The offset of the first record in the result set, allowing the client to skip records.
start : nat64;
// The maximum number of records to return in the result set.
limit : nat64;
}
};

// Input parameters for the `get_records` operation.
type GetRecordsInput = record {
Expand Down Expand Up @@ -91,14 +91,14 @@ type DomainRecordInput = record {
// The domain name, e.g. "mydomain.test.", the name is required for all operations and must end with a dot (.).
name : text;
// The record type refers to the classification or category of a specific record within the system.
type : text;
record_type : text;
// The Time to Live (TTL) is a parameter in a record that specifies the amount of time for which the record
// should be cached. If not set the default value will be used.
ttl : nat32;
// The record data in a domain record refers to the specific information associated with that record type.
// If not set the default value will be used.
data : text;
}
};

// Input parameters for the `append` operation.
type AppendRecordOperationInput = DomainRecordInput;
Expand All @@ -112,7 +112,7 @@ type RemoveRecordOperationInput = record {
name : text;
// The type of the record to remove, same restrictions as the type of a DomainRecord apply.
// If no type is specified, all records with the specified name will be removed.
type : opt text;
record_type : opt text;
};

// The operation to execute on the records, the operation type specifies how the operation will be performed.
Expand Down Expand Up @@ -156,23 +156,23 @@ type Certification = record {
ic_certificate : Certificate;
// The state tree of the canister.
state_tree : StateTree;
}
};

// Information about the naming canister.
type NamingCanisterInfo = record {
// Wether or not the naming canister allows offchain signatures of domain record types.
allow_offchain_signatures : bool;
// The number of domains registered.
domains_registered : nat64;
}
};

// Result of the `get_info` operation.
type GetInfoResult = record {
// The certification information available to validate the query.
certification : Certification;
// Information about the naming canister.
info : NamingCanisterInfo;
}
};

// Input parameters for the `get_domains` operation.
type GetDomainsInput = record {
Expand All @@ -186,15 +186,15 @@ type GetDomainsInput = record {
type GetDomainsItem = record {
// The domain name.
domain : text;
}
};

// Result of the `get_domains` operation.
type GetDomainsResult = record {
// Pagination information about the result set.
info : PaginationInfo;
// The list of domains registered that the caller of the operation has access to.
items : vec GetDomainsItem;
}
};

// The init payload for the naming canister, which can be supplied on install and upgrade.
type NamingCanisterInit = record {
Expand All @@ -203,17 +203,17 @@ type NamingCanisterInit = record {
// send record types with RRSIG signatures, only tECDSA will be allowed, set it to true
// to enable records to be signed off-chain.
allow_offchain_signatures : opt bool;
}
};

service : (opt NamingCanisterInit) {
service : (opt NamingCanisterInit) -> {
// Lookup a domain name and return the records that match the specified record type.
lookup(domain : text, record_type : text) -> (DomainLookup) query;
lookup : (domain : text, record_type : text) -> (DomainLookup) query;
// Get records of the specified domain, the result set is paginated.
get_records(input : GetRecordsInput) -> (GetRecordsResult) query;
get_records : (input : GetRecordsInput) -> (GetRecordsResult) query;
// Get the list of domains registered that the caller of the operation has access to.
get_domains(input : GetDomainsInput) -> (GetDomainsResult) query;
get_domains : (input : GetDomainsInput) -> (GetDomainsResult) query;
// Manage records of the specified domain based on the list of operations.
manage_records(input : ManageRecordsInput) -> (ManageRecordsResult);
manage_records: (input : ManageRecordsInput) -> (ManageRecordsResult);
// Get information about the naming canister.
get_info() -> (GetInfoResult) query;
get_info: () -> (GetInfoResult) query;
};
21 changes: 21 additions & 0 deletions dfx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"dfx": "0.24.2",
"canisters": {
"cns_root": {
"main": "minimal_cns/src/backend/cns_root.mo",
"type": "motoko"
},
"cns_root_test": {
"main": "minimal_cns/src/backend/cns_root.test.mo",
"type": "motoko"
},
"name_registry": {
"main": "canisters/name-registry/src/main.rs",
"type": "rust",
"candid": "canisters/name-registry/spec.did",
"package": "cns-domain-registry"
}
},
"output_env_file": ".env",
"version": 1
}
26 changes: 26 additions & 0 deletions minimal_cns/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# A Minimal CNS (WIP)

This folder contains an experimental implementation of a "minimal" MVP CNS.
While it uses [the full CNS API](../canisters/name-registry/spec.did), it implements
only a small part of the API, necessary to support basic CNS use cases.
Currently, the following components are being worked on:
- A minimal [CNS root canister](./src/backend/cns_root.mo), that supports only the `lookup`-operation
for a single TLD (`.icp`), returning an NC-entry for that TLD, and otherwise returns unsupported/error
(in particular, it does not support registration of new TLD operators yet). Having such a CNS root
initially is to ensure that the client libraries’ flows are correct from the very beginning,
i.e. they won’t change once we add other TLDs.


## Test instuctions

```
dfx start --clean --background
dfx canister create name_registry
dfx canister create cns_root
dfx canister create cns_root_test
dfx deploy cns_root
dfx deploy cns_root_test
dfx canister call cns_root_test runTests "()"
```
39 changes: 39 additions & 0 deletions minimal_cns/src/backend/cns_root.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import NameRegistry "canister:name_registry";
import Text "mo:base/Text";

shared actor class () {
let icpTld = ".icp";
let icpTldCanisterId = "qoctq-giaaa-aaaaa-aaaea-cai";

public shared func lookup(domain : Text, recordType : Text) : async NameRegistry.DomainLookup {
var answers : [NameRegistry.DomainRecord] = [];
var authorities : [NameRegistry.DomainRecord] = [];

if (Text.endsWith(Text.toLowercase(domain), #text icpTld)) {
switch (Text.toUppercase(recordType)) {
case ("NC") {
answers := [{
name = ".icp.";
record_type = "NC";
ttl = 3600;
data = icpTldCanisterId;
}];
};
case _ {
authorities := [{
name = ".icp.";
record_type = "NC";
ttl = 3600;
data = icpTldCanisterId;
}];
};
};
};

{
answers = answers;
additionals = [];
authorities = authorities;
};
};
};
73 changes: 73 additions & 0 deletions minimal_cns/src/backend/cns_root.test.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import CnsRoot "canister:cns_root";

actor {
public func runTests() : async () {
await shouldGetIcpTldOperatorForNcIcpLookups();
await shouldGetIcpTldOperatorForOtherIcpLookups();
await shouldNotGetOtherTldOperator();
};

let icpTldCanisterId = "qoctq-giaaa-aaaaa-aaaea-cai";

func shouldGetIcpTldOperatorForNcIcpLookups() : async () {
for (
(domain, recordType) in [
(".icp", "NC"),
("example.icp", "NC"),
("another.ICP", "nc"),
("one.more.Icp", "Nc"),
].vals()
) {
let response = await CnsRoot.lookup(domain, recordType);
assert (response.answers.size() == 1);
assert (response.additionals.size() == 0);
assert (response.authorities.size() == 0);
let domainRecord = response.answers[0];
assert (domainRecord.name == ".icp.");
assert (domainRecord.record_type == "NC");
assert (domainRecord.ttl == 3600);
assert (domainRecord.data == icpTldCanisterId);
};
};

func shouldGetIcpTldOperatorForOtherIcpLookups() : async () {
for (
(domain, recordType) in [
(".icp", "CID"),
("example.icp", "Cid"),
("another.ICP", "cid"),
("one.more.Icp", "CId"),
("another.example.icp", "NS"),
("yet.another.one.icp", "WeirdReordType"),
].vals()
) {
let response = await CnsRoot.lookup(domain, recordType);
assert (response.answers.size() == 0);
assert (response.additionals.size() == 0);
assert (response.authorities.size() == 1);
let domainRecord = response.authorities[0];
assert (domainRecord.name == ".icp.");
assert (domainRecord.record_type == "NC");
assert (domainRecord.ttl == 3600);
assert (domainRecord.data == icpTldCanisterId);
};
};

func shouldNotGetOtherTldOperator() : async () {
for (
(domain, recordType) in [
(".fun", "NC"),
("example.com", "NC"),
("another.dfn", "NS"),
("", "NC"),
("one.more.dfn", "CID"),
].vals()
) {
let response = await CnsRoot.lookup(domain, recordType);
assert (response.answers.size() == 0);
assert (response.additionals.size() == 0);
assert (response.authorities.size() == 0);
};
};

};

0 comments on commit 20efb14

Please sign in to comment.