@@ -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
699717func 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.
727753func 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
756786func 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
18201858func 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+
19482017const (
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
0 commit comments