Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Start a minimal CNS implementation. #66

Merged
merged 4 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
};
};

};