Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Changelog for NeoFS Contract
- `addStructs`, `createV2` and `getInfo` methods to Container contract (#534)
- NEP-11 support by Container contract (#498)
- New NNS record type `Neo` (#544)
- Support for `__NEOFS__LOCK_UNTIL` container attribute (#558)

### Changed

Expand Down
3 changes: 3 additions & 0 deletions contracts/container/containerconst/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,7 @@ const (
// AlphabetManagesAttributesKey is a key in netmap config which defines if
// Alphabet is allowed to manage container attributes instead of users.
AlphabetManagesAttributesKey = "AlphabetManagesAttributes"

// ErrorLocked is returned with active container lock.
ErrorLocked = "container is locked"
)
180 changes: 143 additions & 37 deletions contracts/container/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ const (
nnsHasAliasKey = "nnsHasAlias"

corsAttributeName = "CORS"
lockAttributeName = "__NEOFS__LOCK_UNTIL"

// nolint:unused
nnsDefaultTLD = "container"
Expand Down Expand Up @@ -394,12 +395,22 @@ func PutNamed(container []byte, signature interop.Signature,
name, zone string) {
ctx := storage.GetContext()

ownerID := ownerFromBinaryContainer(container)
containerID := crypto.Sha256(container)
if storage.Get(ctx, append([]byte{deletedKeyPrefix}, []byte(containerID)...)) != nil {
panic(cst.ErrorDeleted)
}

cnr := fromBytes(container)

for i := range cnr.Attributes {
if cnr.Attributes[i].Key == lockAttributeName {
if _, exc := checkLockUntilAttribute(cnr.Attributes[i].Value); exc != "" {
panic(exc)
}
break
}
}

var (
needRegister bool
nnsContractAddr interop.Hash160
Expand All @@ -415,11 +426,10 @@ func PutNamed(container []byte, signature interop.Signature,
}

alphabet := common.AlphabetNodes()
from := common.WalletToScriptHash(ownerID)
netmapContractAddr := storage.Get(ctx, netmapContractKey).(interop.Hash160)
balanceContractAddr := storage.Get(ctx, balanceContractKey).(interop.Hash160)
containerFee := contract.Call(netmapContractAddr, "config", contract.ReadOnly, cst.RegistrationFeeKey).(int)
balance := contract.Call(balanceContractAddr, "balanceOf", contract.ReadOnly, from).(int)
balance := contract.Call(balanceContractAddr, "balanceOf", contract.ReadOnly, cnr.Owner).(int)
if name != "" {
aliasFee := contract.Call(netmapContractAddr, "config", contract.ReadOnly, cst.AliasFeeKey).(int)
containerFee += aliasFee
Expand All @@ -438,7 +448,7 @@ func PutNamed(container []byte, signature interop.Signature,

if !contract.Call(balanceContractAddr, "transferX",
contract.All,
from,
cnr.Owner,
to,
containerFee,
details,
Expand All @@ -447,7 +457,7 @@ func PutNamed(container []byte, signature interop.Signature,
}
}

addContainer(ctx, containerID, ownerID, container)
addContainer(ctx, containerID, scriptHashToAddress(cnr.Owner), container, cnr)

if name != "" {
if needRegister {
Expand All @@ -468,9 +478,9 @@ func PutNamed(container []byte, signature interop.Signature,
runtime.Log("added new container")
runtime.Notify("PutSuccess", containerID, publicKey)

notifyNEP11Transfer(containerID, nil, from) // 'from' is owner here i.e. 'to' in terms of NEP-11
notifyNEP11Transfer(containerID, nil, cnr.Owner)

onNEP11Payment(containerID, nil, from, nil)
onNEP11Payment(containerID, nil, cnr.Owner, nil)
}

// Create saves container descriptor serialized according to the NeoFS API
Expand Down Expand Up @@ -521,6 +531,10 @@ func Create(cnr []byte, invocScript, verifScript, sessionToken []byte, name, zon
// If '__NEOFS__METAINFO_CONSISTENCY' attribute is set, meta information about
// objects from this container can be collected for it.
//
// If cnr contains '__NEOFS__LOCK_UNTIL' attribute, its value must be a valid
// Unix Timestamp later the current one. On success, referenced container
// becomes locked for removal until specified time.
//
// 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.
Expand Down Expand Up @@ -554,6 +568,10 @@ func CreateV2(cnr Info, invocScript, verifScript, sessionToken []byte) interop.H
zone = cnr.Attributes[i].Value
case "__NEOFS__METAINFO_CONSISTENCY":
metaOnChain = true
case lockAttributeName:
if _, exc := checkLockUntilAttribute(cnr.Attributes[i].Value); exc != "" {
panic(exc)
}
}
}

Expand Down Expand Up @@ -699,30 +717,38 @@ func checkNiceNameAvailable(nnsContractAddr interop.Hash160, domain string) bool
func Delete(containerID []byte, signature interop.Signature, token []byte) {
ctx := storage.GetContext()

ownerID := getOwnerByID(ctx, containerID)
if ownerID == nil {
cnr, ok := tryGetInfo(ctx, containerID)
if !ok {
return
}

common.CheckAlphabetWitness()

if e := checkLock(cnr); e != "" {
panic(e)
}

key := append([]byte(nnsHasAliasKey), containerID...)
domain := storage.Get(ctx, key).(string)
if len(domain) != 0 {
storage.Delete(ctx, key)
deleteNNSRecords(ctx, domain)
}
removeContainer(ctx, containerID, ownerID)

removeContainer(ctx, containerID, scriptHashToAddress(cnr.Owner))
runtime.Log("remove container")
runtime.Notify("DeleteSuccess", containerID)

notifyNEP11Transfer(containerID, common.WalletToScriptHash(ownerID), nil)
notifyNEP11Transfer(containerID, cnr.Owner, nil)
}

// Remove removes all data for the referenced container. Remove is no-op if
// container does not exist. On success, Remove throws 'Removed' notification
// event.
//
// If the container has '__NEOFS__LOCK_UNTIL' attribute with timestamp that has
// not passed yet, Remove throws exception containing [cst.ErrorLocked].
//
// See [CreateV2] for details.
func Remove(id []byte, invocScript, verifScript, sessionToken []byte) {
common.CheckAlphabetWitness()
Expand All @@ -732,13 +758,17 @@ func Remove(id []byte, invocScript, verifScript, sessionToken []byte) {
}

ctx := storage.GetContext()
cnrItemKey := append([]byte{containerKeyPrefix}, id...)
cnrItem := storage.Get(ctx, cnrItemKey)
if cnrItem == nil {

cnr, ok := tryGetInfo(ctx, id)
if !ok {
return
}

owner := ownerFromBinaryContainer(cnrItem.([]byte))
if e := checkLock(cnr); e != "" {
panic(e)
}

owner := scriptHashToAddress(cnr.Owner)

removeContainer(ctx, id, owner)

Expand All @@ -750,7 +780,7 @@ func Remove(id []byte, invocScript, verifScript, sessionToken []byte) {

runtime.Notify("Removed", interop.Hash256(id), owner)

notifyNEP11Transfer(id, common.WalletToScriptHash(owner), nil)
notifyNEP11Transfer(id, cnr.Owner, nil)
}

func deleteNNSRecords(ctx storage.Context, domain string) {
Expand All @@ -776,15 +806,23 @@ func GetInfo(id interop.Hash256) Info {
return getInfo(storage.GetReadOnlyContext(), id)
}

func getInfo(ctx storage.Context, id interop.Hash256) Info {
func tryGetInfo(ctx storage.Context, id interop.Hash256) (Info, bool) {
val := storage.Get(ctx, append([]byte{infoPrefix}, id...))
if val == nil {
if val = storage.Get(ctx, append([]byte{containerKeyPrefix}, id...)); val != nil {
return fromBytes(val.([]byte))
return fromBytes(val.([]byte)), true
}
return Info{}, false
}
return std.Deserialize(val.([]byte)).(Info), true
}

func getInfo(ctx storage.Context, id interop.Hash256) Info {
res, ok := tryGetInfo(ctx, id)
if !ok {
panic(cst.NotFoundError)
}
return std.Deserialize(val.([]byte)).(Info)
return res
}

// Get method returns a structure that contains a stable marshaled Container structure,
Expand Down Expand Up @@ -1806,15 +1844,15 @@ func onNEP11Payment(tokenID []byte, from, to interop.Hash160, data any) {
}
}

func addContainer(ctx storage.Context, id, owner, container []byte) {
func addContainer(ctx storage.Context, id, owner, container []byte, info Info) {
containerListKey := append([]byte{ownerKeyPrefix}, owner...)
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)))
storage.Put(ctx, append([]byte{infoPrefix}, id...), std.Serialize(info))
}

func removeContainer(ctx storage.Context, id []byte, owner []byte) {
Expand Down Expand Up @@ -1945,6 +1983,37 @@ func scriptHashToAddress(h interop.Hash160) []byte {
return addr
}

func checkLock(cnr Info) string {
for i := range cnr.Attributes {
if cnr.Attributes[i].Key == lockAttributeName {
until := std.Atoi10(cnr.Attributes[i].Value) * 1000

if now := runtime.GetTime(); now <= until {
return cst.ErrorLocked + " until " + cnr.Attributes[i].Value + "000, now " + std.Itoa10(now)
}

break
}
}

return ""
}

func checkLockUntilAttribute(val string) (int, string) {
until := std.Atoi10(val)
if until <= 0 {
return 0, "invalid " + lockAttributeName + " attribute: non-positive value " + val
}

untilMs := until * 1000

if now := runtime.GetTime(); untilMs <= now {
return 0, "lock expiration time " + val + "000 is not later than current " + std.Itoa10(now)
}

return until, ""
}

const (
fieldAPIVersion = 1
fieldOwner = 2
Expand Down Expand Up @@ -2293,10 +2362,15 @@ func checkAttributeSigner(ctx storage.Context, userScriptHash []byte) {

// SetAttribute sets container attribute. Not all container attributes can be changed
// with SetAttribute. The supported list of attributes:
// - CORS
// - CORS
// - NEOFS__LOCK_UNTIL
//
// CORS attribute gets JSON encoded `[]CORSRule` as value.
//
// If name is '__NEOFS__LOCK_UNTIL', value must a valid Unix Timestamp later the
// current and already set (if any) ones. On success, referenced container
// becomes locked for removal until specified time.
//
// SetAttribute must have either owner or Alphabet witness.
//
// SessionToken is optional and should be a stable marshaled SessionToken structure from API.
Expand All @@ -2312,29 +2386,46 @@ func SetAttribute(cID interop.Hash256, name, value string, sessionToken []byte)
}

var (
exists bool
ctx = storage.GetContext()
info = getInfo(ctx, cID)
ctx = storage.GetContext()
info = getInfo(ctx, cID)
)

checkAttributeSigner(ctx, info.Owner)

idx := -1

switch name {
case corsAttributeName:
validateCORSAttribute(value)
case lockAttributeName:
until, exc := checkLockUntilAttribute(value)
if exc != "" {
panic(exc)
}

for idx = 0; idx < len(info.Attributes); idx++ { //nolint:intrange
if info.Attributes[idx].Key == name {
if until <= std.Atoi10(info.Attributes[idx].Value) {
panic("lock expiration time " + value + " is not later than already set " + info.Attributes[idx].Value)
}
break
}
}
default:
panic("attribute is immutable")
}

for i, attr := range info.Attributes {
if attr.Key == name {
exists = true
info.Attributes[i].Value = value
break
if idx < 0 { // was not done in switch
for idx = 0; idx < len(info.Attributes); idx++ { //nolint:intrange
if info.Attributes[idx].Key == name {
break
}
}
}

if !exists {
if idx < len(info.Attributes) {
info.Attributes[idx].Value = value
} else {
info.Attributes = append(info.Attributes, Attribute{
Key: name,
Value: value,
Expand Down Expand Up @@ -2475,7 +2566,11 @@ func validateCORSExposeHeaders(items []any) string {

// RemoveAttribute removes container attribute. Not all container attributes can be removed
// with RemoveAttribute. The supported list of attributes:
// - CORS
// - CORS
// - __NEOFS__LOCK_UNTIL
//
// If name is '__NEOFS__LOCK_UNTIL', current time must be later than the
// currently set one if any.
//
// RemoveAttribute must have either owner or Alphabet witness.
//
Expand All @@ -2497,18 +2592,29 @@ func RemoveAttribute(cID interop.Hash256, name string) {

switch name {
case corsAttributeName:
case lockAttributeName:
for index = 0; index < len(info.Attributes); index++ { //nolint:intrange
if info.Attributes[index].Key == name {
now := runtime.GetTime()
if std.Atoi10(info.Attributes[index].Value)*1000 > now {
panic("lock expiration time " + info.Attributes[index].Value + "000 has not passed yet, now " + std.Itoa10(now))
}
break
}
}
default:
panic("attribute is immutable")
}

for i, attr := range info.Attributes {
if attr.Key == name {
index = i
break
if index < 0 {
for index = 0; index < len(info.Attributes); index++ { //nolint:intrange
if info.Attributes[index].Key == name {
break
}
}
}

if index == -1 {
if index == len(info.Attributes) {
return
}

Expand Down
Binary file modified contracts/container/contract.nef
Binary file not shown.
Loading