Skip to content

Commit d34e380

Browse files
Support container deletion lock (#558)
2 parents e3afe03 + d647bfe commit d34e380

File tree

7 files changed

+300
-50
lines changed

7 files changed

+300
-50
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Changelog for NeoFS Contract
88
- `addStructs`, `createV2` and `getInfo` methods to Container contract (#534)
99
- NEP-11 support by Container contract (#498)
1010
- New NNS record type `Neo` (#544)
11+
- Support for `__NEOFS__LOCK_UNTIL` container attribute (#558)
1112

1213
### Changed
1314

contracts/container/containerconst/const.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,7 @@ const (
3030
// AlphabetManagesAttributesKey is a key in netmap config which defines if
3131
// Alphabet is allowed to manage container attributes instead of users.
3232
AlphabetManagesAttributesKey = "AlphabetManagesAttributes"
33+
34+
// ErrorLocked is returned with active container lock.
35+
ErrorLocked = "container is locked"
3336
)

contracts/container/contract.go

Lines changed: 143 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ const (
107107
nnsHasAliasKey = "nnsHasAlias"
108108

109109
corsAttributeName = "CORS"
110+
lockAttributeName = "__NEOFS__LOCK_UNTIL"
110111

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

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

403+
cnr := fromBytes(container)
404+
405+
for i := range cnr.Attributes {
406+
if cnr.Attributes[i].Key == lockAttributeName {
407+
if _, exc := checkLockUntilAttribute(cnr.Attributes[i].Value); exc != "" {
408+
panic(exc)
409+
}
410+
break
411+
}
412+
}
413+
403414
var (
404415
needRegister bool
405416
nnsContractAddr interop.Hash160
@@ -415,11 +426,10 @@ func PutNamed(container []byte, signature interop.Signature,
415426
}
416427

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

439449
if !contract.Call(balanceContractAddr, "transferX",
440450
contract.All,
441-
from,
451+
cnr.Owner,
442452
to,
443453
containerFee,
444454
details,
@@ -447,7 +457,7 @@ func PutNamed(container []byte, signature interop.Signature,
447457
}
448458
}
449459

450-
addContainer(ctx, containerID, ownerID, container)
460+
addContainer(ctx, containerID, scriptHashToAddress(cnr.Owner), container, cnr)
451461

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

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

473-
onNEP11Payment(containerID, nil, from, nil)
483+
onNEP11Payment(containerID, nil, cnr.Owner, nil)
474484
}
475485

476486
// Create saves container descriptor serialized according to the NeoFS API
@@ -521,6 +531,10 @@ func Create(cnr []byte, invocScript, verifScript, sessionToken []byte, name, zon
521531
// If '__NEOFS__METAINFO_CONSISTENCY' attribute is set, meta information about
522532
// objects from this container can be collected for it.
523533
//
534+
// If cnr contains '__NEOFS__LOCK_UNTIL' attribute, its value must be a valid
535+
// Unix Timestamp later the current one. On success, referenced container
536+
// becomes locked for removal until specified time.
537+
//
524538
// The operation is paid. Container owner pays per-container fee (global chain
525539
// configuration) to each committee member. If domain registration is requested,
526540
// additional alias fee (also a configuration) is added to each payment.
@@ -554,6 +568,10 @@ func CreateV2(cnr Info, invocScript, verifScript, sessionToken []byte) interop.H
554568
zone = cnr.Attributes[i].Value
555569
case "__NEOFS__METAINFO_CONSISTENCY":
556570
metaOnChain = true
571+
case lockAttributeName:
572+
if _, exc := checkLockUntilAttribute(cnr.Attributes[i].Value); exc != "" {
573+
panic(exc)
574+
}
557575
}
558576
}
559577

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

702-
ownerID := getOwnerByID(ctx, containerID)
703-
if ownerID == nil {
720+
cnr, ok := tryGetInfo(ctx, containerID)
721+
if !ok {
704722
return
705723
}
706724

707725
common.CheckAlphabetWitness()
708726

727+
if e := checkLock(cnr); e != "" {
728+
panic(e)
729+
}
730+
709731
key := append([]byte(nnsHasAliasKey), containerID...)
710732
domain := storage.Get(ctx, key).(string)
711733
if len(domain) != 0 {
712734
storage.Delete(ctx, key)
713735
deleteNNSRecords(ctx, domain)
714736
}
715-
removeContainer(ctx, containerID, ownerID)
737+
738+
removeContainer(ctx, containerID, scriptHashToAddress(cnr.Owner))
716739
runtime.Log("remove container")
717740
runtime.Notify("DeleteSuccess", containerID)
718741

719-
notifyNEP11Transfer(containerID, common.WalletToScriptHash(ownerID), nil)
742+
notifyNEP11Transfer(containerID, cnr.Owner, nil)
720743
}
721744

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

734760
ctx := storage.GetContext()
735-
cnrItemKey := append([]byte{containerKeyPrefix}, id...)
736-
cnrItem := storage.Get(ctx, cnrItemKey)
737-
if cnrItem == nil {
761+
762+
cnr, ok := tryGetInfo(ctx, id)
763+
if !ok {
738764
return
739765
}
740766

741-
owner := ownerFromBinaryContainer(cnrItem.([]byte))
767+
if e := checkLock(cnr); e != "" {
768+
panic(e)
769+
}
770+
771+
owner := scriptHashToAddress(cnr.Owner)
742772

743773
removeContainer(ctx, id, owner)
744774

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

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

753-
notifyNEP11Transfer(id, common.WalletToScriptHash(owner), nil)
783+
notifyNEP11Transfer(id, cnr.Owner, nil)
754784
}
755785

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

779-
func getInfo(ctx storage.Context, id interop.Hash256) Info {
809+
func tryGetInfo(ctx storage.Context, id interop.Hash256) (Info, bool) {
780810
val := storage.Get(ctx, append([]byte{infoPrefix}, id...))
781811
if val == nil {
782812
if val = storage.Get(ctx, append([]byte{containerKeyPrefix}, id...)); val != nil {
783-
return fromBytes(val.([]byte))
813+
return fromBytes(val.([]byte)), true
784814
}
815+
return Info{}, false
816+
}
817+
return std.Deserialize(val.([]byte)).(Info), true
818+
}
819+
820+
func getInfo(ctx storage.Context, id interop.Hash256) Info {
821+
res, ok := tryGetInfo(ctx, id)
822+
if !ok {
785823
panic(cst.NotFoundError)
786824
}
787-
return std.Deserialize(val.([]byte)).(Info)
825+
return res
788826
}
789827

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

1809-
func addContainer(ctx storage.Context, id, owner, container []byte) {
1847+
func addContainer(ctx storage.Context, id, owner, container []byte, info Info) {
18101848
containerListKey := append([]byte{ownerKeyPrefix}, owner...)
18111849
containerListKey = append(containerListKey, id...)
18121850
storage.Put(ctx, containerListKey, id)
18131851

18141852
idKey := append([]byte{containerKeyPrefix}, id...)
18151853
storage.Put(ctx, idKey, container)
18161854

1817-
storage.Put(ctx, append([]byte{infoPrefix}, id...), std.Serialize(fromBytes(container)))
1855+
storage.Put(ctx, append([]byte{infoPrefix}, id...), std.Serialize(info))
18181856
}
18191857

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

1986+
func checkLock(cnr Info) string {
1987+
for i := range cnr.Attributes {
1988+
if cnr.Attributes[i].Key == lockAttributeName {
1989+
until := std.Atoi10(cnr.Attributes[i].Value) * 1000
1990+
1991+
if now := runtime.GetTime(); now <= until {
1992+
return cst.ErrorLocked + " until " + cnr.Attributes[i].Value + "000, now " + std.Itoa10(now)
1993+
}
1994+
1995+
break
1996+
}
1997+
}
1998+
1999+
return ""
2000+
}
2001+
2002+
func checkLockUntilAttribute(val string) (int, string) {
2003+
until := std.Atoi10(val)
2004+
if until <= 0 {
2005+
return 0, "invalid " + lockAttributeName + " attribute: non-positive value " + val
2006+
}
2007+
2008+
untilMs := until * 1000
2009+
2010+
if now := runtime.GetTime(); untilMs <= now {
2011+
return 0, "lock expiration time " + val + "000 is not later than current " + std.Itoa10(now)
2012+
}
2013+
2014+
return until, ""
2015+
}
2016+
19482017
const (
19492018
fieldAPIVersion = 1
19502019
fieldOwner = 2
@@ -2293,10 +2362,15 @@ func checkAttributeSigner(ctx storage.Context, userScriptHash []byte) {
22932362

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

23142388
var (
2315-
exists bool
2316-
ctx = storage.GetContext()
2317-
info = getInfo(ctx, cID)
2389+
ctx = storage.GetContext()
2390+
info = getInfo(ctx, cID)
23182391
)
23192392

23202393
checkAttributeSigner(ctx, info.Owner)
23212394

2395+
idx := -1
2396+
23222397
switch name {
23232398
case corsAttributeName:
23242399
validateCORSAttribute(value)
2400+
case lockAttributeName:
2401+
until, exc := checkLockUntilAttribute(value)
2402+
if exc != "" {
2403+
panic(exc)
2404+
}
2405+
2406+
for idx = 0; idx < len(info.Attributes); idx++ { //nolint:intrange
2407+
if info.Attributes[idx].Key == name {
2408+
if until <= std.Atoi10(info.Attributes[idx].Value) {
2409+
panic("lock expiration time " + value + " is not later than already set " + info.Attributes[idx].Value)
2410+
}
2411+
break
2412+
}
2413+
}
23252414
default:
23262415
panic("attribute is immutable")
23272416
}
23282417

2329-
for i, attr := range info.Attributes {
2330-
if attr.Key == name {
2331-
exists = true
2332-
info.Attributes[i].Value = value
2333-
break
2418+
if idx < 0 { // was not done in switch
2419+
for idx = 0; idx < len(info.Attributes); idx++ { //nolint:intrange
2420+
if info.Attributes[idx].Key == name {
2421+
break
2422+
}
23342423
}
23352424
}
23362425

2337-
if !exists {
2426+
if idx < len(info.Attributes) {
2427+
info.Attributes[idx].Value = value
2428+
} else {
23382429
info.Attributes = append(info.Attributes, Attribute{
23392430
Key: name,
23402431
Value: value,
@@ -2475,7 +2566,11 @@ func validateCORSExposeHeaders(items []any) string {
24752566

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

24982593
switch name {
24992594
case corsAttributeName:
2595+
case lockAttributeName:
2596+
for index = 0; index < len(info.Attributes); index++ { //nolint:intrange
2597+
if info.Attributes[index].Key == name {
2598+
now := runtime.GetTime()
2599+
if std.Atoi10(info.Attributes[index].Value)*1000 > now {
2600+
panic("lock expiration time " + info.Attributes[index].Value + "000 has not passed yet, now " + std.Itoa10(now))
2601+
}
2602+
break
2603+
}
2604+
}
25002605
default:
25012606
panic("attribute is immutable")
25022607
}
25032608

2504-
for i, attr := range info.Attributes {
2505-
if attr.Key == name {
2506-
index = i
2507-
break
2609+
if index < 0 {
2610+
for index = 0; index < len(info.Attributes); index++ { //nolint:intrange
2611+
if info.Attributes[index].Key == name {
2612+
break
2613+
}
25082614
}
25092615
}
25102616

2511-
if index == -1 {
2617+
if index == len(info.Attributes) {
25122618
return
25132619
}
25142620

contracts/container/contract.nef

923 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)