forked from TrueCloudLab/frostfs-contract
nns: add implementation from neo-go
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
This commit is contained in:
parent
0db7c4b1cb
commit
25a6b00cb9
6 changed files with 767 additions and 1 deletions
5
Makefile
5
Makefile
|
@ -6,11 +6,12 @@ NEOGO?=neo-go
|
|||
.PHONY: all build sidechain
|
||||
build: all
|
||||
all: sidechain mainnet
|
||||
sidechain: alphabet morph
|
||||
sidechain: alphabet morph nns
|
||||
|
||||
alphabet_sc = alphabet
|
||||
morph_sc = audit balance container neofsid netmap proxy reputation
|
||||
mainnet_sc = neofs processing
|
||||
nns_sc = nns
|
||||
|
||||
define sc_template
|
||||
$(2)$(1)/$(1)_contract.nef: $(2)$(1)/$(1)_contract.go
|
||||
|
@ -24,10 +25,12 @@ endef
|
|||
$(foreach sc,$(alphabet_sc),$(eval $(call sc_template,$(sc))))
|
||||
$(foreach sc,$(morph_sc),$(eval $(call sc_template,$(sc))))
|
||||
$(foreach sc,$(mainnet_sc),$(eval $(call sc_template,$(sc))))
|
||||
$(foreach sc,$(nns_sc),$(eval $(call sc_template,$(sc))))
|
||||
|
||||
alphabet: $(foreach sc,$(alphabet_sc),$(sc)/$(sc)_contract.nef)
|
||||
morph: $(foreach sc,$(morph_sc),$(sc)/$(sc)_contract.nef)
|
||||
mainnet: $(foreach sc,$(mainnet_sc),$(sc)/$(sc)_contract.nef)
|
||||
nns: $(foreach sc,$(nns_sc),$(sc)/$(sc)_contract.nef)
|
||||
|
||||
clean:
|
||||
find . -name '*.nef' -exec rm -rf {} \;
|
||||
|
|
19
nns/config.yml
Normal file
19
nns/config.yml
Normal file
|
@ -0,0 +1,19 @@
|
|||
name: "NameService"
|
||||
supportedstandards: ["NEP-11"]
|
||||
safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf",
|
||||
"tokens", "properties", "roots", "getPrice", "isAvailable", "getRecord", "resolve"]
|
||||
events:
|
||||
- name: Transfer
|
||||
parameters:
|
||||
- name: from
|
||||
type: Hash160
|
||||
- name: to
|
||||
type: Hash160
|
||||
- name: amount
|
||||
type: Integer
|
||||
- name: tokenId
|
||||
type: ByteArray
|
||||
permissions:
|
||||
- hash: fffdc93764dbaddd97c48f252a53ea4643faa3fd
|
||||
methods: ["update"]
|
||||
- methods: ["onNEP11Payment"]
|
31
nns/namestate.go
Normal file
31
nns/namestate.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
package nns
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
||||
)
|
||||
|
||||
// NameState represents domain name state.
|
||||
type NameState struct {
|
||||
Owner interop.Hash160
|
||||
Name string
|
||||
Expiration int
|
||||
Admin interop.Hash160
|
||||
}
|
||||
|
||||
// ensureNotExpired panics if domain name is expired.
|
||||
func (n NameState) ensureNotExpired() {
|
||||
if runtime.GetTime() >= n.Expiration {
|
||||
panic("name has expired")
|
||||
}
|
||||
}
|
||||
|
||||
// checkAdmin panics if script container is not signed by the domain name admin.
|
||||
func (n NameState) checkAdmin() {
|
||||
if runtime.CheckWitness(n.Owner) {
|
||||
return
|
||||
}
|
||||
if n.Admin == nil || !runtime.CheckWitness(n.Admin) {
|
||||
panic("not witnessed by admin")
|
||||
}
|
||||
}
|
19
nns/nns.yml
Normal file
19
nns/nns.yml
Normal file
|
@ -0,0 +1,19 @@
|
|||
name: "NameService"
|
||||
supportedstandards: ["NEP-11"]
|
||||
safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf",
|
||||
"tokens", "properties", "roots", "getPrice", "isAvailable", "getRecord", "resolve"]
|
||||
events:
|
||||
- name: Transfer
|
||||
parameters:
|
||||
- name: from
|
||||
type: Hash160
|
||||
- name: to
|
||||
type: Hash160
|
||||
- name: amount
|
||||
type: Integer
|
||||
- name: tokenId
|
||||
type: ByteArray
|
||||
permissions:
|
||||
- hash: fffdc93764dbaddd97c48f252a53ea4643faa3fd
|
||||
methods: ["update"]
|
||||
- methods: ["onNEP11Payment"]
|
674
nns/nns_contract.go
Normal file
674
nns/nns_contract.go
Normal file
|
@ -0,0 +1,674 @@
|
|||
/*
|
||||
Package nns contains non-divisible non-fungible NEP11-compatible token
|
||||
implementation. This token is a compatible analogue of C# Neo Name Service
|
||||
token and is aimed to serve as a domain name service for Neo smart-contracts,
|
||||
thus it's NeoNameService. This token can be minted with new domain name
|
||||
registration, the domain name itself is your NFT. Corresponding domain root
|
||||
must be added by the committee before new domain name can be registered.
|
||||
*/
|
||||
package nns
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/iterator"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/native/crypto"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/native/management"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/native/neo"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/native/std"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/util"
|
||||
)
|
||||
|
||||
// Prefixes used for contract data storage.
|
||||
const (
|
||||
// prefixTotalSupply contains total supply of minted domains.
|
||||
prefixTotalSupply byte = 0x00
|
||||
// prefixBalance contains map from owner to his balance.
|
||||
prefixBalance byte = 0x01
|
||||
// prefixAccountToken contains map from (owner + token key) to token ID,
|
||||
// where token key = hash160(token ID) and token ID = domain name.
|
||||
prefixAccountToken byte = 0x02
|
||||
// prefixRegisterPrice contains price for new domain name registration.
|
||||
prefixRegisterPrice byte = 0x10
|
||||
// prefixRoot contains set of roots (map from root to 0).
|
||||
prefixRoot byte = 0x20
|
||||
// prefixName contains map from token key to token where token is domain
|
||||
// NameState structure.
|
||||
prefixName byte = 0x21
|
||||
// prefixRecord contains map from (token key + hash160(token name) + record type)
|
||||
// to record.
|
||||
prefixRecord byte = 0x22
|
||||
)
|
||||
|
||||
// Values constraints.
|
||||
const (
|
||||
// maxRegisterPrice is the maximum price of register method.
|
||||
maxRegisterPrice = 1_0000_0000_0000
|
||||
// maxRootLength is the maximum domain root length.
|
||||
maxRootLength = 16
|
||||
// maxDomainNameFragmentLength is the maximum length of the domain name fragment.
|
||||
maxDomainNameFragmentLength = 62
|
||||
// minDomainNameLength is minimum domain length.
|
||||
minDomainNameLength = 3
|
||||
// maxDomainNameLength is maximum domain length.
|
||||
maxDomainNameLength = 255
|
||||
// maxTXTRecordLength is the maximum length of the TXT domain record.
|
||||
maxTXTRecordLength = 255
|
||||
)
|
||||
|
||||
// Other constants.
|
||||
const (
|
||||
// defaultRegisterPrice is the default price for new domain registration.
|
||||
defaultRegisterPrice = 10_0000_0000
|
||||
// millisecondsInYear is amount of milliseconds per year.
|
||||
millisecondsInYear = 365 * 24 * 3600 * 1000
|
||||
)
|
||||
|
||||
// Update updates NameService contract.
|
||||
func Update(nef []byte, manifest string) {
|
||||
checkCommittee()
|
||||
management.Update(nef, []byte(manifest))
|
||||
}
|
||||
|
||||
// _deploy initializes defaults (total supply and registration price) on contract deploy.
|
||||
func _deploy(data interface{}, isUpdate bool) {
|
||||
if isUpdate {
|
||||
return
|
||||
}
|
||||
ctx := storage.GetContext()
|
||||
storage.Put(ctx, []byte{prefixTotalSupply}, 0)
|
||||
storage.Put(ctx, []byte{prefixRegisterPrice}, defaultRegisterPrice)
|
||||
}
|
||||
|
||||
// Symbol returns NeoNameService symbol.
|
||||
func Symbol() string {
|
||||
return "NNS"
|
||||
}
|
||||
|
||||
// Decimals returns NeoNameService decimals.
|
||||
func Decimals() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// TotalSupply returns overall number of domains minted by the NeoNameService contract.
|
||||
func TotalSupply() int {
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
return getTotalSupply(ctx)
|
||||
}
|
||||
|
||||
// OwnerOf returns owner of the specified domain.
|
||||
func OwnerOf(tokenID []byte) interop.Hash160 {
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
ns := getNameState(ctx, tokenID)
|
||||
return ns.Owner
|
||||
}
|
||||
|
||||
// Properties returns domain name and expiration date of the specified domain.
|
||||
func Properties(tokenID []byte) map[string]interface{} {
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
ns := getNameState(ctx, tokenID)
|
||||
return map[string]interface{}{
|
||||
"name": ns.Name,
|
||||
"expiration": ns.Expiration,
|
||||
}
|
||||
}
|
||||
|
||||
// BalanceOf returns overall number of domains owned by the specified owner.
|
||||
func BalanceOf(owner interop.Hash160) int {
|
||||
if !isValid(owner) {
|
||||
panic(`invalid owner`)
|
||||
}
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
balance := storage.Get(ctx, append([]byte{prefixBalance}, owner...))
|
||||
if balance == nil {
|
||||
return 0
|
||||
}
|
||||
return balance.(int)
|
||||
}
|
||||
|
||||
// Tokens returns iterator over a set of all registered domain names.
|
||||
func Tokens() iterator.Iterator {
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
return storage.Find(ctx, []byte{prefixName}, storage.ValuesOnly|storage.DeserializeValues|storage.PickField1)
|
||||
}
|
||||
|
||||
// TokensOf returns iterator over minted domains owned by the specified owner.
|
||||
func TokensOf(owner interop.Hash160) iterator.Iterator {
|
||||
if !isValid(owner) {
|
||||
panic(`invalid owner`)
|
||||
}
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
return storage.Find(ctx, append([]byte{prefixAccountToken}, owner...), storage.ValuesOnly)
|
||||
}
|
||||
|
||||
// Transfer transfers domain with the specified name to new owner.
|
||||
func Transfer(to interop.Hash160, tokenID []byte, data interface{}) bool {
|
||||
if !isValid(to) {
|
||||
panic(`invalid receiver`)
|
||||
}
|
||||
var (
|
||||
tokenKey = getTokenKey(tokenID)
|
||||
ctx = storage.GetContext()
|
||||
)
|
||||
ns := getNameStateWithKey(ctx, tokenKey)
|
||||
from := ns.Owner
|
||||
if !runtime.CheckWitness(from) {
|
||||
return false
|
||||
}
|
||||
if !util.Equals(from, to) {
|
||||
// update token info
|
||||
ns.Owner = to
|
||||
ns.Admin = nil
|
||||
putNameStateWithKey(ctx, tokenKey, ns)
|
||||
|
||||
// update `from` balance
|
||||
updateBalance(ctx, tokenID, from, -1)
|
||||
|
||||
// update `to` balance
|
||||
updateBalance(ctx, tokenID, to, +1)
|
||||
}
|
||||
postTransfer(from, to, tokenID, data)
|
||||
return true
|
||||
}
|
||||
|
||||
// AddRoot registers new root.
|
||||
func AddRoot(root string) {
|
||||
checkCommittee()
|
||||
if !checkFragment(root, true) {
|
||||
panic("invalid root format")
|
||||
}
|
||||
var (
|
||||
ctx = storage.GetContext()
|
||||
rootKey = append([]byte{prefixRoot}, []byte(root)...)
|
||||
)
|
||||
if storage.Get(ctx, rootKey) != nil {
|
||||
panic("root already exists")
|
||||
}
|
||||
storage.Put(ctx, rootKey, 0)
|
||||
}
|
||||
|
||||
// Roots returns iterator over a set of NameService roots.
|
||||
func Roots() iterator.Iterator {
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
return storage.Find(ctx, []byte{prefixRoot}, storage.KeysOnly|storage.RemovePrefix)
|
||||
}
|
||||
|
||||
// SetPrice sets the domain registration price.
|
||||
func SetPrice(price int) {
|
||||
checkCommittee()
|
||||
if price < 0 || price > maxRegisterPrice {
|
||||
panic("The price is out of range.")
|
||||
}
|
||||
ctx := storage.GetContext()
|
||||
storage.Put(ctx, []byte{prefixRegisterPrice}, price)
|
||||
}
|
||||
|
||||
// GetPrice returns the domain registration price.
|
||||
func GetPrice() int {
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
return storage.Get(ctx, []byte{prefixRegisterPrice}).(int)
|
||||
}
|
||||
|
||||
// IsAvailable checks whether provided domain name is available.
|
||||
func IsAvailable(name string) bool {
|
||||
fragments := splitAndCheck(name, false)
|
||||
if fragments == nil {
|
||||
panic("invalid domain name format")
|
||||
}
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
if storage.Get(ctx, append([]byte{prefixRoot}, []byte(fragments[1])...)) == nil {
|
||||
panic("root not found")
|
||||
}
|
||||
nsBytes := storage.Get(ctx, append([]byte{prefixName}, getTokenKey([]byte(name))...))
|
||||
if nsBytes == nil {
|
||||
return true
|
||||
}
|
||||
ns := std.Deserialize(nsBytes.([]byte)).(NameState)
|
||||
return runtime.GetTime() >= ns.Expiration
|
||||
}
|
||||
|
||||
// Register registers new domain with the specified owner and name if it's available.
|
||||
func Register(name string, owner interop.Hash160) bool {
|
||||
fragments := splitAndCheck(name, false)
|
||||
if fragments == nil {
|
||||
panic("invalid domain name format")
|
||||
}
|
||||
ctx := storage.GetContext()
|
||||
if storage.Get(ctx, append([]byte{prefixRoot}, []byte(fragments[1])...)) == nil {
|
||||
panic("root not found")
|
||||
}
|
||||
|
||||
if !isValid(owner) {
|
||||
panic("invalid owner")
|
||||
}
|
||||
if !runtime.CheckWitness(owner) {
|
||||
panic("not witnessed by owner")
|
||||
}
|
||||
runtime.BurnGas(GetPrice())
|
||||
var (
|
||||
tokenKey = getTokenKey([]byte(name))
|
||||
oldOwner interop.Hash160
|
||||
)
|
||||
nsBytes := storage.Get(ctx, append([]byte{prefixName}, tokenKey...))
|
||||
if nsBytes != nil {
|
||||
ns := std.Deserialize(nsBytes.([]byte)).(NameState)
|
||||
if runtime.GetTime() < ns.Expiration {
|
||||
return false
|
||||
}
|
||||
oldOwner = ns.Owner
|
||||
updateBalance(ctx, []byte(name), oldOwner, -1)
|
||||
} else {
|
||||
updateTotalSupply(ctx, +1)
|
||||
}
|
||||
ns := NameState{
|
||||
Owner: owner,
|
||||
Name: name,
|
||||
Expiration: runtime.GetTime() + millisecondsInYear,
|
||||
}
|
||||
putNameStateWithKey(ctx, tokenKey, ns)
|
||||
updateBalance(ctx, []byte(name), owner, +1)
|
||||
postTransfer(oldOwner, owner, []byte(name), nil)
|
||||
return true
|
||||
}
|
||||
|
||||
// Renew increases domain expiration date.
|
||||
func Renew(name string) int {
|
||||
if len(name) > maxDomainNameLength {
|
||||
panic("invalid domain name format")
|
||||
}
|
||||
runtime.BurnGas(GetPrice())
|
||||
ctx := storage.GetContext()
|
||||
ns := getNameState(ctx, []byte(name))
|
||||
ns.Expiration += millisecondsInYear
|
||||
putNameState(ctx, ns)
|
||||
return ns.Expiration
|
||||
}
|
||||
|
||||
// SetAdmin updates domain admin.
|
||||
func SetAdmin(name string, admin interop.Hash160) {
|
||||
if len(name) > maxDomainNameLength {
|
||||
panic("invalid domain name format")
|
||||
}
|
||||
if admin != nil && !runtime.CheckWitness(admin) {
|
||||
panic("not witnessed by admin")
|
||||
}
|
||||
ctx := storage.GetContext()
|
||||
ns := getNameState(ctx, []byte(name))
|
||||
if !runtime.CheckWitness(ns.Owner) {
|
||||
panic("not witnessed by owner")
|
||||
}
|
||||
ns.Admin = admin
|
||||
putNameState(ctx, ns)
|
||||
}
|
||||
|
||||
// SetRecord adds new record of the specified type to the provided domain.
|
||||
func SetRecord(name string, typ RecordType, data string) {
|
||||
tokenID := []byte(tokenIDFromName(name))
|
||||
var ok bool
|
||||
switch typ {
|
||||
case A:
|
||||
ok = checkIPv4(data)
|
||||
case CNAME:
|
||||
ok = splitAndCheck(data, true) != nil
|
||||
case TXT:
|
||||
ok = len(data) <= maxTXTRecordLength
|
||||
case AAAA:
|
||||
ok = checkIPv6(data)
|
||||
default:
|
||||
panic("unsupported record type")
|
||||
}
|
||||
if !ok {
|
||||
panic("invalid record data")
|
||||
}
|
||||
ctx := storage.GetContext()
|
||||
ns := getNameState(ctx, tokenID)
|
||||
ns.checkAdmin()
|
||||
putRecord(ctx, tokenID, name, typ, data)
|
||||
}
|
||||
|
||||
// GetRecord returns domain record of the specified type if it exists or an empty
|
||||
// string if not.
|
||||
func GetRecord(name string, typ RecordType) string {
|
||||
tokenID := []byte(tokenIDFromName(name))
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
_ = getNameState(ctx, tokenID) // ensure not expired
|
||||
return getRecord(ctx, tokenID, name, typ)
|
||||
}
|
||||
|
||||
// DeleteRecord removes domain record with the specified type.
|
||||
func DeleteRecord(name string, typ RecordType) {
|
||||
tokenID := []byte(tokenIDFromName(name))
|
||||
ctx := storage.GetContext()
|
||||
ns := getNameState(ctx, tokenID)
|
||||
ns.checkAdmin()
|
||||
recordKey := getRecordKey(tokenID, name, typ)
|
||||
storage.Delete(ctx, recordKey)
|
||||
}
|
||||
|
||||
// Resolve resolves given name (not more then three redirects are allowed).
|
||||
func Resolve(name string, typ RecordType) string {
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
return resolve(ctx, name, typ, 2)
|
||||
}
|
||||
|
||||
// updateBalance updates account's balance and account's tokens.
|
||||
func updateBalance(ctx storage.Context, tokenId []byte, acc interop.Hash160, diff int) {
|
||||
balanceKey := append([]byte{prefixBalance}, acc...)
|
||||
var balance int
|
||||
if b := storage.Get(ctx, balanceKey); b != nil {
|
||||
balance = b.(int)
|
||||
}
|
||||
balance += diff
|
||||
if balance == 0 {
|
||||
storage.Delete(ctx, balanceKey)
|
||||
} else {
|
||||
storage.Put(ctx, balanceKey, balance)
|
||||
}
|
||||
|
||||
tokenKey := getTokenKey(tokenId)
|
||||
accountTokenKey := append(append([]byte{prefixAccountToken}, acc...), tokenKey...)
|
||||
if diff < 0 {
|
||||
storage.Delete(ctx, accountTokenKey)
|
||||
} else {
|
||||
storage.Put(ctx, accountTokenKey, tokenId)
|
||||
}
|
||||
}
|
||||
|
||||
// postTransfer sends Transfer notification to the network and calls onNEP11Payment
|
||||
// method.
|
||||
func postTransfer(from, to interop.Hash160, tokenID []byte, data interface{}) {
|
||||
runtime.Notify("Transfer", from, to, 1, tokenID)
|
||||
if management.GetContract(to) != nil {
|
||||
contract.Call(to, "onNEP11Payment", contract.All, from, 1, tokenID, data)
|
||||
}
|
||||
}
|
||||
|
||||
// getTotalSupply returns total supply from storage.
|
||||
func getTotalSupply(ctx storage.Context) int {
|
||||
val := storage.Get(ctx, []byte{prefixTotalSupply})
|
||||
return val.(int)
|
||||
}
|
||||
|
||||
// updateTotalSupply adds specified diff to the total supply.
|
||||
func updateTotalSupply(ctx storage.Context, diff int) {
|
||||
tsKey := []byte{prefixTotalSupply}
|
||||
ts := getTotalSupply(ctx)
|
||||
storage.Put(ctx, tsKey, ts+diff)
|
||||
}
|
||||
|
||||
// getTokenKey computes hash160 from the given tokenID.
|
||||
func getTokenKey(tokenID []byte) []byte {
|
||||
return crypto.Ripemd160(tokenID)
|
||||
}
|
||||
|
||||
// getNameState returns domain name state by the specified tokenID.
|
||||
func getNameState(ctx storage.Context, tokenID []byte) NameState {
|
||||
tokenKey := getTokenKey(tokenID)
|
||||
return getNameStateWithKey(ctx, tokenKey)
|
||||
}
|
||||
|
||||
// getNameStateWithKey returns domain name state by the specified token key.
|
||||
func getNameStateWithKey(ctx storage.Context, tokenKey []byte) NameState {
|
||||
nameKey := append([]byte{prefixName}, tokenKey...)
|
||||
nsBytes := storage.Get(ctx, nameKey)
|
||||
if nsBytes == nil {
|
||||
panic("token not found")
|
||||
}
|
||||
ns := std.Deserialize(nsBytes.([]byte)).(NameState)
|
||||
ns.ensureNotExpired()
|
||||
return ns
|
||||
}
|
||||
|
||||
// putNameState stores domain name state.
|
||||
func putNameState(ctx storage.Context, ns NameState) {
|
||||
tokenKey := getTokenKey([]byte(ns.Name))
|
||||
putNameStateWithKey(ctx, tokenKey, ns)
|
||||
}
|
||||
|
||||
// putNameStateWithKey stores domain name state with the specified token key.
|
||||
func putNameStateWithKey(ctx storage.Context, tokenKey []byte, ns NameState) {
|
||||
nameKey := append([]byte{prefixName}, tokenKey...)
|
||||
nsBytes := std.Serialize(ns)
|
||||
storage.Put(ctx, nameKey, nsBytes)
|
||||
}
|
||||
|
||||
// getRecord returns domain record.
|
||||
func getRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType) string {
|
||||
recordKey := getRecordKey(tokenId, name, typ)
|
||||
record := storage.Get(ctx, recordKey)
|
||||
return record.(string)
|
||||
}
|
||||
|
||||
// putRecord stores domain record.
|
||||
func putRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType, record string) {
|
||||
recordKey := getRecordKey(tokenId, name, typ)
|
||||
storage.Put(ctx, recordKey, record)
|
||||
}
|
||||
|
||||
// getRecordKey returns key used to store domain records.
|
||||
func getRecordKey(tokenId []byte, name string, typ RecordType) []byte {
|
||||
recordKey := append([]byte{prefixRecord}, getTokenKey(tokenId)...)
|
||||
recordKey = append(recordKey, getTokenKey([]byte(name))...)
|
||||
return append(recordKey, []byte{byte(typ)}...)
|
||||
}
|
||||
|
||||
// isValid returns true if the provided address is a valid Uint160.
|
||||
func isValid(address interop.Hash160) bool {
|
||||
return address != nil && len(address) == 20
|
||||
}
|
||||
|
||||
// checkCommittee panics if the script container is not signed by the committee.
|
||||
func checkCommittee() {
|
||||
committee := neo.GetCommittee()
|
||||
if committee == nil {
|
||||
panic("failed to get committee")
|
||||
}
|
||||
l := len(committee)
|
||||
committeeMultisig := contract.CreateMultisigAccount(l-(l-1)/2, committee)
|
||||
if committeeMultisig == nil || !runtime.CheckWitness(committeeMultisig) {
|
||||
panic("not witnessed by committee")
|
||||
}
|
||||
}
|
||||
|
||||
// checkFragment validates root or a part of domain name.
|
||||
func checkFragment(v string, isRoot bool) bool {
|
||||
maxLength := maxDomainNameFragmentLength
|
||||
if isRoot {
|
||||
maxLength = maxRootLength
|
||||
}
|
||||
if len(v) == 0 || len(v) > maxLength {
|
||||
return false
|
||||
}
|
||||
c := v[0]
|
||||
if isRoot {
|
||||
if !(c >= 'a' && c <= 'z') {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if !isAlNum(c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for i := 1; i < len(v); i++ {
|
||||
if !isAlNum(v[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// isAlNum checks whether provided char is a lowercase letter or a number.
|
||||
func isAlNum(c uint8) bool {
|
||||
return c >= 'a' && c <= 'z' || c >= '0' && c <= '9'
|
||||
}
|
||||
|
||||
// splitAndCheck splits domain name into parts and validates it.
|
||||
func splitAndCheck(name string, allowMultipleFragments bool) []string {
|
||||
l := len(name)
|
||||
if l < minDomainNameLength || maxDomainNameLength < l {
|
||||
return nil
|
||||
}
|
||||
fragments := std.StringSplit(name, ".")
|
||||
l = len(fragments)
|
||||
if l < 2 {
|
||||
return nil
|
||||
}
|
||||
if l > 2 && !allowMultipleFragments {
|
||||
return nil
|
||||
}
|
||||
for i := 0; i < l; i++ {
|
||||
if !checkFragment(fragments[i], i == l-1) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fragments
|
||||
}
|
||||
|
||||
// checkIPv4 checks record on IPv4 compliance.
|
||||
func checkIPv4(data string) bool {
|
||||
l := len(data)
|
||||
if l < 7 || 15 < l {
|
||||
return false
|
||||
}
|
||||
fragments := std.StringSplit(data, ".")
|
||||
if len(fragments) != 4 {
|
||||
return false
|
||||
}
|
||||
numbers := make([]int, 4)
|
||||
for i, f := range fragments {
|
||||
if len(f) == 0 {
|
||||
return false
|
||||
}
|
||||
number := std.Atoi10(f)
|
||||
if number < 0 || 255 < number {
|
||||
panic("not a byte")
|
||||
}
|
||||
if number > 0 && f[0] == '0' {
|
||||
return false
|
||||
}
|
||||
if number == 0 && len(f) > 1 {
|
||||
return false
|
||||
}
|
||||
numbers[i] = number
|
||||
}
|
||||
n0 := numbers[0]
|
||||
n1 := numbers[1]
|
||||
n3 := numbers[3]
|
||||
if n0 == 0 ||
|
||||
n0 == 10 ||
|
||||
n0 == 127 ||
|
||||
n0 >= 224 ||
|
||||
(n0 == 169 && n1 == 254) ||
|
||||
(n0 == 172 && 16 <= n1 && n1 <= 31) ||
|
||||
(n0 == 192 && n1 == 168) ||
|
||||
n3 == 0 ||
|
||||
n3 == 255 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// checkIPv6 checks record on IPv6 compliance.
|
||||
func checkIPv6(data string) bool {
|
||||
l := len(data)
|
||||
if l < 2 || 39 < l {
|
||||
return false
|
||||
}
|
||||
fragments := std.StringSplit(data, ":")
|
||||
l = len(fragments)
|
||||
if l < 3 || 8 < l {
|
||||
return false
|
||||
}
|
||||
var hasEmpty bool
|
||||
nums := make([]int, 8)
|
||||
for i, f := range fragments {
|
||||
if len(f) == 0 {
|
||||
if i == 0 {
|
||||
nums[i] = 0
|
||||
} else if i == l-1 {
|
||||
nums[7] = 0
|
||||
} else if hasEmpty {
|
||||
return false
|
||||
} else {
|
||||
hasEmpty = true
|
||||
endIndex := 9 - l + i
|
||||
for j := i; j < endIndex; j++ {
|
||||
nums[j] = 0
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if len(f) > 4 {
|
||||
return false
|
||||
}
|
||||
n := std.Atoi(f, 16)
|
||||
if 65535 < n {
|
||||
panic("fragment overflows uint16: " + f)
|
||||
}
|
||||
idx := i
|
||||
if hasEmpty {
|
||||
idx = i + 8 - l
|
||||
}
|
||||
nums[idx] = n
|
||||
}
|
||||
}
|
||||
|
||||
f0 := nums[0]
|
||||
if f0 < 0x2000 || f0 == 0x2002 || f0 == 0x3ffe || f0 > 0x3fff { // IPv6 Global Unicast https://www.iana.org/assignments/ipv6-address-space/ipv6-address-space.xhtml
|
||||
return false
|
||||
}
|
||||
if f0 == 0x2001 {
|
||||
f1 := nums[1]
|
||||
if f1 < 0x200 || f1 == 0xdb8 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// tokenIDFromName returns token ID (domain.root) from provided name.
|
||||
func tokenIDFromName(name string) string {
|
||||
fragments := splitAndCheck(name, true)
|
||||
if fragments == nil {
|
||||
panic("invalid domain name format")
|
||||
}
|
||||
l := len(fragments)
|
||||
return name[len(name)-(len(fragments[l-1])+len(fragments[l-2])+1):]
|
||||
}
|
||||
|
||||
// resolve resolves provided name using record with the specified type and given
|
||||
// maximum redirections constraint.
|
||||
func resolve(ctx storage.Context, name string, typ RecordType, redirect int) string {
|
||||
if redirect < 0 {
|
||||
panic("invalid redirect")
|
||||
}
|
||||
records := getRecords(ctx, name)
|
||||
cname := ""
|
||||
for iterator.Next(records) {
|
||||
r := iterator.Value(records).([]string)
|
||||
key := []byte(r[0])
|
||||
value := r[1]
|
||||
rTyp := key[len(key)-1]
|
||||
if rTyp == byte(typ) {
|
||||
return value
|
||||
}
|
||||
if rTyp == byte(CNAME) {
|
||||
cname = value
|
||||
}
|
||||
}
|
||||
if cname == "" {
|
||||
return string([]byte(nil))
|
||||
}
|
||||
return resolve(ctx, cname, typ, redirect-1)
|
||||
}
|
||||
|
||||
// getRecords returns iterator over the set of records corresponded with the
|
||||
// specified name.
|
||||
func getRecords(ctx storage.Context, name string) iterator.Iterator {
|
||||
tokenID := []byte(tokenIDFromName(name))
|
||||
_ = getNameState(ctx, tokenID)
|
||||
recordsKey := append([]byte{prefixRecord}, getTokenKey(tokenID)...)
|
||||
recordsKey = append(recordsKey, getTokenKey([]byte(name))...)
|
||||
return storage.Find(ctx, recordsKey, storage.None)
|
||||
}
|
20
nns/recordtype.go
Normal file
20
nns/recordtype.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package nns
|
||||
|
||||
// RecordType is domain name service record types.
|
||||
type RecordType byte
|
||||
|
||||
// Record types defined in [RFC 1035](https://tools.ietf.org/html/rfc1035)
|
||||
const (
|
||||
// A represents address record type.
|
||||
A RecordType = 1
|
||||
// CNAME represents canonical name record type.
|
||||
CNAME RecordType = 5
|
||||
// TXT represents text record type.
|
||||
TXT RecordType = 16
|
||||
)
|
||||
|
||||
// Record types defined in [RFC 3596](https://tools.ietf.org/html/rfc3596)
|
||||
const (
|
||||
// AAAA represents IPv6 address record type.
|
||||
AAAA RecordType = 28
|
||||
)
|
Loading…
Reference in a new issue