frostfs-contract/tests/nns_test.go

432 lines
14 KiB
Go

package tests
import (
"fmt"
"math/big"
"path"
"strings"
"testing"
"time"
"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
"github.com/nspcc-dev/neo-go/pkg/core/interop/storage"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/neotest"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/gas"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require"
)
const nnsPath = "../nns"
const msPerYear = 365 * 24 * time.Hour / time.Millisecond
func newNNSInvoker(t *testing.T, addRoot bool) *neotest.ContractInvoker {
e := newExecutor(t)
ctr := neotest.CompileFile(t, e.CommitteeHash, nnsPath, path.Join(nnsPath, "config.yml"))
e.DeployContract(t, ctr, nil)
c := e.CommitteeInvoker(ctr.Hash)
if addRoot {
// Set expiration big enough to pass all tests.
refresh, retry, expire, ttl := int64(101), int64(102), int64(msPerYear/1000*100), int64(104)
c.Invoke(t, true, "register",
"com", c.CommitteeHash,
"myemail@frostfs.info", refresh, retry, expire, ttl)
}
return c
}
func TestNNSGeneric(t *testing.T) {
c := newNNSInvoker(t, false)
c.Invoke(t, "NNS", "symbol")
c.Invoke(t, 0, "decimals")
c.Invoke(t, 0, "totalSupply")
}
func TestNNSRegisterTLD(t *testing.T) {
c := newNNSInvoker(t, false)
refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
c.InvokeFail(t, "invalid domain name format", "register",
"0com", c.CommitteeHash,
"email@frostfs.info", refresh, retry, expire, ttl)
acc := c.NewAccount(t)
cAcc := c.WithSigners(acc)
cAcc.InvokeFail(t, "not witnessed by committee", "register",
"com", acc.ScriptHash(),
"email@frostfs.info", refresh, retry, expire, ttl)
t.Run("size checks", func(t *testing.T) {
c.Invoke(t, true, "register",
"ns", c.CommitteeHash,
"email@frostfs.info", refresh, retry, expire, ttl)
c.InvokeFail(t, "invalid domain name format", "register",
"x", c.CommitteeHash,
"email@frostfs.info", refresh, retry, expire, ttl)
})
c.Invoke(t, true, "register",
"com", c.CommitteeHash,
"email@frostfs.info", refresh, retry, expire, ttl)
c.InvokeFail(t, "TLD already exists", "register",
"com", c.CommitteeHash,
"email@frostfs.info", refresh, retry, expire, ttl)
}
func TestNNSRegister(t *testing.T) {
c := newNNSInvoker(t, false)
accTop := c.NewAccount(t)
refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
c1 := c.WithSigners(c.Committee, accTop)
c1.Invoke(t, true, "register",
"com", accTop.ScriptHash(),
"myemail@frostfs.info", refresh, retry, expire, ttl)
acc := c.NewAccount(t)
c2 := c.WithSigners(c.Committee, acc)
c2.InvokeFail(t, "not witnessed by admin", "register",
"testdomain.com", acc.ScriptHash(),
"myemail@frostfs.info", refresh, retry, expire, ttl)
c3 := c.WithSigners(accTop, acc)
t.Run("domain names with hyphen", func(t *testing.T) {
c3.InvokeFail(t, "invalid domain name format", "register",
"-testdomain.com", acc.ScriptHash(),
"myemail@frostfs.info", refresh, retry, expire, ttl)
c3.InvokeFail(t, "invalid domain name format", "register",
"testdomain-.com", acc.ScriptHash(),
"myemail@frostfs.info", refresh, retry, expire, ttl)
c3.Invoke(t, true, "register",
"test-domain.com", acc.ScriptHash(),
"myemail@frostfs.info", refresh, retry, expire, ttl)
})
c3.Invoke(t, true, "register",
"testdomain.com", acc.ScriptHash(),
"myemail@frostfs.info", refresh, retry, expire, ttl)
b := c.TopBlock(t)
expected := stackitem.NewArray([]stackitem.Item{stackitem.NewBuffer(
[]byte(fmt.Sprintf("testdomain.com myemail@frostfs.info %d %d %d %d %d",
b.Timestamp, refresh, retry, expire, ttl)))})
c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.SOA))
cAcc := c.WithSigners(acc)
cAcc.Invoke(t, stackitem.Null{}, "addRecord",
"testdomain.com", int64(nns.TXT), "first TXT record")
cAcc.InvokeFail(t, "record already exists", "addRecord",
"testdomain.com", int64(nns.TXT), "first TXT record")
cAcc.Invoke(t, stackitem.Null{}, "addRecord",
"testdomain.com", int64(nns.TXT), "second TXT record")
expected = stackitem.NewArray([]stackitem.Item{
stackitem.NewByteArray([]byte("first TXT record")),
stackitem.NewByteArray([]byte("second TXT record")),
})
c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.TXT))
cAcc.Invoke(t, stackitem.Null{}, "setRecord",
"testdomain.com", int64(nns.TXT), int64(0), "replaced first")
expected = stackitem.NewArray([]stackitem.Item{
stackitem.NewByteArray([]byte("replaced first")),
stackitem.NewByteArray([]byte("second TXT record")),
})
c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.TXT))
}
func TestTLDRecord(t *testing.T) {
c := newNNSInvoker(t, true)
c.Invoke(t, stackitem.Null{}, "addRecord",
"com", int64(nns.A), "1.2.3.4")
result := []stackitem.Item{stackitem.NewByteArray([]byte("1.2.3.4"))}
c.Invoke(t, result, "resolve", "com", int64(nns.A))
}
func TestNNSRegisterMulti(t *testing.T) {
c := newNNSInvoker(t, true)
newArgs := func(domain string, account neotest.Signer) []any {
return []any{
domain, account.ScriptHash(), "doesnt@matter.com",
int64(101), int64(102), int64(103), int64(104),
}
}
acc := c.NewAccount(t)
cBoth := c.WithSigners(c.Committee, acc)
args := newArgs("neo.com", acc)
cBoth.Invoke(t, true, "register", args...)
c1 := c.WithSigners(acc)
t.Run("parent domain is missing", func(t *testing.T) {
msg := "one of the parent domains is not registered"
args[0] = "testnet.fs.neo.com"
c1.InvokeFail(t, msg, "register", args...)
})
args[0] = "fs.neo.com"
c1.Invoke(t, true, "register", args...)
args[0] = "testnet.fs.neo.com"
c1.Invoke(t, true, "register", args...)
acc2 := c.NewAccount(t)
c2 := c.WithSigners(c.Committee, acc2)
args = newArgs("mainnet.fs.neo.com", acc2)
c2.InvokeFail(t, "not witnessed by admin", "register", args...)
c1.Invoke(t, stackitem.Null{}, "addRecord",
"something.mainnet.fs.neo.com", int64(nns.A), "1.2.3.4")
c1.Invoke(t, stackitem.Null{}, "addRecord",
"another.fs.neo.com", int64(nns.A), "4.3.2.1")
c2 = c.WithSigners(acc, acc2)
c2.InvokeFail(t, "parent domain has conflicting records: something.mainnet.fs.neo.com",
"register", args...)
c1.Invoke(t, stackitem.Null{}, "deleteRecords",
"something.mainnet.fs.neo.com", int64(nns.A))
c2.Invoke(t, true, "register", args...)
c2 = c.WithSigners(acc2)
c2.Invoke(t, stackitem.Null{}, "addRecord",
"cdn.mainnet.fs.neo.com", int64(nns.A), "166.15.14.13")
result := stackitem.NewArray([]stackitem.Item{
stackitem.NewByteArray([]byte("166.15.14.13")),
})
c2.Invoke(t, result, "resolve", "cdn.mainnet.fs.neo.com", int64(nns.A))
}
func TestNNSUpdateSOA(t *testing.T) {
c := newNNSInvoker(t, true)
refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
c.Invoke(t, true, "register",
"testdomain.com", c.CommitteeHash,
"myemail@frostfs.info", refresh, retry, expire, ttl)
refresh *= 2
retry *= 2
expire *= 2
ttl *= 2
c.Invoke(t, stackitem.Null{}, "updateSOA",
"testdomain.com", "newemail@frostfs.info", refresh, retry, expire, ttl)
b := c.TopBlock(t)
expected := stackitem.NewArray([]stackitem.Item{stackitem.NewBuffer(
[]byte(fmt.Sprintf("testdomain.com newemail@frostfs.info %d %d %d %d %d",
b.Timestamp, refresh, retry, expire, ttl)))})
c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.SOA))
}
func TestNNSGetAllRecords(t *testing.T) {
c := newNNSInvoker(t, true)
refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
c.Invoke(t, true, "register",
"testdomain.com", c.CommitteeHash,
"myemail@frostfs.info", refresh, retry, expire, ttl)
c.Invoke(t, stackitem.Null{}, "addRecord", "testdomain.com", int64(nns.TXT), "first TXT record")
c.Invoke(t, stackitem.Null{}, "addRecord", "testdomain.com", int64(nns.A), "1.2.3.4")
b := c.TopBlock(t)
expSOA := fmt.Sprintf("testdomain.com myemail@frostfs.info %d %d %d %d %d",
b.Timestamp, refresh, retry, expire, ttl)
s, err := c.TestInvoke(t, "getAllRecords", "testdomain.com")
require.NoError(t, err)
iter := s.Pop().Value().(*storage.Iterator)
require.True(t, iter.Next())
require.Equal(t, stackitem.NewStruct([]stackitem.Item{
stackitem.Make("testdomain.com"), stackitem.Make(int64(nns.A)),
stackitem.Make("1.2.3.4"), stackitem.Make(new(big.Int)),
}), iter.Value())
require.True(t, iter.Next())
require.Equal(t, stackitem.NewStruct([]stackitem.Item{
stackitem.Make("testdomain.com"), stackitem.Make(int64(nns.SOA)),
stackitem.NewBuffer([]byte(expSOA)), stackitem.Make(new(big.Int)),
}), iter.Value())
require.True(t, iter.Next())
require.Equal(t, stackitem.NewStruct([]stackitem.Item{
stackitem.Make("testdomain.com"), stackitem.Make(int64(nns.TXT)),
stackitem.Make("first TXT record"), stackitem.Make(new(big.Int)),
}), iter.Value())
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)
refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
c.Invoke(t, true, "register",
"testdomain.com", c.CommitteeHash,
"myemail@frostfs.info", refresh, retry, expire, ttl)
acc := c.NewAccount(t)
cAcc := c.WithSigners(acc)
cAcc.InvokeFail(t, "not witnessed by admin", "addRecord",
"testdomain.com", int64(nns.TXT), "won't be added")
c1 := c.WithSigners(c.Committee, acc)
c1.Invoke(t, stackitem.Null{}, "setAdmin", "testdomain.com", acc.ScriptHash())
cAcc.Invoke(t, stackitem.Null{}, "addRecord",
"testdomain.com", int64(nns.TXT), "will be added")
}
func TestNNSIsAvailable(t *testing.T) {
c := newNNSInvoker(t, false)
c.Invoke(t, true, "isAvailable", "com")
c.InvokeFail(t, "TLD not found", "isAvailable", "domain.com")
refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
c.Invoke(t, true, "register",
"com", c.CommitteeHash,
"myemail@frostfs.info", refresh, retry, expire, ttl)
c.Invoke(t, false, "isAvailable", "com")
c.Invoke(t, true, "isAvailable", "domain.com")
acc := c.NewAccount(t)
c1 := c.WithSigners(c.Committee, acc)
c1.Invoke(t, true, "register",
"domain.com", acc.ScriptHash(),
"myemail@frostfs.info", refresh, retry, expire, ttl)
c.Invoke(t, false, "isAvailable", "domain.com")
c.Invoke(t, true, "isAvailable", "dom.domain.com")
c.InvokeFail(t, "parent does not exist or is expired", "isAvailable", "dom.dom.domain.com")
c1.Invoke(t, true, "register",
"dom.domain.com", acc.ScriptHash(),
"myemail@frostfs.info", refresh, retry, expire, ttl)
c.Invoke(t, false, "isAvailable", "dom.domain.com")
c.Invoke(t, true, "isAvailable", "dom.dom.domain.com")
}
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")
}
func TestNNSResolve(t *testing.T) {
c := newNNSInvoker(t, true)
refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
c.Invoke(t, true, "register",
"test.com", c.CommitteeHash,
"myemail@frostfs.info", refresh, retry, expire, ttl)
c.Invoke(t, stackitem.Null{}, "addRecord",
"test.com", int64(nns.TXT), "expected result")
records := stackitem.NewArray([]stackitem.Item{stackitem.Make("expected result")})
c.Invoke(t, records, "resolve", "test.com", int64(nns.TXT))
c.Invoke(t, records, "resolve", "test.com.", int64(nns.TXT))
c.InvokeFail(t, "invalid domain name format", "resolve", "test.com..", int64(nns.TXT))
}
func TestNNSAndProxy(t *testing.T) {
c := newNNSInvoker(t, false)
proxyHash := deployProxyContract(t, c.Executor)
proxySigner := neotest.NewContractSigner(proxyHash, func(*transaction.Transaction) []any { return nil })
g := c.NewInvoker(gas.Hash, c.Validator)
g.Invoke(t, true, "transfer",
c.Validator.ScriptHash(), proxyHash, 100_0000_0000, nil)
cc := c.WithSigners(proxySigner, c.Committee)
cc.Invoke(t, true, "register", "ns", proxyHash,
"ops@frostfs.info", 100, 100, 100, 100)
checkBalance := func(t *testing.T, owner util.Uint160, balance int64) {
s, err := cc.TestInvoke(t, "balanceOf", owner)
require.NoError(t, err)
require.Equal(t, 1, s.Len())
require.Equal(t, int64(balance), s.Pop().BigInt().Int64())
}
checkBalance(t, proxyHash, 1)
checkBalance(t, c.CommitteeHash, 0)
t.Run("ensure domain is not lost", func(t *testing.T) {
cc.Invoke(t, true, "transfer", c.CommitteeHash, "ns", nil)
checkBalance(t, proxyHash, 0)
checkBalance(t, c.CommitteeHash, 1)
})
}