[#165] nns: Ignore domain expirations
All checks were successful
DCO action / DCO (pull_request) Successful in 25s
Code generation / Generate wrappers (pull_request) Successful in 52s
Tests / Tests (pull_request) Successful in 1m3s

Domain expirations undeniably complicate reasoning about contract behaviour:
1. SOA record expire field has a bit of a different semantics
2. For our coredns backend we would like to receive everything we put, sudden domain expirations can make life harder.
3. This expiration depends on block time, which in turn may differ from the real timestamp.

Close #165.

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
This commit is contained in:
Evgenii Stratonikov 2025-04-05 09:34:55 +03:00
parent 9283641cb4
commit b38d42baf3
Signed by: fyrchik
SSH key fingerprint: SHA256:m/TTwCzjnRkXgnzEx9X92ccxy1CcVeinOgDb3NPWWmg
4 changed files with 11 additions and 130 deletions

View file

@ -9,17 +9,12 @@ import (
type NameState struct { type NameState struct {
Owner interop.Hash160 Owner interop.Hash160
Name string Name string
// Expiration field used to contain wall-clock time of a domain expiration.
// It is preserved for backwards compatibility, but is unused by the contract and should be ignored.
Expiration int64 Expiration int64
Admin interop.Hash160 Admin interop.Hash160
} }
// ensureNotExpired panics if domain name is expired.
func (n NameState) ensureNotExpired() {
if int64(runtime.GetTime()) >= n.Expiration {
panic("name has expired")
}
}
// checkAdmin panics if script container is not signed by the domain name admin. // checkAdmin panics if script container is not signed by the domain name admin.
func (n NameState) checkAdmin() { func (n NameState) checkAdmin() {
if runtime.CheckWitness(n.Owner) { if runtime.CheckWitness(n.Owner) {

View file

@ -149,7 +149,6 @@ func Properties(tokenID []byte) map[string]any {
ns := getNameState(ctx, tokenID) ns := getNameState(ctx, tokenID)
return map[string]any{ return map[string]any{
"name": ns.Name, "name": ns.Name,
"expiration": ns.Expiration,
} }
} }
@ -308,7 +307,6 @@ func extractCnametgt(ctx storage.Context, name, domain string) string {
// checkParent returns parent domain or empty string if domain not found. // checkParent returns parent domain or empty string if domain not found.
func checkParent(ctx storage.Context, fragments []string) string { func checkParent(ctx storage.Context, fragments []string) string {
now := int64(runtime.GetTime())
last := len(fragments) - 1 last := len(fragments) - 1
name := fragments[last] name := fragments[last]
parent := "" parent := ""
@ -320,10 +318,6 @@ func checkParent(ctx storage.Context, fragments []string) string {
if nsBytes == nil { if nsBytes == nil {
continue continue
} }
ns := std.Deserialize(nsBytes.([]byte)).(NameState)
if now >= ns.Expiration {
panic("domain expired: " + name)
}
parent = name parent = name
} }
return parent return parent
@ -390,9 +384,6 @@ func register(ctx storage.Context, name string, owner interop.Hash160, email str
nsBytes := storage.Get(ctx, append([]byte{prefixName}, tokenKey...)) nsBytes := storage.Get(ctx, append([]byte{prefixName}, tokenKey...))
if nsBytes != nil { if nsBytes != nil {
ns := std.Deserialize(nsBytes.([]byte)).(NameState) ns := std.Deserialize(nsBytes.([]byte)).(NameState)
if int64(runtime.GetTime()) < ns.Expiration {
return false
}
oldOwner = ns.Owner oldOwner = ns.Owner
updateBalance(ctx, []byte(name), oldOwner, -1) updateBalance(ctx, []byte(name), oldOwner, -1)
} else { } else {
@ -401,8 +392,7 @@ func register(ctx storage.Context, name string, owner interop.Hash160, email str
ns := NameState{ ns := NameState{
Owner: owner, Owner: owner,
Name: name, Name: name,
// NNS expiration is in milliseconds Expiration: 0,
Expiration: int64(runtime.GetTime() + expire*1000),
} }
checkAvailableGlobalDomain(ctx, name) checkAvailableGlobalDomain(ctx, name)
@ -415,18 +405,6 @@ func register(ctx storage.Context, name string, owner interop.Hash160, email str
return true return true
} }
// Renew increases domain expiration date.
func Renew(name string) int64 {
checkDomainNameLength(name)
runtime.BurnGas(GetPrice())
ctx := storage.GetContext()
ns := getNameState(ctx, []byte(name))
ns.checkAdmin()
ns.Expiration += millisecondsInYear
putNameState(ctx, ns)
return ns.Expiration
}
// UpdateSOA updates soa record. // UpdateSOA updates soa record.
func UpdateSOA(name, email string, refresh, retry, expire, ttl int) { func UpdateSOA(name, email string, refresh, retry, expire, ttl int) {
checkDomainNameLength(name) checkDomainNameLength(name)
@ -731,9 +709,7 @@ func getNameStateWithKey(ctx storage.Context, tokenKey []byte) NameState {
if nsBytes == nil { if nsBytes == nil {
panic("token not found") panic("token not found")
} }
ns := std.Deserialize(nsBytes.([]byte)).(NameState) return std.Deserialize(nsBytes.([]byte)).(NameState)
ns.ensureNotExpired()
return ns
} }
// putNameState stores domain name state. // putNameState stores domain name state.
@ -801,7 +777,7 @@ func addRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType,
ns := NameState{ ns := NameState{
Name: globalDomain, Name: globalDomain,
Owner: nsOriginal.Owner, Owner: nsOriginal.Owner,
Expiration: nsOriginal.Expiration, Expiration: 0,
Admin: nsOriginal.Admin, Admin: nsOriginal.Admin,
} }
@ -1125,11 +1101,8 @@ func tokenIDFromName(name string) string {
nameKey := append([]byte{prefixName}, tokenKey...) nameKey := append([]byte{prefixName}, tokenKey...)
nsBytes := storage.Get(ctx, nameKey) nsBytes := storage.Get(ctx, nameKey)
if nsBytes != nil { if nsBytes != nil {
ns := std.Deserialize(nsBytes.([]byte)).(NameState)
if int64(runtime.GetTime()) < ns.Expiration {
return name[sum:] return name[sum:]
} }
}
sum += len(fragments[i]) + 1 sum += len(fragments[i]) + 1
} }
return name return name

View file

@ -286,28 +286,6 @@ func (c *Contract) RegisterUnsigned(name string, owner util.Uint160, email strin
return c.actor.MakeUnsignedRun(script, nil) return c.actor.MakeUnsignedRun(script, nil)
} }
// Renew creates a transaction invoking `renew` method of the contract.
// This transaction is signed and immediately sent to the network.
// The values returned are its hash, ValidUntilBlock value and error if any.
func (c *Contract) Renew(name string) (util.Uint256, uint32, error) {
return c.actor.SendCall(c.hash, "renew", name)
}
// RenewTransaction creates a transaction invoking `renew` method of the contract.
// This transaction is signed, but not sent to the network, instead it's
// returned to the caller.
func (c *Contract) RenewTransaction(name string) (*transaction.Transaction, error) {
return c.actor.MakeCall(c.hash, "renew", name)
}
// RenewUnsigned creates a transaction invoking `renew` method of the contract.
// This transaction is not signed, it's simply returned to the caller.
// Any fields of it that do not affect fees can be changed (ValidUntilBlock,
// Nonce), fee values (NetworkFee, SystemFee) can be increased as well.
func (c *Contract) RenewUnsigned(name string) (*transaction.Transaction, error) {
return c.actor.MakeUnsignedCall(c.hash, "renew", nil, name)
}
// SetAdmin creates a transaction invoking `setAdmin` method of the contract. // SetAdmin creates a transaction invoking `setAdmin` method of the contract.
// This transaction is signed and immediately sent to the network. // This transaction is signed and immediately sent to the network.
// The values returned are its hash, ValidUntilBlock value and error if any. // The values returned are its hash, ValidUntilBlock value and error if any.

View file

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"math/big" "math/big"
"path" "path"
"strings"
"testing" "testing"
"time" "time"
@ -551,45 +550,6 @@ func TestNNSGetAllRecords(t *testing.T) {
require.False(t, iter.Next()) require.False(t, iter.Next())
} }
func TestExpiration(t *testing.T) {
c := newNNSInvoker(t, true)
refresh, retry, expire, ttl := int64(101), int64(102), int64(msPerYear/1000*10), int64(104)
c.Invoke(t, true, "register",
"testdomain.com", c.CommitteeHash,
"myemail@frostfs.info", refresh, retry, expire, ttl)
checkProperties := func(t *testing.T, expiration uint64) {
expected := stackitem.NewMapWithValue([]stackitem.MapElement{
{Key: stackitem.Make("name"), Value: stackitem.Make("testdomain.com")},
{Key: stackitem.Make("expiration"), Value: stackitem.Make(expiration)},
})
s, err := c.TestInvoke(t, "properties", "testdomain.com")
require.NoError(t, err)
require.Equal(t, expected.Value(), s.Top().Item().Value())
}
top := c.TopBlock(t)
expiration := top.Timestamp + uint64(expire*1000)
checkProperties(t, expiration)
b := c.NewUnsignedBlock(t)
b.Timestamp = expiration - 2 // test invoke is done with +1 timestamp
require.NoError(t, c.Chain.AddBlock(c.SignBlock(b)))
checkProperties(t, expiration)
b = c.NewUnsignedBlock(t)
b.Timestamp = expiration - 1
require.NoError(t, c.Chain.AddBlock(c.SignBlock(b)))
_, err := c.TestInvoke(t, "properties", "testdomain.com")
require.Error(t, err)
require.True(t, strings.Contains(err.Error(), "name has expired"))
c.InvokeFail(t, "name has expired", "getAllRecords", "testdomain.com")
c.InvokeFail(t, "name has expired", "ownerOf", "testdomain.com")
}
func TestNNSSetAdmin(t *testing.T) { func TestNNSSetAdmin(t *testing.T) {
c := newNNSInvoker(t, true) c := newNNSInvoker(t, true)
@ -691,31 +651,6 @@ func TestNNSIsAvailable(t *testing.T) {
c.InvokeFail(t, "domain name too long", "isAvailable", getTooLongDomainName(255)) c.InvokeFail(t, "domain name too long", "isAvailable", getTooLongDomainName(255))
} }
func TestNNSRenew(t *testing.T) {
c := newNNSInvoker(t, true)
acc := c.NewAccount(t)
c1 := c.WithSigners(c.Committee, acc)
refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
c1.Invoke(t, true, "register",
"testdomain.com", c.CommitteeHash,
"myemail@frostfs.info", refresh, retry, expire, ttl)
const msPerYear = 365 * 24 * time.Hour / time.Millisecond
b := c.TopBlock(t)
ts := b.Timestamp + uint64(expire*1000) + uint64(msPerYear)
cAcc := c.WithSigners(acc)
cAcc.InvokeFail(t, "not witnessed by admin", "renew", "testdomain.com")
c1.Invoke(t, ts, "renew", "testdomain.com")
expected := stackitem.NewMapWithValue([]stackitem.MapElement{
{Key: stackitem.Make("name"), Value: stackitem.Make("testdomain.com")},
{Key: stackitem.Make("expiration"), Value: stackitem.Make(ts)},
})
cAcc.Invoke(t, expected, "properties", "testdomain.com")
c.InvokeFail(t, "domain name too long", "renew", getTooLongDomainName(255))
}
func TestNNSResolve(t *testing.T) { func TestNNSResolve(t *testing.T) {
c := newNNSInvoker(t, true) c := newNNSInvoker(t, true)