Integrate frostfsid into NNS #119
8 changed files with 176 additions and 3 deletions
|
@ -96,6 +96,7 @@ const (
|
||||||
createSubjectMethod = "createSubject"
|
createSubjectMethod = "createSubject"
|
||||||
getSubjectMethod = "getSubject"
|
getSubjectMethod = "getSubject"
|
||||||
getSubjectExtendedMethod = "getSubjectExtended"
|
getSubjectExtendedMethod = "getSubjectExtended"
|
||||||
|
getSubjectKVMethod = "getSubjectKV"
|
||||||
listSubjectsMethod = "listSubjects"
|
listSubjectsMethod = "listSubjects"
|
||||||
addSubjectKeyMethod = "addSubjectKey"
|
addSubjectKeyMethod = "addSubjectKey"
|
||||||
removeSubjectKeyMethod = "removeSubjectKey"
|
removeSubjectKeyMethod = "removeSubjectKey"
|
||||||
|
@ -266,6 +267,11 @@ func (c Client) GetSubjectExtended(addr util.Uint160) (*SubjectExtended, error)
|
||||||
return ParseSubjectExtended(items)
|
return ParseSubjectExtended(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSubjectKV invokes `getSubjectKV` method of contract.
|
||||||
|
func (c Client) GetSubjectKV(addr util.Uint160, name string) (string, error) {
|
||||||
|
return unwrap.UTF8String(c.act.Call(c.contract, getSubjectKVMethod, addr, name))
|
||||||
|
}
|
||||||
|
|
||||||
// ListSubjects gets all subjects.
|
// ListSubjects gets all subjects.
|
||||||
func (c Client) ListSubjects() ([]util.Uint160, error) {
|
func (c Client) ListSubjects() ([]util.Uint160, error) {
|
||||||
return UnwrapArrayOfUint160(commonclient.ReadIteratorItems(c.act, iteratorBatchSize, c.contract, listSubjectsMethod))
|
return UnwrapArrayOfUint160(commonclient.ReadIteratorItems(c.act, iteratorBatchSize, c.contract, listSubjectsMethod))
|
||||||
|
|
|
@ -7,6 +7,7 @@ safemethods:
|
||||||
- "getGroupByName"
|
- "getGroupByName"
|
||||||
- "getNamespace"
|
- "getNamespace"
|
||||||
- "getNamespaceExtended"
|
- "getNamespaceExtended"
|
||||||
|
- "getSubjectKV"
|
||||||
- "getSubject"
|
- "getSubject"
|
||||||
- "getSubjectExtended"
|
- "getSubjectExtended"
|
||||||
- "getSubjectByKey"
|
- "getSubjectByKey"
|
||||||
|
|
|
@ -472,6 +472,34 @@ func GetSubjectKeyByName(ns, name string) interop.PublicKey {
|
||||||
return subjKey
|
return subjKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSubjectKV GetSubjectKey returns the value associated with the key for the subject.
|
||||||
|
func GetSubjectKV(addr interop.Hash160, name string) string {
|
||||||
|
if len(addr) != interop.Hash160Len {
|
||||||
|
panic("incorrect address length")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := storage.GetReadOnlyContext()
|
||||||
|
sKey := subjectKeyFromAddr(addr)
|
||||||
|
data := storage.Get(ctx, sKey).([]byte)
|
||||||
|
if data == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
sbj := std.Deserialize(data).(Subject)
|
||||||
|
|
||||||
|
if sbj.KV == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range sbj.KV {
|
||||||
|
if k == name {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func ListSubjects() iterator.Iterator {
|
func ListSubjects() iterator.Iterator {
|
||||||
ctx := storage.GetReadOnlyContext()
|
ctx := storage.GetReadOnlyContext()
|
||||||
return storage.Find(ctx, []byte{subjectKeysPrefix}, storage.KeysOnly|storage.RemovePrefix)
|
return storage.Find(ctx, []byte{subjectKeysPrefix}, storage.KeysOnly|storage.RemovePrefix)
|
||||||
|
|
|
@ -44,3 +44,32 @@ The committee makes new tokens (domains), sets, and charges a fee for issuance.
|
||||||
## Globally Unique Domain Zone
|
## Globally Unique Domain Zone
|
||||||
|
|
||||||
For more information, see [here](../docs/globally-unique-domain-zone.md).
|
For more information, see [here](../docs/globally-unique-domain-zone.md).
|
||||||
|
|
||||||
|
## NNS and Frostfsid
|
||||||
|
|
||||||
|
You can register a TLD domain without a committee signature using Frostfsid. To do this, create a new wallet
|
||||||
|
```
|
||||||
|
neo-go wallet init -w newwallet/wallet.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Get wallet address:
|
||||||
|
```
|
||||||
|
neo-go wallet dump-keys -w newwallet/wallet.json
|
||||||
|
[subject-address]
|
||||||
|
[subject-key]
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a subject in `FrostfsID`:
|
||||||
|
```
|
||||||
|
frostfs-adm morph frostfsid create-subject --subject-key="[subject-key]"
|
||||||
|
```
|
||||||
|
|
||||||
|
Grant permissions to the wallet:
|
||||||
|
```
|
||||||
|
frostfs-adm morph nns give-privilege --subject-address="[subject-address]"
|
||||||
|
```
|
||||||
|
|
||||||
|
Register domain:
|
||||||
|
```
|
||||||
|
neo-go contract invokefunction [NNS-hash] register "subdomain.domain" hash160:[subject-address] "email@frostfs.info" 10000 1000 1000 1000 -- [subject-address]:Global
|
||||||
|
```
|
39
nns/frostfsid.go
Normal file
39
nns/frostfsid.go
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
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/native/management"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/interop/native/std"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
const FrostfsIDNNSName = "frostfsid.frostfs"
|
||||||
|
|
||||||
|
const (
|
||||||
|
FrostfsIDNNSTLDPermissionKey = "nns-allow-register-tld"
|
||||||
|
FrostfsIDTLDRegistrationAllowed = "allow"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkFrostfsID(ctx storage.Context, addr interop.Hash160) bool {
|
||||||
|
if len(addr) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
frostfsIDAddress := getRecordsByType(ctx, []byte(tokenIDFromName(FrostfsIDNNSName)), FrostfsIDNNSName, TXT)
|
||||||
|
if len(frostfsIDAddress) < 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedBytes := std.Base58Decode([]byte(frostfsIDAddress[1]))
|
||||||
|
|
||||||
|
if len(decodedBytes) < 21 || management.GetContract(decodedBytes[1:21]) == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if res := contract.Call(decodedBytes[1:21], "getSubjectKV", contract.ReadOnly, addr, FrostfsIDNNSTLDPermissionKey).(string); res == FrostfsIDTLDRegistrationAllowed {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
|
@ -344,7 +344,7 @@ func register(ctx storage.Context, name string, owner interop.Hash160, email str
|
||||||
|
|
||||||
tldBytes := storage.Get(ctx, tldKey)
|
tldBytes := storage.Get(ctx, tldKey)
|
||||||
if countZone == 1 {
|
if countZone == 1 {
|
||||||
checkCommittee()
|
checkCommitteeAndFrostfsID(ctx, owner)
|
||||||
if tldBytes != nil {
|
if tldBytes != nil {
|
||||||
panic("TLD already exists")
|
panic("TLD already exists")
|
||||||
}
|
}
|
||||||
|
@ -924,6 +924,14 @@ func isValid(address interop.Hash160) bool {
|
||||||
return address != nil && len(address) == interop.Hash160Len
|
return address != nil && len(address) == interop.Hash160Len
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkCommitteeAndFrostfsID panics if the script container is not signed by the committee.
|
||||||
|
// or if the owner does not have permission in FrostfsID.
|
||||||
|
func checkCommitteeAndFrostfsID(ctx storage.Context, owner interop.Hash160) {
|
||||||
|
if !checkFrostfsID(ctx, owner) {
|
||||||
|
checkCommittee()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// checkCommittee panics if the script container is not signed by the committee.
|
// checkCommittee panics if the script container is not signed by the committee.
|
||||||
func checkCommittee() {
|
func checkCommittee() {
|
||||||
committee := neo.GetCommittee()
|
committee := neo.GetCommittee()
|
||||||
|
|
|
@ -222,6 +222,11 @@ func (c *ContractReader) GetSubjectExtended(addr util.Uint160) ([]stackitem.Item
|
||||||
return unwrap.Array(c.invoker.Call(c.hash, "getSubjectExtended", addr))
|
return unwrap.Array(c.invoker.Call(c.hash, "getSubjectExtended", addr))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSubjectKV invokes `getSubjectKV` method of contract.
|
||||||
|
func (c *ContractReader) GetSubjectKV(addr util.Uint160, name string) (string, error) {
|
||||||
|
return unwrap.UTF8String(c.invoker.Call(c.hash, "getSubjectKV", addr, name))
|
||||||
|
}
|
||||||
|
|
||||||
// GetSubjectKeyByName invokes `getSubjectKeyByName` method of contract.
|
// GetSubjectKeyByName invokes `getSubjectKeyByName` method of contract.
|
||||||
func (c *ContractReader) GetSubjectKeyByName(ns string, name string) (*keys.PublicKey, error) {
|
func (c *ContractReader) GetSubjectKeyByName(ns string, name string) (*keys.PublicKey, error) {
|
||||||
return unwrap.PublicKey(c.invoker.Call(c.hash, "getSubjectKeyByName", ns, name))
|
return unwrap.PublicKey(c.invoker.Call(c.hash, "getSubjectKeyByName", ns, name))
|
||||||
|
|
|
@ -12,10 +12,12 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/storage"
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/storage"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/neotest"
|
"github.com/nspcc-dev/neo-go/pkg/neotest"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/gas"
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/gas"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -23,8 +25,7 @@ const nnsPath = "../nns"
|
||||||
|
|
||||||
const msPerYear = 365 * 24 * time.Hour / time.Millisecond
|
const msPerYear = 365 * 24 * time.Hour / time.Millisecond
|
||||||
|
|
||||||
func newNNSInvoker(t *testing.T, addRoot bool) *neotest.ContractInvoker {
|
func deployNNS(t *testing.T, e *neotest.Executor, addRoot bool) *neotest.ContractInvoker {
|
||||||
e := newExecutor(t)
|
|
||||||
ctr := neotest.CompileFile(t, e.CommitteeHash, nnsPath, path.Join(nnsPath, "config.yml"))
|
ctr := neotest.CompileFile(t, e.CommitteeHash, nnsPath, path.Join(nnsPath, "config.yml"))
|
||||||
e.DeployContract(t, ctr, nil)
|
e.DeployContract(t, ctr, nil)
|
||||||
|
|
||||||
|
@ -39,6 +40,28 @@ func newNNSInvoker(t *testing.T, addRoot bool) *neotest.ContractInvoker {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newNNSInvoker(t *testing.T, addRoot bool) *neotest.ContractInvoker {
|
||||||
|
e := newExecutor(t)
|
||||||
|
return deployNNS(t, e, addRoot)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNNSInvokerWithFrostfsID(t *testing.T, addRoot bool) (*neotest.ContractInvoker, *neotest.ContractInvoker) {
|
||||||
|
e := newExecutor(t)
|
||||||
|
c := deployNNS(t, e, addRoot)
|
||||||
|
|
||||||
|
frostfdID := deployFrostFSIDContract(t, e, e.CommitteeHash)
|
||||||
|
c.Invoke(t, true, "register",
|
||||||
|
nns.FrostfsIDNNSName, c.CommitteeHash,
|
||||||
|
"myemail@frostfs.info", defaultRefresh, defaultRetry, defaultExpire, defaultTTL)
|
||||||
|
|
||||||
|
c.Invoke(t, stackitem.Null{}, "addRecord",
|
||||||
|
nns.FrostfsIDNNSName, int64(nns.TXT), frostfdID.StringLE())
|
||||||
|
|
||||||
|
c.Invoke(t, stackitem.Null{}, "addRecord",
|
||||||
|
nns.FrostfsIDNNSName, int64(nns.TXT), address.Uint160ToString(frostfdID))
|
||||||
|
return e.NewInvoker(c.Hash), e.CommitteeInvoker(frostfdID)
|
||||||
|
}
|
||||||
|
|
||||||
func TestNNSGeneric(t *testing.T) {
|
func TestNNSGeneric(t *testing.T) {
|
||||||
c := newNNSInvoker(t, false)
|
c := newNNSInvoker(t, false)
|
||||||
|
|
||||||
|
@ -527,6 +550,40 @@ func TestNNSSetAdmin(t *testing.T) {
|
||||||
"testdomain.com", int64(nns.TXT), "will be added")
|
"testdomain.com", int64(nns.TXT), "will be added")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNNS_Frostfsid(t *testing.T) {
|
||||||
|
nnsInv, f := newNNSInvokerWithFrostfsID(t, false)
|
||||||
|
|
||||||
|
acc, err := wallet.NewAccount()
|
||||||
|
require.NoError(t, err)
|
||||||
|
nnsUserInv := nnsInv.NewInvoker(nnsInv.Hash, newSigner(t, nnsInv, acc))
|
||||||
|
|
||||||
|
nnsUserInv.InvokeFail(t, "not witnessed by committee", "register",
|
||||||
|
"ddd", acc.ScriptHash(),
|
||||||
|
"myemail@frostfs.info", defaultRefresh, defaultRetry, defaultExpire, defaultTTL)
|
||||||
|
|
||||||
|
nnsUserInv.InvokeFail(t, "not witnessed by committee", "register",
|
||||||
|
"testdomain.kz", acc.ScriptHash(),
|
||||||
|
"myemail@frostfs.info", defaultRefresh, defaultRetry, defaultExpire, defaultTTL)
|
||||||
|
|
||||||
|
f.Invoke(t, stackitem.Null{}, createSubjectMethod, "", acc.PrivateKey().PublicKey().Bytes())
|
||||||
|
|
||||||
|
nnsUserInv.InvokeFail(t, "not witnessed by committee", "register",
|
||||||
|
"testdomain.kz", acc.ScriptHash(),
|
||||||
|
"myemail@frostfs.info", defaultRefresh, defaultRetry, defaultExpire, defaultTTL)
|
||||||
|
|
||||||
|
f.Invoke(t, stackitem.Null{}, setSubjectKVMethod, acc.ScriptHash(), nns.FrostfsIDNNSTLDPermissionKey, nns.FrostfsIDTLDRegistrationAllowed)
|
||||||
|
|
||||||
|
nnsUserInv.Invoke(t, true, "register",
|
||||||
|
"testdomain.kz", acc.ScriptHash(),
|
||||||
|
"myemail@frostfs.info", defaultRefresh, defaultRetry, defaultExpire, defaultTTL)
|
||||||
|
|
||||||
|
f.Invoke(t, stackitem.Null{}, deleteSubjectKVMethod, acc.ScriptHash(), nns.FrostfsIDNNSTLDPermissionKey)
|
||||||
|
|
||||||
|
nnsUserInv.InvokeFail(t, "not witnessed by committee", "register",
|
||||||
|
"testdomain.uz", acc.ScriptHash(),
|
||||||
|
"myemail@frostfs.info", defaultRefresh, defaultRetry, defaultExpire, defaultTTL)
|
||||||
|
}
|
||||||
|
|
||||||
func TestNNSIsAvailable(t *testing.T) {
|
func TestNNSIsAvailable(t *testing.T) {
|
||||||
c := newNNSInvoker(t, false)
|
c := newNNSInvoker(t, false)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue