1080 lines
28 KiB
Go
1080 lines
28 KiB
Go
package frostfsid
|
|
|
|
import (
|
|
"git.frostfs.info/TrueCloudLab/frostfs-contract/common"
|
|
"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/convert"
|
|
"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/std"
|
|
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
|
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
|
|
)
|
|
|
|
type (
|
|
// Subject represents a subject entity.
|
|
//
|
|
// Fields:
|
|
// - Namespace: a string representing the namespace of the subject.
|
|
// - Name: a string representing the name of the subject.
|
|
// The name must match the following regex pattern: ^[\w+=,.@-]{1,64}$
|
|
// The Subject is stored in the storage as a hash(namespace) + hash(name).
|
|
Subject struct {
|
|
PrimaryKey interop.PublicKey
|
|
AdditionalKeys []interop.PublicKey
|
|
Namespace string
|
|
Name string
|
|
KV map[string]string
|
|
}
|
|
|
|
SubjectExtended struct {
|
|
PrimaryKey interop.PublicKey
|
|
AdditionalKeys []interop.PublicKey
|
|
Namespace string
|
|
Name string
|
|
KV map[string]string
|
|
Groups []Group
|
|
}
|
|
|
|
// Namespace represents a namespace.
|
|
//
|
|
// Fields:
|
|
// - Name: a string representing the name of the namespace.
|
|
// The custom name must match the following regex pattern:
|
|
// (^$)|(^[a-z0-9]{1,2}$)|(^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$)
|
|
// An empty Name is considered the <root> namespace.
|
|
//
|
|
// The Namespace is stored in the storage as a hash(name).
|
|
Namespace struct {
|
|
Name string
|
|
}
|
|
|
|
NamespaceExtended struct {
|
|
Name string
|
|
GroupsCount int
|
|
SubjectsCount int
|
|
}
|
|
|
|
// Group represents a group entity.
|
|
//
|
|
// Fields:
|
|
// - ID: an integer representing the unique identifier of the group.
|
|
// The ID is generated upon creation and is the sequential number of the group within the namespace.
|
|
// It cannot be changed once set.
|
|
// - Name: a string representing the name of the group.
|
|
// The name must match the following regex pattern: [\w+=,.@-]{1,128}$
|
|
// - Namespace: a string representing the namespace of the group.
|
|
// A group exists only within one namespace.
|
|
//
|
|
// The group is stored in the storage as a hash(namespace) + hash(name).
|
|
Group struct {
|
|
ID int
|
|
Name string
|
|
Namespace string
|
|
KV map[string]string
|
|
}
|
|
|
|
GroupExtended struct {
|
|
ID int
|
|
Name string
|
|
Namespace string
|
|
KV map[string]string
|
|
SubjectsCount int
|
|
}
|
|
)
|
|
|
|
const (
|
|
adminKey = 'o'
|
|
subjectKeysPrefix = 's'
|
|
additionalKeysPrefix = 'a'
|
|
namespaceKeysPrefix = 'n'
|
|
namespaceSubjectsKeysPrefix = 'N'
|
|
namespaceSubjectsNamesPrefix = 'l'
|
|
groupKeysPrefix = 'g'
|
|
groupSubjectsKeysPrefix = 'G'
|
|
groupCounterKey = 'c'
|
|
namespaceGroupsNamesPrefix = 'm'
|
|
addressPrefix = 'A'
|
|
)
|
|
|
|
func _deploy(data any, isUpdate bool) {
|
|
ctx := storage.GetContext()
|
|
|
|
args := data.(struct {
|
|
admin interop.Hash160
|
|
})
|
|
|
|
if args.admin != nil {
|
|
if len(args.admin) != interop.Hash160Len {
|
|
panic("incorrect length of owner address")
|
|
}
|
|
storage.Put(ctx, adminKey, args.admin)
|
|
}
|
|
|
|
if isUpdate {
|
|
it := storage.Find(ctx, subjectKeysPrefix, storage.ValuesOnly)
|
|
for iterator.Next(it) {
|
|
subjectRaw := iterator.Value(it)
|
|
subject := std.Deserialize(subjectRaw.([]byte)).(Subject)
|
|
address := addressKey(contract.CreateStandardAccount(subject.PrimaryKey))
|
|
if storage.Get(ctx, address) != nil {
|
|
panic("frostfsid contract contains duplicate keys")
|
|
}
|
|
storage.Put(ctx, address, true)
|
|
|
|
for i := 0; i < len(subject.AdditionalKeys); i++ {
|
|
address = addressKey(contract.CreateStandardAccount(subject.AdditionalKeys[i]))
|
|
if storage.Get(ctx, address) != nil {
|
|
panic("frostfsid contract contains duplicate keys")
|
|
}
|
|
storage.Put(ctx, address, true)
|
|
}
|
|
}
|
|
}
|
|
|
|
storage.Put(ctx, groupCounterKey, 0)
|
|
storage.Put(ctx, namespaceKey(""), std.Serialize(Namespace{}))
|
|
|
|
runtime.Log("frostfsid contract initialized")
|
|
}
|
|
|
|
// SetAdmin sets the admin address for the contract.
|
|
func SetAdmin(addr interop.Hash160) {
|
|
ctx := storage.GetContext()
|
|
if !common.HasUpdateAccess() {
|
|
panic("not witnessed")
|
|
}
|
|
|
|
storage.Put(ctx, adminKey, addr)
|
|
}
|
|
|
|
// ClearAdmin removes the admin address from the contract storage.
|
|
func ClearAdmin() {
|
|
ctx := storage.GetContext()
|
|
if !common.HasUpdateAccess() {
|
|
panic("not witnessed")
|
|
}
|
|
|
|
storage.Delete(ctx, adminKey)
|
|
}
|
|
|
|
// GetAdmin retrieves the admin address from the contract storage.
|
|
func GetAdmin() interop.Hash160 {
|
|
ctx := storage.GetReadOnlyContext()
|
|
return storage.Get(ctx, adminKey).(interop.Hash160)
|
|
}
|
|
|
|
// Update method updates contract source code and manifest. It can be invoked
|
|
// only by committee.
|
|
func Update(script []byte, manifest []byte, data any) {
|
|
if !common.HasUpdateAccess() {
|
|
panic("only committee can update contract")
|
|
}
|
|
|
|
management.UpdateWithData(script, manifest, common.AppendVersion(data))
|
|
runtime.Log("frostfsid contract updated")
|
|
}
|
|
|
|
// Version returns the version of the contract.
|
|
func Version() int {
|
|
return common.Version
|
|
}
|
|
|
|
// CreateSubject creates a new subject in the specified namespace with the provided public key.
|
|
func CreateSubject(ns string, key interop.PublicKey) {
|
|
ctx := storage.GetContext()
|
|
checkContractOwner(ctx)
|
|
|
|
if len(key) != interop.PublicKeyCompressedLen {
|
|
panic("incorrect public key length")
|
|
}
|
|
|
|
addr := contract.CreateStandardAccount(key)
|
|
sKey := subjectKeyFromAddr(addr)
|
|
data := storage.Get(ctx, sKey).([]byte)
|
|
if data != nil {
|
|
panic("subject already exists")
|
|
}
|
|
|
|
saPrefix := subjectAdditionalPrefix(key)
|
|
it := storage.Find(ctx, saPrefix, storage.KeysOnly)
|
|
for iterator.Next(it) {
|
|
panic("key is occupied")
|
|
}
|
|
|
|
allAddressKey := addressKey(addr)
|
|
if storage.Get(ctx, allAddressKey) != nil {
|
|
panic("key is occupied by another additional key")
|
|
}
|
|
|
|
nsKey := namespaceKey(ns)
|
|
data = storage.Get(ctx, nsKey).([]byte)
|
|
if data == nil {
|
|
panic("namespace not found")
|
|
}
|
|
|
|
subj := Subject{
|
|
PrimaryKey: key,
|
|
Namespace: ns,
|
|
}
|
|
|
|
storage.Put(ctx, sKey, std.Serialize(subj))
|
|
|
|
nsSubjKey := namespaceSubjectKey(ns, addr)
|
|
storage.Put(ctx, nsSubjKey, []byte{1})
|
|
storage.Put(ctx, allAddressKey, true)
|
|
|
|
runtime.Notify("CreateSubject", interop.Hash160(addr))
|
|
}
|
|
|
|
// AddSubjectKey adds an additional public key to a subject with the specified address.
|
|
func AddSubjectKey(addr interop.Hash160, key interop.PublicKey) {
|
|
ctx := storage.GetContext()
|
|
checkContractOwner(ctx)
|
|
|
|
if len(addr) != interop.Hash160Len {
|
|
panic("incorrect address length")
|
|
}
|
|
if len(key) != interop.PublicKeyCompressedLen {
|
|
panic("incorrect public key length")
|
|
}
|
|
|
|
addressKey := addressKey(contract.CreateStandardAccount(key))
|
|
if storage.Get(ctx, addressKey) != nil {
|
|
panic("key is occupied")
|
|
}
|
|
|
|
saKey := subjectAdditionalKey(key, addr)
|
|
data := storage.Get(ctx, saKey).([]byte)
|
|
if data != nil {
|
|
panic("key already added")
|
|
}
|
|
|
|
storage.Put(ctx, saKey, []byte{1})
|
|
|
|
sKey := subjectKeyFromAddr(addr)
|
|
data = storage.Get(ctx, sKey).([]byte)
|
|
if data == nil {
|
|
panic("address not found")
|
|
}
|
|
subject := std.Deserialize(data).(Subject)
|
|
subject.AdditionalKeys = append(subject.AdditionalKeys, key)
|
|
|
|
storage.Put(ctx, sKey, std.Serialize(subject))
|
|
storage.Put(ctx, addressKey, true)
|
|
runtime.Notify("AddSubjectKey", addr, key)
|
|
}
|
|
|
|
// RemoveSubjectKey removes an additional public key from the subject with the specified address.
|
|
func RemoveSubjectKey(addr interop.Hash160, key interop.PublicKey) {
|
|
ctx := storage.GetContext()
|
|
checkContractOwner(ctx)
|
|
|
|
if len(addr) != interop.Hash160Len {
|
|
panic("incorrect address length")
|
|
}
|
|
if len(key) != interop.PublicKeyCompressedLen {
|
|
panic("incorrect public key length")
|
|
}
|
|
|
|
saKey := subjectAdditionalKey(key, addr)
|
|
data := storage.Get(ctx, saKey).([]byte)
|
|
if data == nil {
|
|
panic("key already removed")
|
|
}
|
|
|
|
storage.Delete(ctx, saKey)
|
|
|
|
sKey := subjectKeyFromAddr(addr)
|
|
data = storage.Get(ctx, sKey).([]byte)
|
|
if data == nil {
|
|
panic("address not found")
|
|
}
|
|
subject := std.Deserialize(data).(Subject)
|
|
|
|
var additionalKeys []interop.PublicKey
|
|
for i := 0; i < len(subject.AdditionalKeys); i++ {
|
|
if !common.BytesEqual(subject.AdditionalKeys[i], key) {
|
|
additionalKeys = append(additionalKeys, subject.AdditionalKeys[i])
|
|
}
|
|
}
|
|
subject.AdditionalKeys = additionalKeys
|
|
|
|
storage.Put(ctx, sKey, std.Serialize(subject))
|
|
storage.Delete(ctx, addressKey(contract.CreateStandardAccount(key)))
|
|
runtime.Notify("RemoveSubjectKey", addr, key)
|
|
}
|
|
|
|
// SetSubjectName sets a new name for the subject with the specified address.
|
|
func SetSubjectName(addr interop.Hash160, name string) {
|
|
ctx := storage.GetContext()
|
|
checkContractOwner(ctx)
|
|
|
|
if len(addr) != interop.Hash160Len {
|
|
panic("incorrect address length")
|
|
}
|
|
|
|
sKey := subjectKeyFromAddr(addr)
|
|
data := storage.Get(ctx, sKey).([]byte)
|
|
if data == nil {
|
|
panic("subject not found")
|
|
}
|
|
|
|
subject := std.Deserialize(data).(Subject)
|
|
oldName := subject.Name
|
|
subject.Name = name
|
|
storage.Put(ctx, sKey, std.Serialize(subject))
|
|
|
|
updateNamespaceSubjectName(ctx, subject, oldName)
|
|
|
|
runtime.Notify("SetSubjectName", addr, name)
|
|
}
|
|
|
|
// SetSubjectKV sets a key-value pair for the subject with the specified address.
|
|
func SetSubjectKV(addr interop.Hash160, key, val string) {
|
|
ctx := storage.GetContext()
|
|
checkContractOwner(ctx)
|
|
|
|
if len(addr) != interop.Hash160Len {
|
|
panic("incorrect address length")
|
|
}
|
|
|
|
sKey := subjectKeyFromAddr(addr)
|
|
data := storage.Get(ctx, sKey).([]byte)
|
|
if data == nil {
|
|
panic("subject not found")
|
|
}
|
|
|
|
subject := std.Deserialize(data).(Subject)
|
|
if subject.KV == nil {
|
|
subject.KV = map[string]string{}
|
|
}
|
|
subject.KV[key] = val
|
|
|
|
storage.Put(ctx, sKey, std.Serialize(subject))
|
|
runtime.Notify("SetSubjectKV", addr, key, val)
|
|
}
|
|
|
|
// DeleteSubjectKV deletes a key-value pair from the subject with the specified address.
|
|
func DeleteSubjectKV(addr interop.Hash160, key string) {
|
|
ctx := storage.GetContext()
|
|
checkContractOwner(ctx)
|
|
|
|
if len(addr) != interop.Hash160Len {
|
|
panic("incorrect address length")
|
|
}
|
|
|
|
sKey := subjectKeyFromAddr(addr)
|
|
data := storage.Get(ctx, sKey).([]byte)
|
|
if data == nil {
|
|
panic("subject not found")
|
|
}
|
|
|
|
subject := std.Deserialize(data).(Subject)
|
|
delete(subject.KV, key)
|
|
|
|
storage.Put(ctx, sKey, std.Serialize(subject))
|
|
runtime.Notify("DeleteSubjectKV", addr, key)
|
|
}
|
|
|
|
// DeleteSubject deletes the subject with the specified address.
|
|
func DeleteSubject(addr interop.Hash160) {
|
|
ctx := storage.GetContext()
|
|
checkContractOwner(ctx)
|
|
|
|
if len(addr) != interop.Hash160Len {
|
|
panic("incorrect address length")
|
|
}
|
|
|
|
sKey := subjectKeyFromAddr(addr)
|
|
data := storage.Get(ctx, sKey).([]byte)
|
|
if data == nil {
|
|
return
|
|
}
|
|
subj := std.Deserialize(data).(Subject)
|
|
|
|
for i := 0; i < len(subj.AdditionalKeys); i++ {
|
|
storage.Delete(ctx, subjectAdditionalKey(subj.AdditionalKeys[i], addr))
|
|
storage.Delete(ctx, addressKey(contract.CreateStandardAccount(subj.AdditionalKeys[i])))
|
|
}
|
|
storage.Delete(ctx, sKey)
|
|
|
|
removeSubjectFromNamespace(ctx, subj.Namespace, addr)
|
|
deleteNamespaceSubjectName(ctx, subj.Namespace, subj.Name)
|
|
|
|
runtime.Notify("DeleteSubject", addr)
|
|
}
|
|
|
|
// GetSubject retrieves the subject with the specified address.
|
|
func GetSubject(addr interop.Hash160) Subject {
|
|
if len(addr) != interop.Hash160Len {
|
|
panic("incorrect address length")
|
|
}
|
|
|
|
ctx := storage.GetReadOnlyContext()
|
|
sKey := subjectKeyFromAddr(addr)
|
|
data := storage.Get(ctx, sKey).([]byte)
|
|
if data == nil {
|
|
a := getPrimaryAddr(ctx, addr)
|
|
sKey = subjectKeyFromAddr(a)
|
|
data = storage.Get(ctx, sKey).([]byte)
|
|
if data == nil {
|
|
panic("subject not found")
|
|
}
|
|
}
|
|
|
|
return std.Deserialize(data).(Subject)
|
|
}
|
|
|
|
// GetSubjectExtended retrieves the extended information of the subject with the specified address.
|
|
func GetSubjectExtended(addr interop.Hash160) SubjectExtended {
|
|
subj := GetSubject(addr)
|
|
ctx := storage.GetReadOnlyContext()
|
|
|
|
var groups []Group
|
|
nsHash := ripemd160Hash(subj.Namespace)
|
|
it := storage.Find(ctx, groupPrefixFromHash(nsHash), storage.KeysOnly|storage.RemovePrefix)
|
|
for iterator.Next(it) {
|
|
groupHash := iterator.Value(it).([]byte)
|
|
data := storage.Get(ctx, groupSubjectKeyFromHashes(nsHash, groupHash, addr)).([]byte)
|
|
if data != nil {
|
|
data = storage.Get(ctx, groupKeyFromHashes(nsHash, groupHash)).([]byte)
|
|
if data == nil {
|
|
panic("group not found")
|
|
}
|
|
groups = append(groups, std.Deserialize(data).(Group))
|
|
}
|
|
}
|
|
|
|
subjExt := SubjectExtended{
|
|
PrimaryKey: subj.PrimaryKey,
|
|
AdditionalKeys: subj.AdditionalKeys,
|
|
Namespace: subj.Namespace,
|
|
Name: subj.Name,
|
|
KV: subj.KV,
|
|
Groups: groups,
|
|
}
|
|
|
|
return subjExt
|
|
}
|
|
|
|
// GetSubjectByKey retrieves the subject associated with the provided public key.
|
|
func GetSubjectByKey(key interop.PublicKey) Subject {
|
|
if len(key) != interop.PublicKeyCompressedLen {
|
|
panic("incorrect key length")
|
|
}
|
|
|
|
ctx := storage.GetReadOnlyContext()
|
|
|
|
sKey := subjectKey(key)
|
|
data := storage.Get(ctx, sKey).([]byte)
|
|
if data != nil {
|
|
return std.Deserialize(data).(Subject)
|
|
}
|
|
|
|
addr := getPrimaryAddr(ctx, contract.CreateStandardAccount(key))
|
|
sKey = subjectKeyFromAddr(addr)
|
|
data = storage.Get(ctx, sKey).([]byte)
|
|
if data != nil {
|
|
return std.Deserialize(data).(Subject)
|
|
}
|
|
|
|
panic("subject not found")
|
|
}
|
|
|
|
func getPrimaryAddr(ctx storage.Context, addr interop.Hash160) interop.Hash160 {
|
|
saPrefix := append([]byte{additionalKeysPrefix}, addr...)
|
|
it := storage.Find(ctx, saPrefix, storage.KeysOnly|storage.RemovePrefix)
|
|
if iterator.Next(it) {
|
|
return iterator.Value(it).([]byte)
|
|
}
|
|
panic("subject not found")
|
|
}
|
|
|
|
// GetSubjectByName retrieves the subject with the specified name within the given namespace.
|
|
func GetSubjectByName(ns, name string) Subject {
|
|
key := GetSubjectKeyByName(ns, name)
|
|
addr := contract.CreateStandardAccount(key)
|
|
return GetSubject(addr)
|
|
}
|
|
|
|
// GetSubjectKeyByName retrieves the public key of the subject with the specified namespace and name.
|
|
func GetSubjectKeyByName(ns, name string) interop.PublicKey {
|
|
if name == "" {
|
|
panic("invalid or name")
|
|
}
|
|
|
|
ctx := storage.GetReadOnlyContext()
|
|
|
|
nsSubjNameKey := namespaceSubjectNameKey(ns, name)
|
|
subjKey := storage.Get(ctx, nsSubjNameKey).(interop.PublicKey)
|
|
if subjKey == nil {
|
|
panic("subject not found")
|
|
}
|
|
|
|
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 {
|
|
ctx := storage.GetReadOnlyContext()
|
|
return storage.Find(ctx, []byte{subjectKeysPrefix}, storage.KeysOnly|storage.RemovePrefix)
|
|
}
|
|
|
|
// CreateNamespace creates a new namespace with the specified name.
|
|
func CreateNamespace(ns string) {
|
|
ctx := storage.GetContext()
|
|
checkContractOwner(ctx)
|
|
|
|
nsKey := namespaceKey(ns)
|
|
data := storage.Get(ctx, nsKey).([]byte)
|
|
if data != nil {
|
|
panic("namespace already exists")
|
|
}
|
|
|
|
namespace := Namespace{
|
|
Name: ns,
|
|
}
|
|
|
|
storage.Put(ctx, nsKey, std.Serialize(namespace))
|
|
runtime.Notify("CreateNamespace", ns)
|
|
}
|
|
|
|
// GetNamespace retrieves the namespace with the specified name.
|
|
func GetNamespace(ns string) Namespace {
|
|
ctx := storage.GetReadOnlyContext()
|
|
nsKey := namespaceKey(ns)
|
|
data := storage.Get(ctx, nsKey).([]byte)
|
|
if data == nil {
|
|
panic("namespace not found")
|
|
}
|
|
|
|
return std.Deserialize(data).(Namespace)
|
|
}
|
|
|
|
// GetNamespaceExtended retrieves extended information about the namespace.
|
|
func GetNamespaceExtended(ns string) NamespaceExtended {
|
|
ctx := storage.GetReadOnlyContext()
|
|
nsKey := namespaceKey(ns)
|
|
data := storage.Get(ctx, nsKey).([]byte)
|
|
if data == nil {
|
|
panic("namespace not found")
|
|
}
|
|
namespace := std.Deserialize(data).(Namespace)
|
|
|
|
nsExtended := NamespaceExtended{
|
|
Name: namespace.Name,
|
|
}
|
|
|
|
it := storage.Find(ctx, groupPrefix(ns), storage.KeysOnly)
|
|
for iterator.Next(it) {
|
|
nsExtended.GroupsCount += 1
|
|
}
|
|
|
|
it = storage.Find(ctx, namespaceSubjectPrefix(ns), storage.KeysOnly)
|
|
for iterator.Next(it) {
|
|
nsExtended.SubjectsCount += 1
|
|
}
|
|
|
|
return nsExtended
|
|
}
|
|
|
|
func ListNamespaces() iterator.Iterator {
|
|
ctx := storage.GetReadOnlyContext()
|
|
return storage.Find(ctx, []byte{namespaceKeysPrefix}, storage.ValuesOnly|storage.DeserializeValues)
|
|
}
|
|
|
|
func ListNamespaceSubjects(ns string) iterator.Iterator {
|
|
ctx := storage.GetReadOnlyContext()
|
|
return storage.Find(ctx, namespaceSubjectPrefix(ns), storage.KeysOnly|storage.RemovePrefix)
|
|
}
|
|
|
|
// CreateGroup creates a new group within the specified namespace.
|
|
func CreateGroup(ns, group string) int {
|
|
ctx := storage.GetContext()
|
|
checkContractOwner(ctx)
|
|
|
|
if group == "" {
|
|
panic("invalid group name")
|
|
}
|
|
|
|
nsKey := namespaceKey(ns)
|
|
data := storage.Get(ctx, nsKey).([]byte)
|
|
if data == nil {
|
|
panic("namespace not found")
|
|
}
|
|
|
|
groupCountID := storage.Get(ctx, groupCounterKey).(int)
|
|
groupCountID++
|
|
storage.Put(ctx, groupCounterKey, groupCountID)
|
|
|
|
gr := Group{
|
|
ID: groupCountID,
|
|
Name: group,
|
|
Namespace: ns,
|
|
}
|
|
|
|
setNamespaceGroupName(ctx, gr)
|
|
|
|
gKey := groupKey(ns, groupCountID)
|
|
storage.Put(ctx, gKey, std.Serialize(gr))
|
|
|
|
runtime.Notify("CreateGroup", ns, group)
|
|
|
|
return groupCountID
|
|
}
|
|
|
|
// GetGroup retrieves the group with the specified ID within the given namespace.
|
|
func GetGroup(ns string, groupID int) Group {
|
|
ctx := storage.GetReadOnlyContext()
|
|
gKey := groupKey(ns, groupID)
|
|
data := storage.Get(ctx, gKey).([]byte)
|
|
if data == nil {
|
|
panic("group not found")
|
|
}
|
|
|
|
return std.Deserialize(data).(Group)
|
|
}
|
|
|
|
// GetGroupExtended retrieves extended information about the group, including the count of subjects in the group.
|
|
func GetGroupExtended(ns string, groupID int) GroupExtended {
|
|
ctx := storage.GetReadOnlyContext()
|
|
gKey := groupKey(ns, groupID)
|
|
data := storage.Get(ctx, gKey).([]byte)
|
|
if data == nil {
|
|
panic("group not found")
|
|
}
|
|
|
|
gr := std.Deserialize(data).(Group)
|
|
|
|
grExtended := GroupExtended{
|
|
ID: gr.ID,
|
|
Name: gr.Name,
|
|
Namespace: gr.Namespace,
|
|
KV: gr.KV,
|
|
}
|
|
|
|
it := storage.Find(ctx, groupSubjectPrefix(ns, groupID), storage.KeysOnly)
|
|
for iterator.Next(it) {
|
|
grExtended.SubjectsCount += 1
|
|
}
|
|
|
|
return grExtended
|
|
}
|
|
|
|
// GetGroupIDByName retrieves the ID of the group with the specified name within the given namespace.
|
|
func GetGroupIDByName(ns, name string) int {
|
|
if name == "" {
|
|
panic("invalid name")
|
|
}
|
|
|
|
ctx := storage.GetReadOnlyContext()
|
|
|
|
nsGroupNameKey := namespaceGroupNameKey(ns, name)
|
|
groupIDRaw := storage.Get(ctx, nsGroupNameKey).([]byte)
|
|
if groupIDRaw == nil {
|
|
panic("group not found")
|
|
}
|
|
|
|
return std.Deserialize(groupIDRaw).(int)
|
|
}
|
|
|
|
// GetGroupByName retrieves the group with the specified name within the given namespace.
|
|
func GetGroupByName(ns, name string) Group {
|
|
groupID := GetGroupIDByName(ns, name)
|
|
gKey := groupKey(ns, groupID)
|
|
|
|
ctx := storage.GetReadOnlyContext()
|
|
data := storage.Get(ctx, gKey).([]byte)
|
|
if data == nil {
|
|
panic("group not found")
|
|
}
|
|
|
|
return std.Deserialize(data).(Group)
|
|
}
|
|
|
|
// SetGroupName sets a new name for the group with the specified ID within the given namespace.
|
|
func SetGroupName(ns string, groupID int, name string) {
|
|
ctx := storage.GetContext()
|
|
checkContractOwner(ctx)
|
|
|
|
gKey := groupKey(ns, groupID)
|
|
data := storage.Get(ctx, gKey).([]byte)
|
|
if data == nil {
|
|
panic("group not found")
|
|
}
|
|
|
|
gr := std.Deserialize(data).(Group)
|
|
oldName := gr.Name
|
|
gr.Name = name
|
|
storage.Put(ctx, gKey, std.Serialize(gr))
|
|
|
|
updateNamespaceGroupName(ctx, gr, oldName)
|
|
|
|
runtime.Notify("SetGroupName", ns, groupID, name)
|
|
}
|
|
|
|
// SetGroupKV sets a key-value pair for the group with the specified ID within the given namespace.
|
|
func SetGroupKV(ns string, groupID int, key, val string) {
|
|
ctx := storage.GetContext()
|
|
checkContractOwner(ctx)
|
|
|
|
gKey := groupKey(ns, groupID)
|
|
data := storage.Get(ctx, gKey).([]byte)
|
|
if data == nil {
|
|
panic("group not found")
|
|
}
|
|
|
|
gr := std.Deserialize(data).(Group)
|
|
if gr.KV == nil {
|
|
gr.KV = map[string]string{}
|
|
}
|
|
gr.KV[key] = val
|
|
|
|
storage.Put(ctx, gKey, std.Serialize(gr))
|
|
runtime.Notify("SetGroupKV", ns, groupID, key, val)
|
|
}
|
|
|
|
// DeleteGroupKV deletes a key-value pair from the group with the specified ID within the given namespace.
|
|
func DeleteGroupKV(ns string, groupID int, key string) {
|
|
ctx := storage.GetContext()
|
|
checkContractOwner(ctx)
|
|
|
|
gKey := groupKey(ns, groupID)
|
|
data := storage.Get(ctx, gKey).([]byte)
|
|
if data == nil {
|
|
panic("group not found")
|
|
}
|
|
|
|
gr := std.Deserialize(data).(Group)
|
|
delete(gr.KV, key)
|
|
|
|
storage.Put(ctx, gKey, std.Serialize(gr))
|
|
runtime.Notify("DeleteGroupKV", ns, groupID, key)
|
|
}
|
|
|
|
func ListGroups(ns string) iterator.Iterator {
|
|
ctx := storage.GetReadOnlyContext()
|
|
return storage.Find(ctx, groupPrefix(ns), storage.ValuesOnly|storage.DeserializeValues)
|
|
}
|
|
|
|
// AddSubjectToGroup adds a subject to a group with the specified ID.
|
|
func AddSubjectToGroup(addr interop.Hash160, groupID int) {
|
|
ctx := storage.GetContext()
|
|
checkContractOwner(ctx)
|
|
|
|
if len(addr) != interop.Hash160Len {
|
|
panic("invalid address length")
|
|
}
|
|
|
|
sKey := subjectKeyFromAddr(addr)
|
|
data := storage.Get(ctx, sKey).([]byte)
|
|
if data == nil {
|
|
panic("subject not found")
|
|
}
|
|
subject := std.Deserialize(data).(Subject)
|
|
|
|
gKey := groupKey(subject.Namespace, groupID)
|
|
data = storage.Get(ctx, gKey).([]byte)
|
|
if data == nil {
|
|
panic("group not found")
|
|
}
|
|
group := std.Deserialize(data).(Group)
|
|
|
|
if group.Namespace != subject.Namespace {
|
|
panic("subject and group must be in the same namespace")
|
|
}
|
|
|
|
gsKey := groupSubjectKey(subject.Namespace, groupID, addr)
|
|
storage.Put(ctx, gsKey, []byte{1})
|
|
|
|
runtime.Notify("AddSubjectToGroup", addr, subject.Namespace, groupID)
|
|
}
|
|
|
|
// RemoveSubjectFromGroup removes a subject from a group with the specified ID.
|
|
func RemoveSubjectFromGroup(addr interop.Hash160, groupID int) {
|
|
ctx := storage.GetContext()
|
|
checkContractOwner(ctx)
|
|
|
|
if len(addr) != interop.Hash160Len {
|
|
panic("invalid address length")
|
|
}
|
|
|
|
sKey := subjectKeyFromAddr(addr)
|
|
data := storage.Get(ctx, sKey).([]byte)
|
|
if data == nil {
|
|
panic("subject not found")
|
|
}
|
|
|
|
subject := std.Deserialize(data).(Subject)
|
|
|
|
gKey := groupKey(subject.Namespace, groupID)
|
|
data = storage.Get(ctx, gKey).([]byte)
|
|
if data == nil {
|
|
panic("group not found")
|
|
}
|
|
group := std.Deserialize(data).(Group)
|
|
|
|
if group.Namespace != subject.Namespace {
|
|
panic("subject and group must be in the same namespace")
|
|
}
|
|
|
|
gsKey := groupSubjectKey(subject.Namespace, groupID, addr)
|
|
storage.Delete(ctx, gsKey)
|
|
|
|
runtime.Notify("RemoveSubjectFromGroup", addr, subject.Namespace, groupID)
|
|
}
|
|
|
|
func ListGroupSubjects(ns string, groupID int) iterator.Iterator {
|
|
ctx := storage.GetReadOnlyContext()
|
|
return storage.Find(ctx, groupSubjectPrefix(ns, groupID), storage.KeysOnly|storage.RemovePrefix)
|
|
}
|
|
|
|
// DeleteGroup deletes the group with the specified ID within the given namespace.
|
|
func DeleteGroup(ns string, groupID int) {
|
|
ctx := storage.GetContext()
|
|
checkContractOwner(ctx)
|
|
|
|
gKey := groupKey(ns, groupID)
|
|
data := storage.Get(ctx, gKey).([]byte)
|
|
if data == nil {
|
|
panic("group not found")
|
|
}
|
|
gr := std.Deserialize(data).(Group)
|
|
|
|
storage.Delete(ctx, gKey)
|
|
|
|
it := storage.Find(ctx, groupSubjectPrefix(ns, groupID), storage.KeysOnly)
|
|
for iterator.Next(it) {
|
|
gsKey := iterator.Value(it).([]byte)
|
|
storage.Delete(ctx, gsKey)
|
|
}
|
|
|
|
deleteNamespaceGroupName(ctx, ns, gr.Name)
|
|
|
|
runtime.Notify("DeleteGroup", ns, groupID)
|
|
}
|
|
|
|
func checkContractOwner(ctx storage.Context) {
|
|
addr := storage.Get(ctx, adminKey)
|
|
if addr != nil && runtime.CheckWitness(addr.(interop.Hash160)) {
|
|
return
|
|
}
|
|
if common.HasUpdateAccess() {
|
|
return
|
|
}
|
|
panic("not witnessed")
|
|
}
|
|
|
|
func removeSubjectFromNamespace(ctx storage.Context, ns string, addr interop.Hash160) {
|
|
nsSubjKey := namespaceSubjectKey(ns, addr)
|
|
storage.Delete(ctx, nsSubjKey)
|
|
|
|
nsHash := ripemd160Hash(ns)
|
|
it := storage.Find(ctx, groupPrefixFromHash(nsHash), storage.KeysOnly|storage.RemovePrefix)
|
|
for iterator.Next(it) {
|
|
groupHash := iterator.Value(it).([]byte)
|
|
storage.Delete(ctx, groupSubjectKeyFromHashes(nsHash, groupHash, addr))
|
|
}
|
|
}
|
|
|
|
func setNamespaceSubjectName(ctx storage.Context, subj Subject) {
|
|
if subj.Name == "" {
|
|
return
|
|
}
|
|
|
|
nsSubjNameKey := namespaceSubjectNameKey(subj.Namespace, subj.Name)
|
|
subjKey := storage.Get(ctx, nsSubjNameKey).(interop.PublicKey)
|
|
if subjKey == nil {
|
|
storage.Put(ctx, nsSubjNameKey, subj.PrimaryKey)
|
|
} else if !common.BytesEqual(subjKey, subj.PrimaryKey) {
|
|
panic("subject name is not available in the current namespace")
|
|
}
|
|
}
|
|
|
|
func deleteNamespaceSubjectName(ctx storage.Context, ns, subjName string) {
|
|
if subjName == "" {
|
|
return
|
|
}
|
|
|
|
nsSubjNameKey := namespaceSubjectNameKey(ns, subjName)
|
|
storage.Delete(ctx, nsSubjNameKey)
|
|
}
|
|
|
|
func updateNamespaceSubjectName(ctx storage.Context, subj Subject, oldName string) {
|
|
if subj.Name == oldName {
|
|
return
|
|
}
|
|
|
|
deleteNamespaceSubjectName(ctx, subj.Namespace, oldName)
|
|
setNamespaceSubjectName(ctx, subj)
|
|
}
|
|
|
|
func updateNamespaceGroupName(ctx storage.Context, gr Group, oldName string) {
|
|
if gr.Name == oldName {
|
|
return
|
|
}
|
|
|
|
deleteNamespaceGroupName(ctx, gr.Namespace, oldName)
|
|
setNamespaceGroupName(ctx, gr)
|
|
}
|
|
|
|
func deleteNamespaceGroupName(ctx storage.Context, ns, grName string) {
|
|
if grName == "" {
|
|
return
|
|
}
|
|
|
|
nsGroupNameKey := namespaceGroupNameKey(ns, grName)
|
|
storage.Delete(ctx, nsGroupNameKey)
|
|
}
|
|
|
|
func setNamespaceGroupName(ctx storage.Context, gr Group) {
|
|
if gr.Name == "" {
|
|
return
|
|
}
|
|
|
|
nsGroupNameKey := namespaceGroupNameKey(gr.Namespace, gr.Name)
|
|
groupIDRaw := storage.Get(ctx, nsGroupNameKey).([]byte)
|
|
if groupIDRaw == nil {
|
|
storage.Put(ctx, nsGroupNameKey, std.Serialize(gr.ID))
|
|
return
|
|
}
|
|
|
|
groupID := std.Deserialize(groupIDRaw).(int)
|
|
if groupID != gr.ID {
|
|
panic("group name is not available in the current namespace")
|
|
}
|
|
}
|
|
|
|
func subjectKey(key interop.PublicKey) []byte {
|
|
addr := contract.CreateStandardAccount(key)
|
|
return subjectKeyFromAddr(addr)
|
|
}
|
|
|
|
func subjectKeyFromAddr(addr interop.Hash160) []byte {
|
|
return append([]byte{subjectKeysPrefix}, addr...)
|
|
}
|
|
|
|
func subjectAdditionalKey(additionalKey interop.PublicKey, primeAddr interop.Hash160) []byte {
|
|
return append(subjectAdditionalPrefix(additionalKey), primeAddr...)
|
|
}
|
|
|
|
func subjectAdditionalPrefix(additionalKey interop.PublicKey) []byte {
|
|
addr := contract.CreateStandardAccount(additionalKey)
|
|
return append([]byte{additionalKeysPrefix}, addr...)
|
|
}
|
|
|
|
func namespaceKey(ns string) []byte {
|
|
return namespaceKeyFromHash(ripemd160Hash(ns))
|
|
}
|
|
|
|
func namespaceKeyFromHash(ns []byte) []byte {
|
|
return append([]byte{namespaceKeysPrefix}, ns...)
|
|
}
|
|
|
|
func groupKey(ns string, groupID int) []byte {
|
|
prefix := groupPrefix(ns)
|
|
return append(prefix, idToBytes(groupID)...)
|
|
}
|
|
|
|
func groupKeyFromHashes(nsHash []byte, groupIDBytes []byte) []byte {
|
|
prefix := groupPrefixFromHash(nsHash)
|
|
return append(prefix, groupIDBytes...)
|
|
}
|
|
|
|
func groupPrefix(ns string) []byte {
|
|
return groupPrefixFromHash(ripemd160Hash(ns))
|
|
}
|
|
|
|
func groupPrefixFromHash(nsHash []byte) []byte {
|
|
return append([]byte{groupKeysPrefix}, nsHash...)
|
|
}
|
|
|
|
func ripemd160Hash(data string) []byte {
|
|
return crypto.Ripemd160([]byte(data))
|
|
}
|
|
|
|
func namespaceSubjectKey(ns string, addr interop.Hash160) []byte {
|
|
prefix := namespaceSubjectPrefix(ns)
|
|
return append(prefix, addr...)
|
|
}
|
|
|
|
func namespaceSubjectPrefix(ns string) []byte {
|
|
nsHash := ripemd160Hash(ns)
|
|
return append([]byte{namespaceSubjectsKeysPrefix}, nsHash...)
|
|
}
|
|
|
|
func namespaceSubjectNameKey(ns, subjName string) []byte {
|
|
nsHash := ripemd160Hash(ns)
|
|
nameHash := ripemd160Hash(subjName)
|
|
return append([]byte{namespaceSubjectsNamesPrefix}, append(nsHash, nameHash...)...)
|
|
}
|
|
|
|
func namespaceGroupNameKey(ns, groupName string) []byte {
|
|
nsHash := ripemd160Hash(ns)
|
|
nameHash := ripemd160Hash(groupName)
|
|
return append([]byte{namespaceGroupsNamesPrefix}, append(nsHash, nameHash...)...)
|
|
}
|
|
|
|
func groupSubjectKey(ns string, groupID int, addr interop.Hash160) []byte {
|
|
prefix := groupSubjectPrefix(ns, groupID)
|
|
return append(prefix, addr...)
|
|
}
|
|
|
|
func groupSubjectKeyFromHashes(nsHash, groupHash []byte, addr interop.Hash160) []byte {
|
|
prefix := groupSubjectPrefixFromHashes(nsHash, groupHash)
|
|
return append(prefix, addr...)
|
|
}
|
|
|
|
func groupSubjectPrefix(ns string, groupID int) []byte {
|
|
nsHash := ripemd160Hash(ns)
|
|
return groupSubjectPrefixFromHashes(nsHash, idToBytes(groupID))
|
|
}
|
|
|
|
func groupSubjectPrefixFromHashes(nsHash, groupIDBytes []byte) []byte {
|
|
return append([]byte{groupSubjectsKeysPrefix}, append(nsHash, groupIDBytes...)...)
|
|
}
|
|
|
|
// idToBytes converts i64 value to BE bytes. Panics if value is bigger than i64.
|
|
func idToBytes(itemID int) []byte {
|
|
b := convert.ToBytes(itemID)
|
|
ln := len(b)
|
|
if ln > 8 {
|
|
panic("item ID exceeds 8 byte limit")
|
|
}
|
|
zeros := make([]byte, 8-ln)
|
|
return append(b, zeros...)
|
|
}
|
|
|
|
func addressKey(address []byte) []byte {
|
|
return append([]byte{addressPrefix}, address...)
|
|
}
|