diff --git a/CHANGELOG.md b/CHANGELOG.md index 341be9a1..cc0d7171 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,12 @@ Changelog for NeoFS Contract ## [Unreleased] ### Added +- Structured containers to Container contract storage (#534) +- `createV2` and `getInfo` methods to Container contract (#534) ### Changed - Initial GAS distributed to alphabet members during deploy is raised from 300 to 10000 (#529) +- Container contract no longer creates or reads container protobuf storage items (#XXX) ### Updated - NeoGo dependency to 0.113.0 (#521) diff --git a/contracts/container/config.yml b/contracts/container/config.yml index 3d831164..fb3b545c 100644 --- a/contracts/container/config.yml +++ b/contracts/container/config.yml @@ -2,7 +2,7 @@ name: "NeoFS Container" supportedstandards: ["NEP-22"] safemethods: ["alias", "count", "containersOf", "get", "owner", "nodes", "replicasNumbers", "verifyPlacementSignatures", "eACL", "version", "getContainerData", "getEACLData", "getNodeReportSummary", "getReportByNode", - "iterateReports", "iterateAllReportSummaries", "containerQuota", "userQuota", "getTakenSpaceByUser"] + "iterateReports", "iterateAllReportSummaries", "containerQuota", "userQuota", "getTakenSpaceByUser", "getInfo"] permissions: - methods: ["update", "addKey", "transferX", "register", "registerTLD", "addRecord", "deleteRecords", "unsubscribeFromNewEpoch"] diff --git a/contracts/container/contract.go b/contracts/container/contract.go index 7e4a2e74..a5c8933b 100644 --- a/contracts/container/contract.go +++ b/contracts/container/contract.go @@ -13,9 +13,32 @@ import ( "github.com/nspcc-dev/neofs-contract/common" cst "github.com/nspcc-dev/neofs-contract/contracts/container/containerconst" "github.com/nspcc-dev/neofs-contract/contracts/nns/recordtype" + iproto "github.com/nspcc-dev/neofs-contract/internal/proto" ) type ( + // APIVersion represents NeoFS API protocol version. + APIVersion struct { + Major uint32 + Minor uint32 + } + + // Attribute represents container attribute. + Attribute struct { + Key string + Value string + } + + // Info represents information about container. + Info struct { + Version APIVersion + Owner []byte // 25 bytes address + Nonce []byte // 16 byte UUID + BasicACL uint32 + Attributes []Attribute + StoragePolicy []byte + } + StorageNode struct { Info []byte } @@ -91,6 +114,7 @@ const ( containerQuotaKeyPrefix = 'a' userQuotaKeyPrefix = 'b' userLoadKeyPrefix = 'e' + infoPrefix = 0 // default SOA record field values. defaultRefresh = 3600 // 1 hour @@ -155,6 +179,20 @@ func _deploy(data any, isUpdate bool) { common.UnsubscribeFromNewEpoch() } + if version < 26_000 { + it := storage.Find(ctx, []byte{containerKeyPrefix}, storage.RemovePrefix) + for iterator.Next(it) { + item := iterator.Value(it).(struct{ key, val []byte }) + storage.Put(ctx, append([]byte{infoPrefix}, item.key...), std.Serialize(fromBytes(item.val))) + } + } + + if version < 27_000 { + for it := storage.Find(ctx, []byte{containerKeyPrefix}, storage.KeysOnly); iterator.Next(it); { + storage.Delete(ctx, iterator.Value(it).([]byte)) + } + } + return } @@ -327,7 +365,7 @@ func requireMapValue(m map[string]any, key string) any { // meta-information be handled and notified using the chain. If name and // zone are non-empty strings, it behaves the same as [PutNamed]; empty // strings make a regular [Put] call. -// Deprecated: use [Create] instead. +// Deprecated: use [CreateV2] instead. func PutMeta(container []byte, signature interop.Signature, publicKey interop.PublicKey, token []byte, name, zone string, metaOnChain bool) { if metaOnChain { ctx := storage.GetContext() @@ -345,21 +383,21 @@ func PutMeta(container []byte, signature interop.Signature, publicKey interop.Pu // PublicKey contains the public key of the signer. // Token is optional and should be a stable marshaled SessionToken structure from // API. -// Deprecated: use [Create] instead. +// Deprecated: use [CreateV2] instead. func Put(container []byte, signature interop.Signature, publicKey interop.PublicKey, token []byte) { PutNamed(container, signature, publicKey, token, "", "") } // PutNamedOverloaded is the same as [Put] (and exposed as put from the contract via // overload), but allows named container creation via NNS contract. -// Deprecated: use [Create] instead. +// Deprecated: use [CreateV2] instead. func PutNamedOverloaded(container []byte, signature interop.Signature, publicKey interop.PublicKey, token []byte, name, zone string) { PutNamed(container, signature, publicKey, token, name, zone) } // PutNamed is similar to put but also sets a TXT record in nns contract. // Note that zone must exist. -// DEPRECATED: use [Create] instead. +// DEPRECATED: use [CreateV2] instead. func PutNamed(container []byte, signature interop.Signature, publicKey interop.PublicKey, token []byte, name, zone string) { @@ -464,6 +502,8 @@ func PutNamed(container []byte, signature interop.Signature, // users' requests. IR verifies requests and approves them via multi-signature. // Once creation is approved, container is persisted and becomes accessible. // Credentials are disposable and do not persist in the chain. +// +// Deprecated: use [CreateV2] with instead. func Create(cnr []byte, invocScript, verifScript, sessionToken []byte, name, zone string, metaOnChain bool) { owner := ownerFromBinaryContainer(cnr) alphabet := common.AlphabetNodes() @@ -516,8 +556,8 @@ func Create(cnr []byte, invocScript, verifScript, sessionToken []byte, name, zon } } + storage.Put(ctx, append([]byte{infoPrefix}, id...), std.Serialize(fromBytes(cnr))) storage.Put(ctx, append(append([]byte{ownerKeyPrefix}, owner...), id...), id) - storage.Put(ctx, append([]byte{containerKeyPrefix}, id...), cnr) if metaOnChain { storage.Put(ctx, append([]byte{containersWithMetaPrefix}, id...), []byte{}) } @@ -525,6 +565,106 @@ func Create(cnr []byte, invocScript, verifScript, sessionToken []byte, name, zon runtime.Notify("Created", id, owner) } +// CreateV2 creates container with given info in the contract. Created +// containers are content-addressed: they may be accessed by SHA-256 checksum of +// their data. On success, CreateV2 throws 'Created' notification event. +// +// Created containers are disposable: if they are deleted, they cannot be +// created again. CreateV2 throws [cst.ErrorDeleted] exception on recreation +// attempts. +// +// If '__NEOFS__NAME' and '__NEOFS__ZONE' attributes are set, they are used to +// register 'name.zone' domain for given container. Domain zone is optional: it +// defaults to the 6th contract deployment parameter which itself defaults to +// 'container'. +// +// If '__NEOFS__METAINFO_CONSISTENCY' attribute is set, meta information about +// objects from this container can be collected for it. +// +// The operation is paid. Container owner pays per-container fee (global chain +// configuration) to each committee member. If domain registration is requested, +// additional alias fee (also a configuration) is added to each payment. +// +// CreateV2 must have chain's committee multi-signature witness. Invocation +// script, verification script and session token parameters are owner +// credentials. They are transmitted in notary transactions carrying original +// users' requests. IR verifies requests and approves them via multi-signature. +// Once creation is approved, container is persisted and becomes accessible. +// Credentials are disposable and do not persist in the chain. +func CreateV2(cnr Info, invocScript, verifScript, sessionToken []byte) { + alphabet := common.AlphabetNodes() + if !runtime.CheckWitness(common.Multiaddress(alphabet, false)) { + panic(common.ErrAlphabetWitnessFailed) + } + + ctx := storage.GetContext() + cnrBytes := toBytes(cnr) + id := crypto.Sha256(cnrBytes) + if storage.Get(ctx, append([]byte{deletedKeyPrefix}, id...)) != nil { + panic(cst.ErrorDeleted) + } + + var name, zone string + var metaOnChain bool + for i := range cnr.Attributes { + switch cnr.Attributes[i].Key { + case "__NEOFS__NAME": + name = cnr.Attributes[i].Value + case "__NEOFS__ZONE": + zone = cnr.Attributes[i].Value + case "__NEOFS__METAINFO_CONSISTENCY": + metaOnChain = true + } + } + + if name != "" { + if zone == "" { + zone = storage.Get(ctx, nnsRootKey).(string) + } + nnsContract := storage.Get(ctx, nnsContractKey).(interop.Hash160) + domain := name + "." + zone + + if checkNiceNameAvailable(nnsContract, domain) { + if !contract.Call(nnsContract, "register", contract.All, domain, runtime.GetExecutingScriptHash(), + "ops@nspcc.ru", defaultRefresh, defaultRetry, defaultExpire, defaultTTL).(bool) { + panic("domain registration failed") + } + } + + contract.Call(nnsContract, "addRecord", contract.All, domain, recordtype.TXT, std.Base58Encode(id)) + + storage.Put(ctx, append([]byte(nnsHasAliasKey), id...), domain) + } + + netmapContract := storage.Get(ctx, netmapContractKey).(interop.Hash160) + fee := contract.Call(netmapContract, "config", contract.ReadOnly, cst.RegistrationFeeKey).(int) + if name != "" { + fee += contract.Call(netmapContract, "config", contract.ReadOnly, cst.AliasFeeKey).(int) + } + if fee > 0 { + ownerAcc := common.WalletToScriptHash(cnr.Owner[:]) + balanceContract := storage.Get(ctx, balanceContractKey).(interop.Hash160) + ownerBalance := contract.Call(balanceContract, "balanceOf", contract.ReadOnly, ownerAcc).(int) + if ownerBalance < fee*len(alphabet) { + panic("insufficient balance to create container") + } + + transferDetails := common.ContainerFeeTransferDetails(id) + for i := range alphabet { + contract.Call(balanceContract, "transferX", contract.All, + ownerAcc, contract.CreateStandardAccount(alphabet[i]), fee, transferDetails) + } + } + + storage.Put(ctx, append([]byte{infoPrefix}, id...), std.Serialize(cnr)) + storage.Put(ctx, append(append([]byte{ownerKeyPrefix}, cnr.Owner[:]...), id...), id) + if metaOnChain { + storage.Put(ctx, append([]byte{containersWithMetaPrefix}, id...), []byte{}) + } + + runtime.Notify("Created", id, cnr.Owner[:]) +} + // checkNiceNameAvailable checks if the nice name is available for the container. // It panics if the name is taken. Returned value specifies if new domain registration is needed. func checkNiceNameAvailable(nnsContractAddr interop.Hash160, domain string) bool { @@ -584,7 +724,7 @@ func Delete(containerID []byte, signature interop.Signature, token []byte) { // container does not exist. On success, Remove throws 'Removed' notification // event. // -// See [Create] for details. +// See [CreateV2] for details. func Remove(id []byte, invocScript, verifScript, sessionToken []byte) { common.CheckAlphabetWitness() @@ -593,7 +733,7 @@ func Remove(id []byte, invocScript, verifScript, sessionToken []byte) { } ctx := storage.GetContext() - cnrItemKey := append([]byte{containerKeyPrefix}, id...) + cnrItemKey := append([]byte{infoPrefix}, id...) cnrItem := storage.Get(ctx, cnrItemKey) if cnrItem == nil { return @@ -629,31 +769,44 @@ func deleteNNSRecords(ctx storage.Context, domain string) { contract.Call(nnsContractAddr, "deleteRecords", contract.All, domain, recordtype.TXT) } +// GetInfo reads container by ID. If the container is missing, GetInfo throws +// [cst.NotFoundError] exception. +func GetInfo(id interop.Hash256) Info { + val := storage.Get(storage.GetReadOnlyContext(), append([]byte{infoPrefix}, id...)) + if val == nil { + panic(cst.NotFoundError) + } + return std.Deserialize(val.([]byte)).(Info) +} + // Get method returns a structure that contains a stable marshaled Container structure, // the signature, the public key of the container creator and a stable marshaled SessionToken // structure if it was provided. // // If the container doesn't exist, it panics with NotFoundError. -// Deprecated: use [GetContainerData] instead. +// +// Deprecated: use [GetInfo] instead. func Get(containerID []byte) Container { ctx := storage.GetReadOnlyContext() - cnt := getContainer(ctx, containerID) - if len(cnt.Value) == 0 { + val := storage.Get(ctx, append([]byte{infoPrefix}, containerID...)) + if val == nil { panic(cst.NotFoundError) } - return cnt + return Container{Value: toBytes(std.Deserialize(val.([]byte)).(Info)), Sig: interop.Signature{}, Pub: interop.PublicKey{}, Token: []byte{}} } // GetContainerData returns binary of the container it was created with by ID. // // If the container is missing, GetContainerData throws [cst.NotFoundError] // exception. +// +// Deprecated: use [GetInfo] instead. func GetContainerData(id []byte) []byte { - cnr := storage.Get(storage.GetReadOnlyContext(), append([]byte{containerKeyPrefix}, id...)) - if cnr == nil { + val := storage.Get(storage.GetReadOnlyContext(), append([]byte{infoPrefix}, id...)) + if val == nil { panic(cst.NotFoundError) } - return cnr.([]byte) + return toBytes(std.Deserialize(val.([]byte)).(Info)) } // Owner method returns a 25 byte Owner ID of the container. @@ -685,7 +838,7 @@ func Alias(cid []byte) string { func Count() int { count := 0 ctx := storage.GetReadOnlyContext() - it := storage.Find(ctx, []byte{containerKeyPrefix}, storage.KeysOnly) + it := storage.Find(ctx, []byte{infoPrefix}, storage.KeysOnly) for iterator.Next(it) { count++ } @@ -978,7 +1131,7 @@ func SetEACL(eACL []byte, signature interop.Signature, publicKey interop.PublicK // [cst.NotFoundError] exception. On success, PutEACL throws 'EACLChanged' // notification event. // -// See [Create] for details. +// See [CreateV2] for details. func PutEACL(eACL []byte, invocScript, verifScript, sessionToken []byte) { common.CheckAlphabetWitness() @@ -994,7 +1147,7 @@ func PutEACL(eACL []byte, invocScript, verifScript, sessionToken []byte) { id := eACL[idOff : idOff+containerIDSize] ctx := storage.GetContext() - if storage.Get(ctx, append([]byte{containerKeyPrefix}, id...)) == nil { + if storage.Get(ctx, append([]byte{infoPrefix}, id...)) == nil { panic(cst.NotFoundError) } @@ -1027,7 +1180,7 @@ func EACL(containerID []byte) ExtendedACL { // exception. func GetEACLData(id []byte) []byte { ctx := storage.GetReadOnlyContext() - if storage.Get(ctx, append([]byte{containerKeyPrefix}, id...)) == nil { + if storage.Get(ctx, append([]byte{infoPrefix}, id...)) == nil { panic(cst.NotFoundError) } @@ -1362,8 +1515,7 @@ func addContainer(ctx storage.Context, id, owner, container []byte) { containerListKey = append(containerListKey, id...) storage.Put(ctx, containerListKey, id) - idKey := append([]byte{containerKeyPrefix}, id...) - storage.Put(ctx, idKey, container) + storage.Put(ctx, append([]byte{infoPrefix}, id...), std.Serialize(fromBytes(container))) } func removeContainer(ctx storage.Context, id []byte, owner []byte) { @@ -1397,7 +1549,7 @@ func removeContainer(ctx storage.Context, id []byte, owner []byte) { storage.Delete(ctx, containerQuotaKey(id)) storage.Delete(ctx, summaryKey) - storage.Delete(ctx, append([]byte{containerKeyPrefix}, id...)) + storage.Delete(ctx, append([]byte{infoPrefix}, id...)) storage.Delete(ctx, append([]byte{containersWithMetaPrefix}, id...)) storage.Delete(ctx, append(eACLPrefix, id...)) storage.Put(ctx, append([]byte{deletedKeyPrefix}, id...), []byte{}) @@ -1413,22 +1565,13 @@ func getEACL(ctx storage.Context, cid []byte) ExtendedACL { return ExtendedACL{Value: []byte{}, Sig: interop.Signature{}, Pub: interop.PublicKey{}, Token: []byte{}} } -func getContainer(ctx storage.Context, cid []byte) Container { - data := storage.Get(ctx, append([]byte{containerKeyPrefix}, cid...)) - if data != nil { - return Container{Value: data.([]byte), Sig: interop.Signature{}, Pub: interop.PublicKey{}, Token: []byte{}} - } - - return Container{Value: []byte{}, Sig: interop.Signature{}, Pub: interop.PublicKey{}, Token: []byte{}} -} - func getOwnerByID(ctx storage.Context, cid []byte) []byte { - container := getContainer(ctx, cid) - if len(container.Value) == 0 { + val := storage.Get(ctx, append([]byte{infoPrefix}, cid...)) + if val == nil { return nil } - return ownerFromBinaryContainer(container.Value) + return std.Deserialize(val.([]byte)).(Info).Owner } func ownerFromBinaryContainer(container []byte) []byte { @@ -1479,3 +1622,337 @@ func userTotalStorageKey(user []byte) []byte { return key } + +const ( + fieldAPIVersion = 1 + fieldOwner = 2 + fieldNonce = 3 + fieldBasicACL = 4 + fieldAttribute = 5 + fieldPolicy = 6 + + fieldAPIVersionMajor = 1 + fieldAPIVersionMinor = 2 + + fieldOwnerVal = 1 + + fieldAttributeKey = 1 + fieldAttributeValue = 2 + + nonceLen = 16 +) + +func toBytes(cnr Info) []byte { + versionLen := iproto.SizeTag(fieldAPIVersionMajor) + iproto.SizeVarint(uint64(cnr.Version.Major)) + + iproto.SizeTag(fieldAPIVersionMinor) + iproto.SizeVarint(uint64(cnr.Version.Minor)) + + ownerLen := iproto.SizeTag(fieldOwnerVal) + iproto.SizeLEN(len(cnr.Owner)) + + attributeKeyTagLen := iproto.SizeTag(fieldAttributeKey) + attributeValTagLen := iproto.SizeTag(fieldAttributeValue) + + fullLen := iproto.SizeTag(fieldAPIVersion) + iproto.SizeLEN(versionLen) + + iproto.SizeTag(fieldOwner) + iproto.SizeLEN(ownerLen) + + iproto.SizeTag(fieldNonce) + iproto.SizeLEN(len(cnr.Nonce)) + + iproto.SizeTag(fieldBasicACL) + iproto.SizeVarint(uint64(cnr.BasicACL)) + + len(cnr.Attributes)*iproto.SizeTag(fieldAttribute) + + iproto.SizeTag(fieldPolicy) + iproto.SizeLEN(len(cnr.StoragePolicy)) + + attrLens := make([]int, len(cnr.Attributes)) + for i := range cnr.Attributes { + attrLens[i] = attributeKeyTagLen + iproto.SizeLEN(len(cnr.Attributes[i].Key)) + + attributeValTagLen + iproto.SizeLEN(len(cnr.Attributes[i].Value)) + fullLen += iproto.SizeLEN(attrLens[i]) + } + + b := make([]byte, fullLen) + + // version + off := iproto.PutUvarint(b, 0, iproto.EncodeTag(fieldAPIVersion, iproto.FieldTypeLEN)) + off += iproto.PutUvarint(b, off, uint64(versionLen)) + off += iproto.PutUvarint(b, off, iproto.EncodeTag(fieldAPIVersionMajor, iproto.FieldTypeVARINT)) + off += iproto.PutUvarint(b, off, uint64(cnr.Version.Major)) + off += iproto.PutUvarint(b, off, iproto.EncodeTag(fieldAPIVersionMinor, iproto.FieldTypeVARINT)) + off += iproto.PutUvarint(b, off, uint64(cnr.Version.Minor)) + + // owner + off += iproto.PutUvarint(b, off, iproto.EncodeTag(fieldOwner, iproto.FieldTypeLEN)) + off += iproto.PutUvarint(b, off, uint64(ownerLen)) + off += iproto.PutUvarint(b, off, iproto.EncodeTag(fieldOwnerVal, iproto.FieldTypeLEN)) + off += iproto.PutUvarint(b, off, uint64(len(cnr.Owner))) + off += copy(b[off:], cnr.Owner[:]) + + // nonce + off += iproto.PutUvarint(b, off, iproto.EncodeTag(fieldNonce, iproto.FieldTypeLEN)) + off += iproto.PutUvarint(b, off, uint64(len(cnr.Nonce))) + off += copy(b[off:], cnr.Nonce[:]) + + // basic ACL + off += iproto.PutUvarint(b, off, iproto.EncodeTag(fieldBasicACL, iproto.FieldTypeVARINT)) + off += iproto.PutUvarint(b, off, uint64(cnr.BasicACL)) + + // attributes + for i := range cnr.Attributes { + off += iproto.PutUvarint(b, off, iproto.EncodeTag(fieldAttribute, iproto.FieldTypeLEN)) + off += iproto.PutUvarint(b, off, uint64(attrLens[i])) + // key + off += iproto.PutUvarint(b, off, iproto.EncodeTag(fieldAttributeKey, iproto.FieldTypeLEN)) + off += iproto.PutUvarint(b, off, uint64(len(cnr.Attributes[i].Key))) + off += copy(b[off:], []byte(cnr.Attributes[i].Key)) + // value + off += iproto.PutUvarint(b, off, iproto.EncodeTag(fieldAttributeValue, iproto.FieldTypeLEN)) + off += iproto.PutUvarint(b, off, uint64(len(cnr.Attributes[i].Value))) + off += copy(b[off:], []byte(cnr.Attributes[i].Value)) + } + + // policy + off += iproto.PutUvarint(b, off, iproto.EncodeTag(fieldPolicy, iproto.FieldTypeLEN)) + off += iproto.PutUvarint(b, off, uint64(len(cnr.StoragePolicy))) + copy(b[off:], cnr.StoragePolicy) + + return b +} + +func fromBytes(b []byte) Info { + var res Info + var e string + var fieldNum, fieldTyp, off, read, nestedLen int + + for off < len(b) { + fieldNum, fieldTyp, read, e = iproto.ReadTag(b[off:]) + if e != "" { + panic("read next field tag: " + e) + } + + switch fieldNum { + case fieldAPIVersion: + iproto.AssertFieldType(fieldNum, fieldTyp, iproto.FieldTypeLEN) + + off += read + + if nestedLen, read, e = iproto.ReadSizeLEN(b[off:]); e != "" { + panic("read 'version' field len: " + e) + } + + off += read + + if res.Version, e = apiVersionFromBytes(b[off : off+nestedLen]); e != "" { + panic("decode 'version' field: " + e) + } + + off += nestedLen + case fieldOwner: + iproto.AssertFieldType(fieldNum, fieldTyp, iproto.FieldTypeLEN) + + off += read + + if nestedLen, read, e = iproto.ReadSizeLEN(b[off:]); e != "" { + panic("read 'owner_id' field len: " + e) + } + + off += read + + if res.Owner, e = userIDFromBytes(b[off : off+nestedLen]); e != "" { + panic("decode 'owner_id' field: " + e) + } + + off += nestedLen + case fieldNonce: + iproto.AssertFieldType(fieldNum, fieldTyp, iproto.FieldTypeLEN) + + off += read + + if nestedLen, read, e = iproto.ReadSizeLEN(b[off:]); e != "" { + panic("read 'nonce' field len: " + e) + } + if nestedLen != nonceLen { + panic("wrong 'nonce' field len: expected " + std.Itoa10(common.NeoFSUserAccountLength) + ", got " + std.Itoa10(nestedLen)) + } + + off += read + + res.Nonce = b[off : off+nestedLen] + + off += nestedLen + case fieldBasicACL: + iproto.AssertFieldType(fieldNum, fieldTyp, iproto.FieldTypeVARINT) + + off += read + + if res.BasicACL, read, e = iproto.ReadUint32(b[off:]); e != "" { + panic("read 'basic_acl' field: " + e) + } + off += read + case fieldAttribute: + iproto.AssertFieldType(fieldNum, fieldTyp, iproto.FieldTypeLEN) + + off += read + + if nestedLen, read, e = iproto.ReadSizeLEN(b[off:]); e != "" { + panic("read 'attribute' field len: " + e) + } + + off += read + + var attr Attribute + if attr, e = attributeFromBytes(b[off : off+nestedLen]); e != "" { + panic("read 'attribute' field: " + e) + } + + res.Attributes = append(res.Attributes, attr) + + off += nestedLen + case fieldPolicy: + iproto.AssertFieldType(fieldNum, fieldTyp, iproto.FieldTypeLEN) + + off += read + + if nestedLen, read, e = iproto.ReadSizeLEN(b[off:]); e != "" { + panic("read 'placement_policy' field len: " + e) + } + + off += read + + res.StoragePolicy = b[off : off+nestedLen] + + off += nestedLen + default: + panic("unsupported container field #" + std.Itoa10(fieldNum)) + } + } + + return res +} + +func apiVersionFromBytes(b []byte) (APIVersion, string) { + var res APIVersion + var e string + var fieldNum, fieldTyp, off, read int + + for off < len(b) { + fieldNum, fieldTyp, read, e = iproto.ReadTag(b[off:]) + if e != "" { + return APIVersion{}, "read next field tag: " + e + } + + switch fieldNum { + case fieldAPIVersionMajor: + if e = iproto.CheckFieldType(fieldNum, fieldTyp, iproto.FieldTypeVARINT); e != "" { + return APIVersion{}, e + } + + off += read + + if res.Major, read, e = iproto.ReadUint32(b[off:]); e != "" { + return APIVersion{}, "read 'major' field: " + e + } + off += read + case fieldAPIVersionMinor: + if e = iproto.CheckFieldType(fieldNum, fieldTyp, iproto.FieldTypeVARINT); e != "" { + return APIVersion{}, e + } + + off += read + + if res.Minor, read, e = iproto.ReadUint32(b[off:]); e != "" { + return APIVersion{}, "read 'minor' field: " + e + } + off += read + default: + return APIVersion{}, "unsupported field #" + std.Itoa10(fieldNum) + } + } + + return res, "" +} + +func userIDFromBytes(b []byte) ([]byte, string) { + var res []byte + var e string + var fieldNum, fieldTyp, off, read, nestedLen int + + for off < len(b) { + fieldNum, fieldTyp, read, e = iproto.ReadTag(b[off:]) + if e != "" { + return nil, "read next field tag: " + e + } + + switch fieldNum { + case fieldOwnerVal: + if e = iproto.CheckFieldType(fieldNum, fieldTyp, iproto.FieldTypeLEN); e != "" { + return nil, e + } + + off += read + + if nestedLen, read, e = iproto.ReadSizeLEN(b[off:]); e != "" { + return nil, "read 'value' field len: " + e + } + if nestedLen != common.NeoFSUserAccountLength { + return nil, "wrong 'value' field len: expected " + std.Itoa10(common.NeoFSUserAccountLength) + ", got " + std.Itoa10(nestedLen) + } + + off += read + + res = b[off : off+nestedLen] + + off += nestedLen + default: + return nil, "unsupported field #" + std.Itoa10(fieldNum) + } + } + + return res, "" +} + +func attributeFromBytes(b []byte) (Attribute, string) { + var res Attribute + var e string + var fieldNum, fieldTyp, off, read, nestedLen int + + for off < len(b) { + fieldNum, fieldTyp, read, e = iproto.ReadTag(b[off:]) + if e != "" { + return Attribute{}, "read next field tag: " + e + } + + switch fieldNum { + case fieldAttributeKey: + if e = iproto.CheckFieldType(fieldNum, fieldTyp, iproto.FieldTypeLEN); e != "" { + return Attribute{}, e + } + + off += read + + if nestedLen, read, e = iproto.ReadSizeLEN(b[off:]); e != "" { + return Attribute{}, "read 'key' field len: " + e + } + + off += read + + res.Key = string(b[off : off+nestedLen]) + + off += nestedLen + case fieldAttributeValue: + if e = iproto.CheckFieldType(fieldNum, fieldTyp, iproto.FieldTypeLEN); e != "" { + return Attribute{}, e + } + + off += read + + if nestedLen, read, e = iproto.ReadSizeLEN(b[off:]); e != "" { + return Attribute{}, "read 'value' field len: " + e + } + + off += read + + res.Value = string(b[off : off+nestedLen]) + + off += nestedLen + default: + return Attribute{}, "unsupported field #" + std.Itoa10(fieldNum) + } + } + + return res, "" +} diff --git a/contracts/container/contract.nef b/contracts/container/contract.nef index f4454317..4246e999 100755 Binary files a/contracts/container/contract.nef and b/contracts/container/contract.nef differ diff --git a/contracts/container/doc.go b/contracts/container/doc.go index 55e1aa61..979e3a81 100644 --- a/contracts/container/doc.go +++ b/contracts/container/doc.go @@ -104,6 +104,8 @@ Key-value storage format: NNS root domain zone for containers - 'x' -> []byte container descriptors encoded into NeoFS API binary protocol format + - '0' -> std.Serialize(Info) + container descriptors - 'd' -> "" deleted container IDs with no values for replay protection - 'o' -> diff --git a/contracts/container/manifest.json b/contracts/container/manifest.json index 9a8c923a..6315ad06 100755 --- a/contracts/container/manifest.json +++ b/contracts/container/manifest.json @@ -1 +1 @@ -{"name":"NeoFS Container","abi":{"methods":[{"name":"_initialize","offset":0,"parameters":[],"returntype":"Void","safe":false},{"name":"_deploy","offset":112,"parameters":[{"name":"data","type":"Any"},{"name":"isUpdate","type":"Boolean"}],"returntype":"Void","safe":false},{"name":"addNextEpochNodes","offset":6033,"parameters":[{"name":"cID","type":"Hash256"},{"name":"placementVector","type":"Integer"},{"name":"publicKeys","type":"Array"}],"returntype":"Void","safe":false},{"name":"alias","offset":5839,"parameters":[{"name":"cid","type":"ByteArray"}],"returntype":"String","safe":true},{"name":"commitContainerListUpdate","offset":6641,"parameters":[{"name":"cID","type":"Hash256"},{"name":"replicas","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"containerQuota","offset":9880,"parameters":[{"name":"cID","type":"Hash256"}],"returntype":"Array","safe":true},{"name":"containersOf","offset":5979,"parameters":[{"name":"owner","type":"ByteArray"}],"returntype":"InteropInterface","safe":true},{"name":"count","offset":5934,"parameters":[],"returntype":"Integer","safe":true},{"name":"create","offset":3956,"parameters":[{"name":"cnr","type":"ByteArray"},{"name":"invocScript","type":"ByteArray"},{"name":"verifScript","type":"ByteArray"},{"name":"sessionToken","type":"ByteArray"},{"name":"name","type":"String"},{"name":"zone","type":"String"},{"name":"metaOnChain","type":"Boolean"}],"returntype":"Void","safe":false},{"name":"delete","offset":5040,"parameters":[{"name":"containerID","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"token","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"eACL","offset":8186,"parameters":[{"name":"containerID","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"get","offset":5650,"parameters":[{"name":"containerID","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"getContainerData","offset":5712,"parameters":[{"name":"id","type":"ByteArray"}],"returntype":"ByteArray","safe":true},{"name":"getEACLData","offset":8244,"parameters":[{"name":"id","type":"ByteArray"}],"returntype":"ByteArray","safe":true},{"name":"getNodeReportSummary","offset":9202,"parameters":[{"name":"cid","type":"Hash256"}],"returntype":"Array","safe":true},{"name":"getReportByNode","offset":9306,"parameters":[{"name":"cid","type":"Hash256"},{"name":"pubKey","type":"PublicKey"}],"returntype":"Array","safe":true},{"name":"getTakenSpaceByUser","offset":9162,"parameters":[{"name":"user","type":"ByteArray"}],"returntype":"Integer","safe":true},{"name":"iterateAllReportSummaries","offset":9507,"parameters":[],"returntype":"InteropInterface","safe":true},{"name":"iterateReports","offset":9431,"parameters":[{"name":"cid","type":"Hash256"}],"returntype":"InteropInterface","safe":true},{"name":"nodes","offset":7577,"parameters":[{"name":"cID","type":"Hash256"},{"name":"placementVector","type":"Integer"}],"returntype":"InteropInterface","safe":true},{"name":"onNEP11Payment","offset":1735,"parameters":[{"name":"a","type":"Hash160"},{"name":"b","type":"Integer"},{"name":"c","type":"ByteArray"},{"name":"d","type":"Any"}],"returntype":"Void","safe":false},{"name":"owner","offset":5788,"parameters":[{"name":"containerID","type":"ByteArray"}],"returntype":"ByteArray","safe":true},{"name":"put","offset":3120,"parameters":[{"name":"container","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"putEACL","offset":7940,"parameters":[{"name":"eACL","type":"ByteArray"},{"name":"invocScript","type":"ByteArray"},{"name":"verifScript","type":"ByteArray"},{"name":"sessionToken","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"put","offset":3057,"parameters":[{"name":"container","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"},{"name":"name","type":"String"},{"name":"zone","type":"String"},{"name":"metaOnChain","type":"Boolean"}],"returntype":"Void","safe":false},{"name":"putNamed","offset":3150,"parameters":[{"name":"container","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"},{"name":"name","type":"String"},{"name":"zone","type":"String"}],"returntype":"Void","safe":false},{"name":"put","offset":3136,"parameters":[{"name":"container","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"},{"name":"name","type":"String"},{"name":"zone","type":"String"}],"returntype":"Void","safe":false},{"name":"putReport","offset":8334,"parameters":[{"name":"cid","type":"Hash256"},{"name":"sizeBytes","type":"Integer"},{"name":"objsNumber","type":"Integer"},{"name":"pubKey","type":"PublicKey"}],"returntype":"Void","safe":false},{"name":"remove","offset":5202,"parameters":[{"name":"id","type":"ByteArray"},{"name":"invocScript","type":"ByteArray"},{"name":"verifScript","type":"ByteArray"},{"name":"sessionToken","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"replicasNumbers","offset":7479,"parameters":[{"name":"cID","type":"Hash256"}],"returntype":"InteropInterface","safe":true},{"name":"setEACL","offset":7701,"parameters":[{"name":"eACL","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"setHardContainerQuota","offset":9548,"parameters":[{"name":"cID","type":"Hash256"},{"name":"size","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setHardUserQuota","offset":10022,"parameters":[{"name":"user","type":"ByteArray"},{"name":"size","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setSoftContainerQuota","offset":9535,"parameters":[{"name":"cID","type":"Hash256"},{"name":"size","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setSoftUserQuota","offset":10012,"parameters":[{"name":"user","type":"ByteArray"},{"name":"size","type":"Integer"}],"returntype":"Void","safe":false},{"name":"submitObjectPut","offset":1967,"parameters":[{"name":"metaInformation","type":"ByteArray"},{"name":"sigs","type":"Array"}],"returntype":"Void","safe":false},{"name":"update","offset":1834,"parameters":[{"name":"nefFile","type":"ByteArray"},{"name":"manifest","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"userQuota","offset":10195,"parameters":[{"name":"user","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"verifyPlacementSignatures","offset":6473,"parameters":[{"name":"cid","type":"Hash256"},{"name":"msg","type":"ByteArray"},{"name":"sigs","type":"Array"}],"returntype":"Boolean","safe":true},{"name":"version","offset":10282,"parameters":[],"returntype":"Integer","safe":true}],"events":[{"name":"PutSuccess","parameters":[{"name":"containerID","type":"Hash256"},{"name":"publicKey","type":"PublicKey"}]},{"name":"Created","parameters":[{"name":"containerID","type":"Hash256"},{"name":"owner","type":"ByteArray"}]},{"name":"DeleteSuccess","parameters":[{"name":"containerID","type":"ByteArray"}]},{"name":"Removed","parameters":[{"name":"containerID","type":"Hash256"},{"name":"owner","type":"ByteArray"}]},{"name":"SetEACLSuccess","parameters":[{"name":"containerID","type":"ByteArray"},{"name":"publicKey","type":"PublicKey"}]},{"name":"EACLChanged","parameters":[{"name":"containerID","type":"Hash256"}]},{"name":"NodesUpdate","parameters":[{"name":"ContainerID","type":"Hash256"}]},{"name":"ObjectPut","parameters":[{"name":"ContainerID","type":"Hash256"},{"name":"ObjectID","type":"Hash256"},{"name":"Meta","type":"Map"}]},{"name":"ContainerQuotaSet","parameters":[{"name":"ContainerID","type":"Hash256"},{"name":"LimitValue","type":"Integer"},{"name":"Hard","type":"Boolean"}]},{"name":"UserQuotaSet","parameters":[{"name":"UserID","type":"ByteArray"},{"name":"LimitValue","type":"Integer"},{"name":"Hard","type":"Boolean"}]}]},"features":{},"groups":[],"permissions":[{"contract":"*","methods":["update","addKey","transferX","register","registerTLD","addRecord","deleteRecords","unsubscribeFromNewEpoch"]}],"supportedstandards":["NEP-22"],"trusts":[],"extra":null} \ No newline at end of file +{"name":"NeoFS Container","abi":{"methods":[{"name":"_initialize","offset":0,"parameters":[],"returntype":"Void","safe":false},{"name":"_deploy","offset":112,"parameters":[{"name":"data","type":"Any"},{"name":"isUpdate","type":"Boolean"}],"returntype":"Void","safe":false},{"name":"addNextEpochNodes","offset":8180,"parameters":[{"name":"cID","type":"Hash256"},{"name":"placementVector","type":"Integer"},{"name":"publicKeys","type":"Array"}],"returntype":"Void","safe":false},{"name":"alias","offset":7986,"parameters":[{"name":"cid","type":"ByteArray"}],"returntype":"String","safe":true},{"name":"commitContainerListUpdate","offset":8788,"parameters":[{"name":"cID","type":"Hash256"},{"name":"replicas","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"containerQuota","offset":12027,"parameters":[{"name":"cID","type":"Hash256"}],"returntype":"Array","safe":true},{"name":"containersOf","offset":8126,"parameters":[{"name":"owner","type":"ByteArray"}],"returntype":"InteropInterface","safe":true},{"name":"count","offset":8081,"parameters":[],"returntype":"Integer","safe":true},{"name":"create","offset":4882,"parameters":[{"name":"cnr","type":"ByteArray"},{"name":"invocScript","type":"ByteArray"},{"name":"verifScript","type":"ByteArray"},{"name":"sessionToken","type":"ByteArray"},{"name":"name","type":"String"},{"name":"zone","type":"String"},{"name":"metaOnChain","type":"Boolean"}],"returntype":"Void","safe":false},{"name":"createV2","offset":5747,"parameters":[{"name":"cnr","type":"Array"},{"name":"invocScript","type":"ByteArray"},{"name":"verifScript","type":"ByteArray"},{"name":"sessionToken","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"delete","offset":7018,"parameters":[{"name":"containerID","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"token","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"eACL","offset":10333,"parameters":[{"name":"containerID","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"get","offset":7714,"parameters":[{"name":"containerID","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"getContainerData","offset":7839,"parameters":[{"name":"id","type":"ByteArray"}],"returntype":"ByteArray","safe":true},{"name":"getEACLData","offset":10391,"parameters":[{"name":"id","type":"ByteArray"}],"returntype":"ByteArray","safe":true},{"name":"getInfo","offset":7628,"parameters":[{"name":"id","type":"Hash256"}],"returntype":"Array","safe":true},{"name":"getNodeReportSummary","offset":11349,"parameters":[{"name":"cid","type":"Hash256"}],"returntype":"Array","safe":true},{"name":"getReportByNode","offset":11453,"parameters":[{"name":"cid","type":"Hash256"},{"name":"pubKey","type":"PublicKey"}],"returntype":"Array","safe":true},{"name":"getTakenSpaceByUser","offset":11309,"parameters":[{"name":"user","type":"ByteArray"}],"returntype":"Integer","safe":true},{"name":"iterateAllReportSummaries","offset":11654,"parameters":[],"returntype":"InteropInterface","safe":true},{"name":"iterateReports","offset":11578,"parameters":[{"name":"cid","type":"Hash256"}],"returntype":"InteropInterface","safe":true},{"name":"nodes","offset":9724,"parameters":[{"name":"cID","type":"Hash256"},{"name":"placementVector","type":"Integer"}],"returntype":"InteropInterface","safe":true},{"name":"onNEP11Payment","offset":2661,"parameters":[{"name":"a","type":"Hash160"},{"name":"b","type":"Integer"},{"name":"c","type":"ByteArray"},{"name":"d","type":"Any"}],"returntype":"Void","safe":false},{"name":"owner","offset":7935,"parameters":[{"name":"containerID","type":"ByteArray"}],"returntype":"ByteArray","safe":true},{"name":"put","offset":4046,"parameters":[{"name":"container","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"putEACL","offset":10087,"parameters":[{"name":"eACL","type":"ByteArray"},{"name":"invocScript","type":"ByteArray"},{"name":"verifScript","type":"ByteArray"},{"name":"sessionToken","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"put","offset":3983,"parameters":[{"name":"container","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"},{"name":"name","type":"String"},{"name":"zone","type":"String"},{"name":"metaOnChain","type":"Boolean"}],"returntype":"Void","safe":false},{"name":"putNamed","offset":4076,"parameters":[{"name":"container","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"},{"name":"name","type":"String"},{"name":"zone","type":"String"}],"returntype":"Void","safe":false},{"name":"put","offset":4062,"parameters":[{"name":"container","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"},{"name":"name","type":"String"},{"name":"zone","type":"String"}],"returntype":"Void","safe":false},{"name":"putReport","offset":10481,"parameters":[{"name":"cid","type":"Hash256"},{"name":"sizeBytes","type":"Integer"},{"name":"objsNumber","type":"Integer"},{"name":"pubKey","type":"PublicKey"}],"returntype":"Void","safe":false},{"name":"remove","offset":7180,"parameters":[{"name":"id","type":"ByteArray"},{"name":"invocScript","type":"ByteArray"},{"name":"verifScript","type":"ByteArray"},{"name":"sessionToken","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"replicasNumbers","offset":9626,"parameters":[{"name":"cID","type":"Hash256"}],"returntype":"InteropInterface","safe":true},{"name":"setEACL","offset":9848,"parameters":[{"name":"eACL","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"setHardContainerQuota","offset":11695,"parameters":[{"name":"cID","type":"Hash256"},{"name":"size","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setHardUserQuota","offset":12169,"parameters":[{"name":"user","type":"ByteArray"},{"name":"size","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setSoftContainerQuota","offset":11682,"parameters":[{"name":"cID","type":"Hash256"},{"name":"size","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setSoftUserQuota","offset":12159,"parameters":[{"name":"user","type":"ByteArray"},{"name":"size","type":"Integer"}],"returntype":"Void","safe":false},{"name":"submitObjectPut","offset":2893,"parameters":[{"name":"metaInformation","type":"ByteArray"},{"name":"sigs","type":"Array"}],"returntype":"Void","safe":false},{"name":"update","offset":2760,"parameters":[{"name":"nefFile","type":"ByteArray"},{"name":"manifest","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"userQuota","offset":12342,"parameters":[{"name":"user","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"verifyPlacementSignatures","offset":8620,"parameters":[{"name":"cid","type":"Hash256"},{"name":"msg","type":"ByteArray"},{"name":"sigs","type":"Array"}],"returntype":"Boolean","safe":true},{"name":"version","offset":12429,"parameters":[],"returntype":"Integer","safe":true}],"events":[{"name":"PutSuccess","parameters":[{"name":"containerID","type":"Hash256"},{"name":"publicKey","type":"PublicKey"}]},{"name":"Created","parameters":[{"name":"containerID","type":"Hash256"},{"name":"owner","type":"ByteArray"}]},{"name":"DeleteSuccess","parameters":[{"name":"containerID","type":"ByteArray"}]},{"name":"Removed","parameters":[{"name":"containerID","type":"Hash256"},{"name":"owner","type":"ByteArray"}]},{"name":"SetEACLSuccess","parameters":[{"name":"containerID","type":"ByteArray"},{"name":"publicKey","type":"PublicKey"}]},{"name":"EACLChanged","parameters":[{"name":"containerID","type":"Hash256"}]},{"name":"NodesUpdate","parameters":[{"name":"ContainerID","type":"Hash256"}]},{"name":"ObjectPut","parameters":[{"name":"ContainerID","type":"Hash256"},{"name":"ObjectID","type":"Hash256"},{"name":"Meta","type":"Map"}]},{"name":"ContainerQuotaSet","parameters":[{"name":"ContainerID","type":"Hash256"},{"name":"LimitValue","type":"Integer"},{"name":"Hard","type":"Boolean"}]},{"name":"UserQuotaSet","parameters":[{"name":"UserID","type":"ByteArray"},{"name":"LimitValue","type":"Integer"},{"name":"Hard","type":"Boolean"}]}]},"features":{},"groups":[],"permissions":[{"contract":"*","methods":["update","addKey","transferX","register","registerTLD","addRecord","deleteRecords","unsubscribeFromNewEpoch"]}],"supportedstandards":["NEP-22"],"trusts":[],"extra":null} \ No newline at end of file diff --git a/contracts/container/migration_test.go b/contracts/container/migration_test.go index 567028ac..e39c5a15 100644 --- a/contracts/container/migration_test.go +++ b/contracts/container/migration_test.go @@ -11,7 +11,9 @@ import ( "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neofs-contract/tests/dump" "github.com/nspcc-dev/neofs-contract/tests/migration" + protocontainer "github.com/nspcc-dev/neofs-sdk-go/proto/container" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" ) const name = "container" @@ -111,7 +113,7 @@ func testMigrationFromDump(t *testing.T, d *dump.Reader) { require.Equal(t, prevContainerCount, newContainerCount, "number of containers should remain") require.ElementsMatch(t, prevContainers, newContainers, "container list should remain") - require.True(t, maps.EqualFunc(prevCnrStorageItems, newCnrStorageItems, bytes.Equal), "containers' binary items should remain") + require.Empty(t, newCnrStorageItems, "containers' binary items should disappear") require.True(t, maps.EqualFunc(prevEACLStorageItems, newEACLStorageItems, bytes.Equal), "eACLs' binary items should remain") require.Equal(t, len(prevOwnersToContainers), len(newOwnersToContainers)) @@ -120,4 +122,77 @@ func testMigrationFromDump(t *testing.T, d *dump.Reader) { require.True(t, ok) require.ElementsMatch(t, vPrev, vNew, "containers of '%s' owner should remain", base58.Encode([]byte(k))) } + + infoStorageItems := make(map[string][]byte) // CID -> Info + c.SeekStorage([]byte{0}, func(k, v []byte) bool { + infoStorageItems[string(k)] = slices.Clone(v) + return true + }) + require.Len(t, infoStorageItems, len(prevCnrStorageItems)) + + for id, b := range prevCnrStorageItems { + cnrStructBytes, ok := infoStorageItems[id] + require.Truef(t, ok, "container %s is not migrated to structured info", base58.Encode([]byte(id))) + + expFields := containerBytesToStructFields(t, b) + + gotStruct := c.Call(t, "getInfo", id) + require.IsType(t, (*stackitem.Struct)(nil), gotStruct) + assertEqualItemArray(t, expFields, gotStruct.Value().([]stackitem.Item)) + + gotStruct, err := stackitem.Deserialize(cnrStructBytes) + require.NoError(t, err) + require.IsType(t, (*stackitem.Struct)(nil), gotStruct) + assertEqualItemArray(t, expFields, gotStruct.Value().([]stackitem.Item)) + } +} + +// copy-paste from tests/container_test.go. +func containerBytesToStructFields(t testing.TB, b []byte) []stackitem.Item { + var cnr protocontainer.Container + require.NoError(t, proto.Unmarshal(b, &cnr)) + + var attrs []stackitem.Item + for _, a := range cnr.Attributes { + attrs = append(attrs, stackitem.NewStruct([]stackitem.Item{ + stackitem.Make(a.Key), stackitem.Make(a.Value), + })) + } + + policyBytes, err := proto.Marshal(cnr.PlacementPolicy) + require.NoError(t, err) + + return []stackitem.Item{ + stackitem.NewStruct([]stackitem.Item{ + stackitem.Make(cnr.GetVersion().GetMajor()), + stackitem.Make(cnr.GetVersion().GetMinor()), + }), + stackitem.NewBuffer(cnr.GetOwnerId().GetValue()), + stackitem.NewBuffer(cnr.GetNonce()), + stackitem.Make(cnr.GetBasicAcl()), + stackitem.NewArray(attrs), + stackitem.NewBuffer(policyBytes), + } +} + +func assertEqualItemArray(t testing.TB, exp, got []stackitem.Item) { + for i := range exp { + if expArr, err := exp[i].Convert(stackitem.ArrayT); err == nil { + gotArr, err := got[i].Convert(stackitem.ArrayT) + require.NoError(t, err) + + assertEqualItemArray(t, stackItemToArray(expArr), stackItemToArray(gotArr)) + + continue + } + + require.Equal(t, exp[i].Value(), got[i].Value(), i) + } +} + +func stackItemToArray(item stackitem.Item) []stackitem.Item { + if _, ok := item.(stackitem.Null); !ok { + return item.Value().([]stackitem.Item) + } + return nil } diff --git a/go.mod b/go.mod index 6c2bc86b..81775c69 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.14 github.com/stretchr/testify v1.11.1 go.uber.org/zap v1.27.0 + google.golang.org/protobuf v1.36.8 ) require ( @@ -58,6 +59,5 @@ require ( golang.org/x/tools v0.37.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect google.golang.org/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.8 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/internal/proto/encoding.go b/internal/proto/encoding.go new file mode 100644 index 00000000..1647f51b --- /dev/null +++ b/internal/proto/encoding.go @@ -0,0 +1,122 @@ +package proto + +import ( + "math" + + "github.com/nspcc-dev/neo-go/pkg/interop/native/std" +) + +const errVarintOverflow = "varint overflow" + +// EncodeTag encodes protobuf tag for field with given number and type. +func EncodeTag(num, typ uint64) uint64 { + return num<<3 | typ&7 +} + +// SizeTag returns size of protobuf tag for field with given number. +func SizeTag(num uint64) int { + return SizeVarint(num << 3) +} + +// SizeLEN returns length of nested [FieldTypeLEN] field. +func SizeLEN(ln int) int { + return SizeVarint(uint64(ln)) + ln +} + +// SizeVarint returns length of [FieldTypeVARINT] field. +func SizeVarint(x uint64) int { + i := 0 + for x >= 0x80 { + x = x >> 7 // TODO: https://github.com/nspcc-dev/neo-go/pull/4059 + i++ + } + return i + 1 +} + +// PutUvarint encodes x into buf with given offset and returns the number of +// bytes written. +func PutUvarint(buf []byte, off int, x uint64) int { + i := 0 + for x >= 0x80 { + // &0xFF is the only difference with binary.PutUvarint needed because + // VM throws exception on type narrowing. + buf[off+i] = byte(x&0xFF | 0x80) + x = x >> 7 // TODO: https://github.com/nspcc-dev/neo-go/pull/4059 + i++ + } + buf[off+i] = byte(x) + return i + 1 +} + +// ReadTag reads tag of protobuf field from b. Returns field number, type, +// number of bytes read and exception. +func ReadTag(b []byte) (int, int, int, string) { + n, r, e := uvarint(b) + if e != "" { + return 0, 0, 0, e + } + + num := n >> 3 + if num == 0 || num > MaxFieldNumber { + return 0, 0, 0, "invalid/unsupported protobuf field num " + std.Itoa10(int(n)) + } + + typ := n & 7 + if typ >= firstUnknownFieldType { + return 0, 0, 0, "invalid/unsupported protobuf field type " + std.Itoa10(int(typ)) + } + + return int(num), int(typ), r, "" +} + +// ReadSizeLEN reads length of nested [FieldTypeLEN] field from b. Returns +// resulting length, number of bytes read and exception. +func ReadSizeLEN(b []byte) (int, int, string) { + n, r, e := uvarint(b) + if e != "" { + return 0, 0, e + } + + if full := uint64(len(b)); n > full || n > full-uint64(r) { + return 0, 0, "too big field len " + std.Itoa10(int(n)) + ", full " + std.Itoa10(len(b)) + } + + return int(n), r, "" +} + +// ReadUint32 reads protobuf field of uint32 type. Returns field value, number +// of bytes read and exception. +func ReadUint32(b []byte) (uint32, int, string) { + n, r, e := uvarint(b) + if e != "" { + return 0, 0, e + } + + if n > math.MaxUint32 { + return 0, 0, std.Itoa10(int(n)) + " overflows uint32" + } + + return uint32(n), r, "" +} + +func uvarint(buf []byte) (uint64, int, string) { + const maxVarintLen = 10 + var x uint64 + var s uint + for i, b := range buf { + if i == maxVarintLen { + // Catch byte reads past maxVarintLen. + // See issue https://golang.org/issues/41185 + return 0, 0, errVarintOverflow + } + if b < 0x80 { + if i == maxVarintLen-1 && b > 1 { + return 0, 0, errVarintOverflow + } + return x | uint64(b)<