Compare commits

..

14 commits

Author SHA1 Message Date
201db45bd7
[#142] common: Update version
Signed-off-by: Alexander Chuprov <a.chuprov@yadro.com>
2024-12-12 15:25:14 +03:00
e002a1fa80
[#142] common: Downgrade previous version
Signed-off-by: Alexander Chuprov <a.chuprov@yadro.com>
2024-12-12 15:25:02 +03:00
31709a3d95
[#141] frostfsid: Remove address index in DeleteSubject()
Signed-off-by: Alexander Chuprov <a.chuprov@yadro.com>
2024-12-12 14:42:03 +03:00
3c7a9cb743
[#136] container/test: Move container domain to constant
Signed-off-by: Alexander Chuprov <a.chuprov@yadro.com>
2024-12-12 13:19:34 +03:00
780b48cedf
[#136] container: Add SetAdmin
SetAdmin will allow to manage the root domain of the container.

Signed-off-by: Alexander Chuprov <a.chuprov@yadro.com>
2024-12-12 13:19:34 +03:00
a3021f18cf [#135] frostfsid: Make migration idempotent
Signed-off-by: Alexander Chuprov <a.chuprov@yadro.com>
2024-12-12 06:28:19 +00:00
891e268170
[#138] frostfs: Remove AlphabetUpdate event from the configuration
It does not exist in code.
Refs #7.

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2024-12-11 14:36:55 +03:00
762d7f9f9f
[#134] container: Use Hash256 for container ID in Delete()
This is what we use inside Put(). It leads to a better auto-generated
code.

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2024-12-05 11:38:07 +03:00
7e0f9b8b8e
Release v0.21.0
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2024-12-05 11:38:07 +03:00
fe7a767e8f
[#131] nns: Declare getAllRecords() as safe
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2024-11-21 10:23:31 +03:00
60b81c4bf6
[#131] *: Reformat code
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2024-11-21 10:23:31 +03:00
a2c2791146
[#129] frostfsid: Return subject by an additional address
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2024-11-19 17:41:06 +03:00
7a8c64b966
[#118] frostfsid: Restrict keys to a single subject
Signed-off-by: Alexander Chuprov <a.chuprov@yadro.com>
2024-11-13 21:35:15 +03:00
8b586081eb
[#125] CODEOWNERS: Refine ownership
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2024-11-07 09:24:36 +03:00
17 changed files with 249 additions and 143 deletions

View file

@ -1,6 +1,7 @@
# Changelog # Changelog
Changelog for FrostFS Contract Changelog for FrostFS Contract
## [Unreleased] ## [Unreleased]
### Added ### Added
@ -9,6 +10,21 @@ Changelog for FrostFS Contract
### Updated ### Updated
### Fixed ### Fixed
## [0.21.0]
### Added
- Mention domain name in error messages in the nns contract (#92)
- Emit DeleteRecord event on record deletion in the nns contract (#114)
### Changed
- Allow to register TLD automatically (#114)
- Use frostfsid claims as a permission to create TLD (#115)
- Ensure subject keys are unique (#118, #129)
### Fixed
- Terminate session in `ReadIteratorItems()` (#85)
- Declare `nns.getAllRecords` as safe (#131)
## [0.20.0] ## [0.20.0]
### Added ### Added

View file

@ -1 +1,5 @@
.* @fyrchik .forgejo/.* @potyarkin
Makefile @potyarkin
frostfsid/client/.* @dkirillov
.* @TrueCloudLab/storage-core-developers @TrueCloudLab/storage-core-committers @TrueCloudLab/storage-service-developers @TrueCloudLab/storage-service-committers
tests/.* @fyrchik

View file

@ -1 +1 @@
v0.20.0 v0.21.1

View file

@ -4,15 +4,15 @@ import "github.com/nspcc-dev/neo-go/pkg/interop/native/std"
const ( const (
major = 0 major = 0
minor = 20 minor = 21
patch = 0 patch = 1
// Versions from which an update should be performed. // Versions from which an update should be performed.
// These should be used in a group (so prevMinor can be equal to minor if there are // These should be used in a group (so prevMinor can be equal to minor if there are
// any migration routines. // any migration routines.
prevMajor = 0 prevMajor = 0
prevMinor = 19 prevMinor = 19
prevPatch = 3 prevPatch = 4
Version = major*1_000_000 + minor*1_000 + patch Version = major*1_000_000 + minor*1_000 + patch

View file

@ -18,6 +18,7 @@ permissions:
- "register" - "register"
- "transferX" - "transferX"
- "update" - "update"
- "setAdmin"
events: events:
- name: PutSuccess - name: PutSuccess
@ -29,7 +30,7 @@ events:
- name: DeleteSuccess - name: DeleteSuccess
parameters: parameters:
- name: containerID - name: containerID
type: ByteArray type: Hash256
- name: SetEACLSuccess - name: SetEACLSuccess
parameters: parameters:
- name: containerID - name: containerID

View file

@ -279,7 +279,7 @@ func checkNiceNameAvailable(nnsContractAddr interop.Hash160, domain string) bool
// API. // API.
// //
// If the container doesn't exist, it panics with NotFoundError. // If the container doesn't exist, it panics with NotFoundError.
func Delete(containerID []byte, signature interop.Signature, publicKey interop.PublicKey, token []byte) { func Delete(containerID interop.Hash256, signature interop.Signature, publicKey interop.PublicKey, token []byte) {
ctx := storage.GetContext() ctx := storage.GetContext()
ownerID := getOwnerByID(ctx, containerID) ownerID := getOwnerByID(ctx, containerID)
@ -565,6 +565,21 @@ func NewEpoch(epochNum int) {
cleanupContainers(ctx, epochNum) cleanupContainers(ctx, epochNum)
} }
// SetAdmin sets admin for root container domain.
func SetAdmin(admin interop.Hash160) {
ctx := storage.GetReadOnlyContext()
if !runtime.CheckWitness(common.CommitteeAddress()) {
panic("only committee can set admin")
}
addrNNS := storage.Get(ctx, nnsContractKey).(interop.Hash160)
rootContainerDomain := storage.Get(ctx, nnsRootKey).(string)
contract.Call(addrNNS, "setAdmin", contract.All,
rootContainerDomain, admin)
}
// StartContainerEstimation method produces StartEstimation notification. // StartContainerEstimation method produces StartEstimation notification.
// It can be invoked only by Alphabet nodes of the Inner Ring. // It can be invoked only by Alphabet nodes of the Inner Ring.
func StartContainerEstimation(epoch int) { func StartContainerEstimation(epoch int) {

View file

@ -47,12 +47,6 @@ events:
type: ByteArray type: ByteArray
- name: keys - name: keys
type: Array type: Array
- name: AlphabetUpdate
parameters:
- name: id
type: ByteArray
- name: alphabet
type: Array
- name: SetConfig - name: SetConfig
parameters: parameters:
- name: id - name: id

View file

@ -20,6 +20,7 @@ FrostFSID contract does not produce notifications to process.
| `G` + [ RIPEMD160 of namespace ] + [ 8 byte group id ] + [ subject address ] | []byte{1} | subject that belongs to the group | | `G` + [ RIPEMD160 of namespace ] + [ 8 byte group id ] + [ subject address ] | []byte{1} | subject that belongs to the group |
| `c` | Int | group id counter | | `c` | Int | group id counter |
| `m` + [ RIPEMD160 of namespace ] + [ RIPEMD160 of subject name ] | Serialized group id int | group name to group id index | | `m` + [ RIPEMD160 of namespace ] + [ RIPEMD160 of subject name ] | Serialized group id int | group name to group id index |
| `A` + [ subject address ] | bool | means that the wallet has been used |
*/ */

View file

@ -96,13 +96,15 @@ const (
groupSubjectsKeysPrefix = 'G' groupSubjectsKeysPrefix = 'G'
groupCounterKey = 'c' groupCounterKey = 'c'
namespaceGroupsNamesPrefix = 'm' namespaceGroupsNamesPrefix = 'm'
addressPrefix = 'A'
) )
func _deploy(data any, isUpdate bool) { func _deploy(data any, isUpdate bool) {
ctx := storage.GetContext() ctx := storage.GetContext()
args := data.(struct { args := data.(struct {
admin interop.Hash160 admin interop.Hash160
version int
}) })
if args.admin != nil { if args.admin != nil {
@ -112,6 +114,28 @@ func _deploy(data any, isUpdate bool) {
storage.Put(ctx, adminKey, args.admin) storage.Put(ctx, adminKey, args.admin)
} }
if isUpdate {
common.CheckVersion(args.version)
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, groupCounterKey, 0)
storage.Put(ctx, namespaceKey(""), std.Serialize(Namespace{})) storage.Put(ctx, namespaceKey(""), std.Serialize(Namespace{}))
@ -182,6 +206,11 @@ func CreateSubject(ns string, key interop.PublicKey) {
panic("key is occupied") panic("key is occupied")
} }
allAddressKey := addressKey(addr)
if storage.Get(ctx, allAddressKey) != nil {
panic("key is occupied by another additional key")
}
nsKey := namespaceKey(ns) nsKey := namespaceKey(ns)
data = storage.Get(ctx, nsKey).([]byte) data = storage.Get(ctx, nsKey).([]byte)
if data == nil { if data == nil {
@ -197,6 +226,7 @@ func CreateSubject(ns string, key interop.PublicKey) {
nsSubjKey := namespaceSubjectKey(ns, addr) nsSubjKey := namespaceSubjectKey(ns, addr)
storage.Put(ctx, nsSubjKey, []byte{1}) storage.Put(ctx, nsSubjKey, []byte{1})
storage.Put(ctx, allAddressKey, true)
runtime.Notify("CreateSubject", interop.Hash160(addr)) runtime.Notify("CreateSubject", interop.Hash160(addr))
} }
@ -213,6 +243,11 @@ func AddSubjectKey(addr interop.Hash160, key interop.PublicKey) {
panic("incorrect public key length") panic("incorrect public key length")
} }
addressKey := addressKey(contract.CreateStandardAccount(key))
if storage.Get(ctx, addressKey) != nil {
panic("key is occupied")
}
saKey := subjectAdditionalKey(key, addr) saKey := subjectAdditionalKey(key, addr)
data := storage.Get(ctx, saKey).([]byte) data := storage.Get(ctx, saKey).([]byte)
if data != nil { if data != nil {
@ -230,6 +265,7 @@ func AddSubjectKey(addr interop.Hash160, key interop.PublicKey) {
subject.AdditionalKeys = append(subject.AdditionalKeys, key) subject.AdditionalKeys = append(subject.AdditionalKeys, key)
storage.Put(ctx, sKey, std.Serialize(subject)) storage.Put(ctx, sKey, std.Serialize(subject))
storage.Put(ctx, addressKey, true)
runtime.Notify("AddSubjectKey", addr, key) runtime.Notify("AddSubjectKey", addr, key)
} }
@ -269,6 +305,7 @@ func RemoveSubjectKey(addr interop.Hash160, key interop.PublicKey) {
subject.AdditionalKeys = additionalKeys subject.AdditionalKeys = additionalKeys
storage.Put(ctx, sKey, std.Serialize(subject)) storage.Put(ctx, sKey, std.Serialize(subject))
storage.Delete(ctx, addressKey(contract.CreateStandardAccount(key)))
runtime.Notify("RemoveSubjectKey", addr, key) runtime.Notify("RemoveSubjectKey", addr, key)
} }
@ -362,7 +399,9 @@ func DeleteSubject(addr interop.Hash160) {
for i := 0; i < len(subj.AdditionalKeys); i++ { for i := 0; i < len(subj.AdditionalKeys); i++ {
storage.Delete(ctx, subjectAdditionalKey(subj.AdditionalKeys[i], addr)) storage.Delete(ctx, subjectAdditionalKey(subj.AdditionalKeys[i], addr))
storage.Delete(ctx, addressKey(contract.CreateStandardAccount(subj.AdditionalKeys[i])))
} }
storage.Delete(ctx, addressKey(addr))
storage.Delete(ctx, sKey) storage.Delete(ctx, sKey)
removeSubjectFromNamespace(ctx, subj.Namespace, addr) removeSubjectFromNamespace(ctx, subj.Namespace, addr)
@ -381,7 +420,12 @@ func GetSubject(addr interop.Hash160) Subject {
sKey := subjectKeyFromAddr(addr) sKey := subjectKeyFromAddr(addr)
data := storage.Get(ctx, sKey).([]byte) data := storage.Get(ctx, sKey).([]byte)
if data == nil { if data == nil {
panic("subject not found") 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) return std.Deserialize(data).(Subject)
@ -433,21 +477,25 @@ func GetSubjectByKey(key interop.PublicKey) Subject {
return std.Deserialize(data).(Subject) return std.Deserialize(data).(Subject)
} }
saPrefix := subjectAdditionalPrefix(key) addr := getPrimaryAddr(ctx, contract.CreateStandardAccount(key))
it := storage.Find(ctx, saPrefix, storage.KeysOnly|storage.RemovePrefix) sKey = subjectKeyFromAddr(addr)
for iterator.Next(it) { data = storage.Get(ctx, sKey).([]byte)
addr := iterator.Value(it).([]byte) if data != nil {
sKey = subjectKeyFromAddr(addr) return std.Deserialize(data).(Subject)
data = storage.Get(ctx, sKey).([]byte)
if data != nil {
return std.Deserialize(data).(Subject)
}
break
} }
panic("subject not found") 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. // GetSubjectByName retrieves the subject with the specified name within the given namespace.
func GetSubjectByName(ns, name string) Subject { func GetSubjectByName(ns, name string) Subject {
key := GetSubjectKeyByName(ns, name) key := GetSubjectKeyByName(ns, name)
@ -1029,3 +1077,7 @@ func idToBytes(itemID int) []byte {
zeros := make([]byte, 8-ln) zeros := make([]byte, 8-ln)
return append(b, zeros...) return append(b, zeros...)
} }
func addressKey(address []byte) []byte {
return append([]byte{addressPrefix}, address...)
}

View file

@ -2,6 +2,7 @@ name: "NameService"
supportedstandards: ["NEP-11"] supportedstandards: ["NEP-11"]
safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf", safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf",
"tokens", "properties", "roots", "getPrice", "isAvailable", "getRecords", "tokens", "properties", "roots", "getPrice", "isAvailable", "getRecords",
"getAllRecords",
"resolve", "version"] "resolve", "version"]
events: events:
- name: RegisterDomain - name: RegisterDomain

View file

@ -41,8 +41,8 @@ const (
// prefixRecord contains map from (token key + hash160(token name) + record type) // prefixRecord contains map from (token key + hash160(token name) + record type)
// to record. // to record.
prefixRecord byte = 0x22 prefixRecord byte = 0x22
//prefixGlobalDomain contains a flag indicating that this domain was created using GlobalDomain. // prefixGlobalDomain contains a flag indicating that this domain was created using GlobalDomain.
//This is necessary to distinguish it from regular CNAME records. // This is necessary to distinguish it from regular CNAME records.
prefixGlobalDomain byte = 0x23 prefixGlobalDomain byte = 0x23
// prefixCountSubDomains contains information about the number of domains in the zone. // prefixCountSubDomains contains information about the number of domains in the zone.
// If it is nil, it will definitely be calculated on the first removal. // If it is nil, it will definitely be calculated on the first removal.
@ -79,7 +79,7 @@ const (
const ( const (
// Cnametgt is a special TXT record ensuring all created subdomains point to the global domain - the value of this variable. // Cnametgt is a special TXT record ensuring all created subdomains point to the global domain - the value of this variable.
//It is guaranteed that two domains cannot point to the same global domain. // It is guaranteed that two domains cannot point to the same global domain.
Cnametgt = "cnametgt" Cnametgt = "cnametgt"
) )

View file

@ -25,7 +25,7 @@ type PutSuccessEvent struct {
// DeleteSuccessEvent represents "DeleteSuccess" event emitted by the contract. // DeleteSuccessEvent represents "DeleteSuccess" event emitted by the contract.
type DeleteSuccessEvent struct { type DeleteSuccessEvent struct {
ContainerID []byte ContainerID util.Uint256
} }
// SetEACLSuccessEvent represents "SetEACLSuccess" event emitted by the contract. // SetEACLSuccessEvent represents "SetEACLSuccess" event emitted by the contract.
@ -163,14 +163,14 @@ func (c *ContractReader) Version() (*big.Int, error) {
// Delete creates a transaction invoking `delete` method of the contract. // Delete creates a transaction invoking `delete` 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.
func (c *Contract) Delete(containerID []byte, signature []byte, publicKey *keys.PublicKey, token []byte) (util.Uint256, uint32, error) { func (c *Contract) Delete(containerID util.Uint256, signature []byte, publicKey *keys.PublicKey, token []byte) (util.Uint256, uint32, error) {
return c.actor.SendCall(c.hash, "delete", containerID, signature, publicKey, token) return c.actor.SendCall(c.hash, "delete", containerID, signature, publicKey, token)
} }
// DeleteTransaction creates a transaction invoking `delete` method of the contract. // DeleteTransaction creates a transaction invoking `delete` method of the contract.
// This transaction is signed, but not sent to the network, instead it's // This transaction is signed, but not sent to the network, instead it's
// returned to the caller. // returned to the caller.
func (c *Contract) DeleteTransaction(containerID []byte, signature []byte, publicKey *keys.PublicKey, token []byte) (*transaction.Transaction, error) { func (c *Contract) DeleteTransaction(containerID util.Uint256, signature []byte, publicKey *keys.PublicKey, token []byte) (*transaction.Transaction, error) {
return c.actor.MakeCall(c.hash, "delete", containerID, signature, publicKey, token) return c.actor.MakeCall(c.hash, "delete", containerID, signature, publicKey, token)
} }
@ -178,7 +178,7 @@ func (c *Contract) DeleteTransaction(containerID []byte, signature []byte, publi
// This transaction is not signed, it's simply returned to the caller. // 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, // Any fields of it that do not affect fees can be changed (ValidUntilBlock,
// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. // Nonce), fee values (NetworkFee, SystemFee) can be increased as well.
func (c *Contract) DeleteUnsigned(containerID []byte, signature []byte, publicKey *keys.PublicKey, token []byte) (*transaction.Transaction, error) { func (c *Contract) DeleteUnsigned(containerID util.Uint256, signature []byte, publicKey *keys.PublicKey, token []byte) (*transaction.Transaction, error) {
return c.actor.MakeUnsignedCall(c.hash, "delete", nil, containerID, signature, publicKey, token) return c.actor.MakeUnsignedCall(c.hash, "delete", nil, containerID, signature, publicKey, token)
} }
@ -270,6 +270,28 @@ func (c *Contract) PutNamedUnsigned(container []byte, signature []byte, publicKe
return c.actor.MakeUnsignedCall(c.hash, "putNamed", nil, container, signature, publicKey, token, name, zone) return c.actor.MakeUnsignedCall(c.hash, "putNamed", nil, container, signature, publicKey, token, name, zone)
} }
// 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.
func (c *Contract) SetAdmin(admin util.Uint160) (util.Uint256, uint32, error) {
return c.actor.SendCall(c.hash, "setAdmin", admin)
}
// SetAdminTransaction creates a transaction invoking `setAdmin` method of the contract.
// This transaction is signed, but not sent to the network, instead it's
// returned to the caller.
func (c *Contract) SetAdminTransaction(admin util.Uint160) (*transaction.Transaction, error) {
return c.actor.MakeCall(c.hash, "setAdmin", admin)
}
// SetAdminUnsigned creates a transaction invoking `setAdmin` 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) SetAdminUnsigned(admin util.Uint160) (*transaction.Transaction, error) {
return c.actor.MakeUnsignedCall(c.hash, "setAdmin", nil, admin)
}
// SetEACL creates a transaction invoking `setEACL` method of the contract. // SetEACL creates a transaction invoking `setEACL` 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.
@ -480,7 +502,17 @@ func (e *DeleteSuccessEvent) FromStackItem(item *stackitem.Array) error {
err error err error
) )
index++ index++
e.ContainerID, err = arr[index].TryBytes() e.ContainerID, err = func(item stackitem.Item) (util.Uint256, error) {
b, err := item.TryBytes()
if err != nil {
return util.Uint256{}, err
}
u, err := util.Uint256DecodeBytesBE(b)
if err != nil {
return util.Uint256{}, err
}
return u, nil
}(arr[index])
if err != nil { if err != nil {
return fmt.Errorf("field ContainerID: %w", err) return fmt.Errorf("field ContainerID: %w", err)
} }

View file

@ -50,12 +50,6 @@ type UnbindEvent struct {
Keys []any Keys []any
} }
// AlphabetUpdateEvent represents "AlphabetUpdate" event emitted by the contract.
type AlphabetUpdateEvent struct {
Id []byte
Alphabet []any
}
// SetConfigEvent represents "SetConfig" event emitted by the contract. // SetConfigEvent represents "SetConfig" event emitted by the contract.
type SetConfigEvent struct { type SetConfigEvent struct {
Id []byte Id []byte
@ -710,77 +704,6 @@ func (e *UnbindEvent) FromStackItem(item *stackitem.Array) error {
return nil return nil
} }
// AlphabetUpdateEventsFromApplicationLog retrieves a set of all emitted events
// with "AlphabetUpdate" name from the provided [result.ApplicationLog].
func AlphabetUpdateEventsFromApplicationLog(log *result.ApplicationLog) ([]*AlphabetUpdateEvent, error) {
if log == nil {
return nil, errors.New("nil application log")
}
var res []*AlphabetUpdateEvent
for i, ex := range log.Executions {
for j, e := range ex.Events {
if e.Name != "AlphabetUpdate" {
continue
}
event := new(AlphabetUpdateEvent)
err := event.FromStackItem(e.Item)
if err != nil {
return nil, fmt.Errorf("failed to deserialize AlphabetUpdateEvent from stackitem (execution #%d, event #%d): %w", i, j, err)
}
res = append(res, event)
}
}
return res, nil
}
// FromStackItem converts provided [stackitem.Array] to AlphabetUpdateEvent or
// returns an error if it's not possible to do to so.
func (e *AlphabetUpdateEvent) FromStackItem(item *stackitem.Array) error {
if item == nil {
return errors.New("nil item")
}
arr, ok := item.Value().([]stackitem.Item)
if !ok {
return errors.New("not an array")
}
if len(arr) != 2 {
return errors.New("wrong number of structure elements")
}
var (
index = -1
err error
)
index++
e.Id, err = arr[index].TryBytes()
if err != nil {
return fmt.Errorf("field Id: %w", err)
}
index++
e.Alphabet, err = func(item stackitem.Item) ([]any, error) {
arr, ok := item.Value().([]stackitem.Item)
if !ok {
return nil, errors.New("not an array")
}
res := make([]any, len(arr))
for i := range res {
res[i], err = arr[i].Value(), error(nil)
if err != nil {
return nil, fmt.Errorf("item %d: %w", i, err)
}
}
return res, nil
}(arr[index])
if err != nil {
return fmt.Errorf("field Alphabet: %w", err)
}
return nil
}
// SetConfigEventsFromApplicationLog retrieves a set of all emitted events // SetConfigEventsFromApplicationLog retrieves a set of all emitted events
// with "SetConfig" name from the provided [result.ApplicationLog]. // with "SetConfig" name from the provided [result.ApplicationLog].
func SetConfigEventsFromApplicationLog(log *result.ApplicationLog) ([]*SetConfigEvent, error) { func SetConfigEventsFromApplicationLog(log *result.ApplicationLog) ([]*SetConfigEvent, error) {

View file

@ -91,6 +91,20 @@ func New(actor Actor, hash util.Uint160) *Contract {
return &Contract{ContractReader{nep11ndt.NonDivisibleReader, actor, hash}, nep11ndt.BaseWriter, actor, hash} return &Contract{ContractReader{nep11ndt.NonDivisibleReader, actor, hash}, nep11ndt.BaseWriter, actor, hash}
} }
// GetAllRecords invokes `getAllRecords` method of contract.
func (c *ContractReader) GetAllRecords(name string) (uuid.UUID, result.Iterator, error) {
return unwrap.SessionIterator(c.invoker.Call(c.hash, "getAllRecords", name))
}
// GetAllRecordsExpanded is similar to GetAllRecords (uses the same contract
// method), but can be useful if the server used doesn't support sessions and
// doesn't expand iterators. It creates a script that will get the specified
// number of result items from the iterator right in the VM and return them to
// you. It's only limited by VM stack and GAS available for RPC invocations.
func (c *ContractReader) GetAllRecordsExpanded(name string, _numOfIteratorItems int) ([]stackitem.Item, error) {
return unwrap.Array(c.invoker.CallAndExpandIterator(c.hash, "getAllRecords", _numOfIteratorItems, name))
}
// GetPrice invokes `getPrice` method of contract. // GetPrice invokes `getPrice` method of contract.
func (c *ContractReader) GetPrice() (*big.Int, error) { func (c *ContractReader) GetPrice() (*big.Int, error) {
return unwrap.BigInt(c.invoker.Call(c.hash, "getPrice")) return unwrap.BigInt(c.invoker.Call(c.hash, "getPrice"))
@ -234,28 +248,6 @@ func (c *Contract) DeleteRecordsUnsigned(name string, typ *big.Int) (*transactio
return c.actor.MakeUnsignedCall(c.hash, "deleteRecords", nil, name, typ) return c.actor.MakeUnsignedCall(c.hash, "deleteRecords", nil, name, typ)
} }
// GetAllRecords creates a transaction invoking `getAllRecords` 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) GetAllRecords(name string) (util.Uint256, uint32, error) {
return c.actor.SendCall(c.hash, "getAllRecords", name)
}
// GetAllRecordsTransaction creates a transaction invoking `getAllRecords` method of the contract.
// This transaction is signed, but not sent to the network, instead it's
// returned to the caller.
func (c *Contract) GetAllRecordsTransaction(name string) (*transaction.Transaction, error) {
return c.actor.MakeCall(c.hash, "getAllRecords", name)
}
// GetAllRecordsUnsigned creates a transaction invoking `getAllRecords` 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) GetAllRecordsUnsigned(name string) (*transaction.Transaction, error) {
return c.actor.MakeUnsignedCall(c.hash, "getAllRecords", nil, name)
}
func (c *Contract) scriptForRegister(name string, owner util.Uint160, email string, refresh *big.Int, retry *big.Int, expire *big.Int, ttl *big.Int) ([]byte, error) { func (c *Contract) scriptForRegister(name string, owner util.Uint160, email string, refresh *big.Int, retry *big.Int, expire *big.Int, ttl *big.Int) ([]byte, error) {
return smartcontract.CreateCallWithAssertScript(c.hash, "register", name, owner, email, refresh, retry, expire, ttl) return smartcontract.CreateCallWithAssertScript(c.hash, "register", name, owner, email, refresh, retry, expire, ttl)
} }

View file

@ -28,13 +28,17 @@ const (
containerAliasFee = 0o_0050_0000 containerAliasFee = 0o_0050_0000
) )
const (
containerRootDomain = "frostfs"
)
func deployContainerContract(t *testing.T, e *neotest.Executor, addrNetmap, addrBalance, addrNNS util.Uint160) util.Uint160 { func deployContainerContract(t *testing.T, e *neotest.Executor, addrNetmap, addrBalance, addrNNS util.Uint160) util.Uint160 {
args := make([]any, 5) args := make([]any, 5)
args[0] = addrNetmap args[0] = addrNetmap
args[1] = addrBalance args[1] = addrBalance
args[2] = util.Uint160{} // not needed for now args[2] = util.Uint160{} // not needed for now
args[3] = addrNNS args[3] = addrNNS
args[4] = "frostfs" args[4] = containerRootDomain
c := neotest.CompileFile(t, e.CommitteeHash, containerPath, path.Join(containerPath, "config.yml")) c := neotest.CompileFile(t, e.CommitteeHash, containerPath, path.Join(containerPath, "config.yml"))
e.DeployContract(t, c, args) e.DeployContract(t, c, args)
@ -207,14 +211,14 @@ func TestContainerPut(t *testing.T) {
stackitem.NewByteArray([]byte(base58.Encode(cnt.id[:]))), stackitem.NewByteArray([]byte(base58.Encode(cnt.id[:]))),
}) })
cNNS := c.CommitteeInvoker(nnsHash) cNNS := c.CommitteeInvoker(nnsHash)
cNNS.Invoke(t, expected, "resolve", "mycnt.frostfs", int64(nns.TXT)) cNNS.Invoke(t, expected, "resolve", "mycnt."+containerRootDomain, int64(nns.TXT))
t.Run("name is already taken", func(t *testing.T) { t.Run("name is already taken", func(t *testing.T) {
c.InvokeFail(t, "name is already taken", "putNamed", putArgs...) c.InvokeFail(t, "name is already taken", "putNamed", putArgs...)
}) })
c.Invoke(t, stackitem.Null{}, "delete", cnt.id[:], cnt.sig, cnt.pub, cnt.token) c.Invoke(t, stackitem.Null{}, "delete", cnt.id[:], cnt.sig, cnt.pub, cnt.token)
cNNS.Invoke(t, stackitem.Null{}, "resolve", "mycnt.frostfs", int64(nns.TXT)) cNNS.Invoke(t, stackitem.Null{}, "resolve", "mycnt."+containerRootDomain, int64(nns.TXT))
t.Run("register in advance", func(t *testing.T) { t.Run("register in advance", func(t *testing.T) {
cnt.value[len(cnt.value)-1] = 10 cnt.value[len(cnt.value)-1] = 10
@ -241,6 +245,16 @@ func TestContainerPut(t *testing.T) {
}) })
}) })
t.Run("test setAdmin", func(t *testing.T) {
ctrNNS := neotest.CompileFile(t, c.CommitteeHash, nnsPath, path.Join(nnsPath, "config.yml"))
nnsInv := c.NewInvoker(ctrNNS.Hash, acc)
containerInv := c.WithSigners(c.Committee, acc)
nnsInv.InvokeFail(t, "not witnessed by admin", "addRecord", containerRootDomain, int64(nns.TXT), nns.Cnametgt+"=animals")
containerInv.Invoke(t, stackitem.Null{}, "setAdmin", acc.ScriptHash())
nnsInv.Invoke(t, stackitem.Null{}, "addRecord", containerRootDomain, int64(nns.TXT), nns.Cnametgt+"=animals")
})
t.Run("create global domain", func(t *testing.T) { t.Run("create global domain", func(t *testing.T) {
ctrNNS := neotest.CompileFile(t, c.CommitteeHash, nnsPath, path.Join(nnsPath, "config.yml")) ctrNNS := neotest.CompileFile(t, c.CommitteeHash, nnsPath, path.Join(nnsPath, "config.yml"))
nnsHash := ctrNNS.Hash nnsHash := ctrNNS.Hash

View file

@ -238,6 +238,13 @@ func TestFrostFSID_SubjectManagement(t *testing.T) {
subj = parseSubject(t, s) subj = parseSubject(t, s)
require.True(t, subjKey.PublicKey().Equal(&subj.PrimaryKey), "keys must be the same") require.True(t, subjKey.PublicKey().Equal(&subj.PrimaryKey), "keys must be the same")
t.Run("with GetSubject", func(t *testing.T) {
s, err = anonInvoker.TestInvoke(t, getSubjectMethod, subjKey.PublicKey().GetScriptHash())
require.NoError(t, err)
subj = parseSubject(t, s)
require.True(t, subjKey.PublicKey().Equal(&subj.PrimaryKey), "keys must be the same")
})
t.Run("remove subject key", func(t *testing.T) { t.Run("remove subject key", func(t *testing.T) {
anonInvoker.InvokeFail(t, notWitnessedError, removeSubjectKeyMethod, subjKeyAddr, subjNewKey.PublicKey().Bytes()) anonInvoker.InvokeFail(t, notWitnessedError, removeSubjectKeyMethod, subjKeyAddr, subjNewKey.PublicKey().Bytes())
invoker.Invoke(t, stackitem.Null{}, removeSubjectKeyMethod, subjKeyAddr, subjNewKey.PublicKey().Bytes()) invoker.Invoke(t, stackitem.Null{}, removeSubjectKeyMethod, subjKeyAddr, subjNewKey.PublicKey().Bytes())
@ -604,6 +611,51 @@ func TestFrostFSID_GroupManagement(t *testing.T) {
}) })
} }
func TestAdditionalKeyFromPrimarySubject(t *testing.T) {
f := newFrostFSIDInvoker(t)
invoker := f.OwnerInvoker()
subjAPrimaryKey, err := keys.NewPrivateKey()
require.NoError(t, err)
subjAKeyAddr := subjAPrimaryKey.PublicKey().GetScriptHash()
subjBPrimaryKey, err := keys.NewPrivateKey()
require.NoError(t, err)
subjBKeyAddr := subjBPrimaryKey.PublicKey().GetScriptHash()
subjCPrimaryKey, err := keys.NewPrivateKey()
require.NoError(t, err)
subjDPrimaryKey, err := keys.NewPrivateKey()
require.NoError(t, err)
subjFPrimaryKey, err := keys.NewPrivateKey()
require.NoError(t, err)
invoker.Invoke(t, stackitem.Null{}, createSubjectMethod, defaultNamespace, subjAPrimaryKey.PublicKey().Bytes())
invoker.Invoke(t, stackitem.Null{}, createSubjectMethod, defaultNamespace, subjBPrimaryKey.PublicKey().Bytes())
invoker.InvokeFail(t, "key is occupied", addSubjectKeyMethod, subjBKeyAddr, subjAPrimaryKey.PublicKey().Bytes())
invoker.Invoke(t, stackitem.Null{}, addSubjectKeyMethod, subjAKeyAddr, subjCPrimaryKey.PublicKey().Bytes())
invoker.InvokeFail(t, "key is occupied", addSubjectKeyMethod, subjBKeyAddr, subjCPrimaryKey.PublicKey().Bytes())
invoker.Invoke(t, stackitem.Null{}, addSubjectKeyMethod, subjAKeyAddr, subjDPrimaryKey.PublicKey().Bytes())
invoker.InvokeFail(t, "key is occupied", addSubjectKeyMethod, subjBKeyAddr, subjDPrimaryKey.PublicKey().Bytes())
invoker.Invoke(t, stackitem.Null{}, removeSubjectKeyMethod, subjAKeyAddr, subjDPrimaryKey.PublicKey().Bytes())
invoker.Invoke(t, stackitem.Null{}, addSubjectKeyMethod, subjBKeyAddr, subjDPrimaryKey.PublicKey().Bytes())
invoker.InvokeFail(t, "key is occupied", addSubjectKeyMethod, subjAKeyAddr, subjDPrimaryKey.PublicKey().Bytes())
invoker.Invoke(t, stackitem.Null{}, deleteSubjectMethod, subjBKeyAddr)
invoker.Invoke(t, stackitem.Null{}, addSubjectKeyMethod, subjAKeyAddr, subjDPrimaryKey.PublicKey().Bytes())
invoker.Invoke(t, stackitem.Null{}, createSubjectMethod, defaultNamespace, subjFPrimaryKey.PublicKey().Bytes())
invoker.Invoke(t, stackitem.Null{}, deleteSubjectMethod, subjFPrimaryKey.PublicKey().GetScriptHash())
invoker.Invoke(t, stackitem.Null{}, createSubjectMethod, defaultNamespace, subjFPrimaryKey.PublicKey().Bytes())
}
func checkPublicKeyResult(t *testing.T, s *vm.Stack, err error, key *keys.PrivateKey) { func checkPublicKeyResult(t *testing.T, s *vm.Stack, err error, key *keys.PrivateKey) {
if key == nil { if key == nil {
require.ErrorContains(t, err, "not found") require.ErrorContains(t, err, "not found")

View file

@ -177,8 +177,10 @@ func TestNNSRegister(t *testing.T) {
cAcc := c.WithSigners(acc) cAcc := c.WithSigners(acc)
expected = stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte("testdomain.com")), expected = stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(nns.TXT)))}) stackitem.NewByteArray([]byte("testdomain.com")),
stackitem.NewBigInteger(big.NewInt(int64(nns.TXT))),
})
tx = cAcc.Invoke(t, stackitem.Null{}, "addRecord", tx = cAcc.Invoke(t, stackitem.Null{}, "addRecord",
"testdomain.com", int64(nns.TXT), "first TXT record") "testdomain.com", int64(nns.TXT), "first TXT record")
c.CheckTxNotificationEvent(t, tx, 0, state.NotificationEvent{ScriptHash: c.Hash, Name: "AddRecord", Item: expected}) c.CheckTxNotificationEvent(t, tx, 0, state.NotificationEvent{ScriptHash: c.Hash, Name: "AddRecord", Item: expected})
@ -194,8 +196,10 @@ func TestNNSRegister(t *testing.T) {
}) })
c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.TXT)) c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.TXT))
expected = stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte("testdomain.com")), expected = stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(nns.TXT)))}) stackitem.NewByteArray([]byte("testdomain.com")),
stackitem.NewBigInteger(big.NewInt(int64(nns.TXT))),
})
tx = cAcc.Invoke(t, stackitem.Null{}, "setRecord", tx = cAcc.Invoke(t, stackitem.Null{}, "setRecord",
"testdomain.com", int64(nns.TXT), int64(0), "replaced first") "testdomain.com", int64(nns.TXT), int64(0), "replaced first")
c.CheckTxNotificationEvent(t, tx, 0, state.NotificationEvent{ScriptHash: c.Hash, Name: "AddRecord", Item: expected}) c.CheckTxNotificationEvent(t, tx, 0, state.NotificationEvent{ScriptHash: c.Hash, Name: "AddRecord", Item: expected})
@ -207,8 +211,10 @@ func TestNNSRegister(t *testing.T) {
c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.TXT)) c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.TXT))
tx = cAcc.Invoke(t, stackitem.Null{}, "deleteRecords", "testdomain.com", int64(nns.TXT)) tx = cAcc.Invoke(t, stackitem.Null{}, "deleteRecords", "testdomain.com", int64(nns.TXT))
expected = stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte("testdomain.com")), expected = stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(nns.TXT)))}) stackitem.NewByteArray([]byte("testdomain.com")),
stackitem.NewBigInteger(big.NewInt(int64(nns.TXT))),
})
c.CheckTxNotificationEvent(t, tx, 0, state.NotificationEvent{ScriptHash: c.Hash, Name: "DeleteRecords", Item: expected}) c.CheckTxNotificationEvent(t, tx, 0, state.NotificationEvent{ScriptHash: c.Hash, Name: "DeleteRecords", Item: expected})
c.Invoke(t, stackitem.Null{}, "getRecords", "testdomain.com", int64(nns.TXT)) c.Invoke(t, stackitem.Null{}, "getRecords", "testdomain.com", int64(nns.TXT))
@ -255,8 +261,10 @@ func TestNNSRegister(t *testing.T) {
c.Invoke(t, stackitem.Null{}, "getRecords", "testdomain.com", int64(nns.TXT)) c.Invoke(t, stackitem.Null{}, "getRecords", "testdomain.com", int64(nns.TXT))
tx = cAcc.Invoke(t, stackitem.Null{}, "deleteDomain", "testdomain.com") tx = cAcc.Invoke(t, stackitem.Null{}, "deleteDomain", "testdomain.com")
expected = stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte("testdomain.com")), expected = stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(nns.CNAME)))}) stackitem.NewByteArray([]byte("testdomain.com")),
stackitem.NewBigInteger(big.NewInt(int64(nns.CNAME))),
})
c.CheckTxNotificationEvent(t, tx, 0, state.NotificationEvent{ScriptHash: c.Hash, Name: "DeleteRecords", Item: expected}) c.CheckTxNotificationEvent(t, tx, 0, state.NotificationEvent{ScriptHash: c.Hash, Name: "DeleteRecords", Item: expected})
expected = stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte("testdomain.com"))}) expected = stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte("testdomain.com"))})
@ -371,6 +379,7 @@ func TestGlobalDomain(t *testing.T) {
c.InvokeFail(t, "global domain is already taken", "isAvailable", "dom.testdomain.com") c.InvokeFail(t, "global domain is already taken", "isAvailable", "dom.testdomain.com")
} }
func TestTLDRecord(t *testing.T) { func TestTLDRecord(t *testing.T) {
c := newNNSInvoker(t, true) c := newNNSInvoker(t, true)
c.Invoke(t, stackitem.Null{}, "addRecord", c.Invoke(t, stackitem.Null{}, "addRecord",