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

@ -7,19 +7,14 @@ import (
// NameState represents domain name state.
type NameState struct {
Owner interop.Hash160
Name string
Owner interop.Hash160
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
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.
func (n NameState) checkAdmin() {
if runtime.CheckWitness(n.Owner) {

View file

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

View file

@ -286,28 +286,6 @@ func (c *Contract) RegisterUnsigned(name string, owner util.Uint160, email strin
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.
// This transaction is signed and immediately sent to the network.
// The values returned are its hash, ValidUntilBlock value and error if any.

View file

@ -4,7 +4,6 @@ import (
"fmt"
"math/big"
"path"
"strings"
"testing"
"time"
@ -551,45 +550,6 @@ func TestNNSGetAllRecords(t *testing.T) {
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) {
c := newNNSInvoker(t, true)
@ -691,31 +651,6 @@ func TestNNSIsAvailable(t *testing.T) {
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) {
c := newNNSInvoker(t, true)