Skip to content

Commit 8be2e26

Browse files
committed
container: Provide Lock method
TBD Signed-off-by: Leonard Lyubich <leonard@morphbits.io>
1 parent b5fe92c commit 8be2e26

File tree

7 files changed

+155
-22
lines changed

7 files changed

+155
-22
lines changed

contracts/container/containerconst/const.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,7 @@ const (
2626

2727
// ErrorInvalidPublicKey is returned on an attempt to work with an incorrect public key.
2828
ErrorInvalidPublicKey = "invalid public key"
29+
30+
// ErrorLocked is returned with active container lock.
31+
ErrorLocked = "container is locked"
2932
)

contracts/container/contract.go

Lines changed: 113 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
cst "github.com/nspcc-dev/neofs-contract/contracts/container/containerconst"
1616
"github.com/nspcc-dev/neofs-contract/contracts/nns/recordtype"
1717
iproto "github.com/nspcc-dev/neofs-contract/internal/proto"
18+
istd "github.com/nspcc-dev/neofs-contract/internal/std"
1819
)
1920

2021
type (
@@ -138,6 +139,11 @@ const (
138139
nep11TransferAmount = 1 // non-divisible NFT
139140
)
140141

142+
// Attributes.
143+
const (
144+
attributeLock = "__NEOFS__LOCK_UNTIL"
145+
)
146+
141147
var (
142148
eACLPrefix = []byte("eACL")
143149
)
@@ -390,9 +396,17 @@ func PutNamedOverloaded(container []byte, signature interop.Signature, publicKey
390396
func PutNamed(container []byte, signature interop.Signature,
391397
publicKey interop.PublicKey, token []byte,
392398
name, zone string) {
399+
cnr := fromBytes(container)
400+
401+
for i := range cnr.Attributes {
402+
if cnr.Attributes[i].Key == attributeLock {
403+
parseLockAttribute(cnr.Attributes[i].Value)
404+
break
405+
}
406+
}
407+
393408
ctx := storage.GetContext()
394409

395-
ownerID := ownerFromBinaryContainer(container)
396410
containerID := crypto.Sha256(container)
397411
if storage.Get(ctx, append([]byte{deletedKeyPrefix}, []byte(containerID)...)) != nil {
398412
panic(cst.ErrorDeleted)
@@ -413,11 +427,10 @@ func PutNamed(container []byte, signature interop.Signature,
413427
}
414428

415429
alphabet := common.AlphabetNodes()
416-
from := common.WalletToScriptHash(ownerID)
417430
netmapContractAddr := storage.Get(ctx, netmapContractKey).(interop.Hash160)
418431
balanceContractAddr := storage.Get(ctx, balanceContractKey).(interop.Hash160)
419432
containerFee := contract.Call(netmapContractAddr, "config", contract.ReadOnly, cst.RegistrationFeeKey).(int)
420-
balance := contract.Call(balanceContractAddr, "balanceOf", contract.ReadOnly, from).(int)
433+
balance := contract.Call(balanceContractAddr, "balanceOf", contract.ReadOnly, cnr.Owner).(int)
421434
if name != "" {
422435
aliasFee := contract.Call(netmapContractAddr, "config", contract.ReadOnly, cst.AliasFeeKey).(int)
423436
containerFee += aliasFee
@@ -436,7 +449,7 @@ func PutNamed(container []byte, signature interop.Signature,
436449

437450
if !contract.Call(balanceContractAddr, "transferX",
438451
contract.All,
439-
from,
452+
cnr.Owner,
440453
to,
441454
containerFee,
442455
details,
@@ -445,7 +458,7 @@ func PutNamed(container []byte, signature interop.Signature,
445458
}
446459
}
447460

448-
addContainer(ctx, containerID, ownerID, container)
461+
addContainer(ctx, containerID, container, cnr)
449462

450463
if name != "" {
451464
if needRegister {
@@ -466,9 +479,9 @@ func PutNamed(container []byte, signature interop.Signature,
466479
runtime.Log("added new container")
467480
runtime.Notify("PutSuccess", containerID, publicKey)
468481

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

471-
onNEP11Payment(containerID, nil, from, nil)
484+
onNEP11Payment(containerID, nil, cnr.Owner, nil)
472485
}
473486

474487
// Create saves container descriptor serialized according to the NeoFS API
@@ -552,6 +565,8 @@ func CreateV2(cnr Info, invocScript, verifScript, sessionToken []byte) interop.H
552565
zone = cnr.Attributes[i].Value
553566
case "__NEOFS__METAINFO_CONSISTENCY":
554567
metaOnChain = true
568+
case attributeLock:
569+
parseLockAttribute(cnr.Attributes[i].Value)
555570
}
556571
}
557572

@@ -688,19 +703,24 @@ func checkNiceNameAvailable(nnsContractAddr interop.Hash160, domain string) bool
688703
func Delete(containerID []byte, signature interop.Signature, token []byte) {
689704
ctx := storage.GetContext()
690705

691-
ownerID := getOwnerByID(ctx, containerID)
692-
if ownerID == nil {
706+
cnr, ok := getInfo(ctx, containerID)
707+
if !ok {
693708
return
694709
}
695710

696711
common.CheckAlphabetWitness()
697712

713+
checkLock(cnr)
714+
698715
key := append([]byte(nnsHasAliasKey), containerID...)
699716
domain := storage.Get(ctx, key).(string)
700717
if len(domain) != 0 {
701718
storage.Delete(ctx, key)
702719
deleteNNSRecords(ctx, domain)
703720
}
721+
722+
ownerID := scriptHashToAddress(cnr.Owner)
723+
704724
removeContainer(ctx, containerID, ownerID)
705725
runtime.Log("remove container")
706726
runtime.Notify("DeleteSuccess", containerID)
@@ -721,13 +741,15 @@ func Remove(id []byte, invocScript, verifScript, sessionToken []byte) {
721741
}
722742

723743
ctx := storage.GetContext()
724-
cnrItemKey := append([]byte{containerKeyPrefix}, id...)
725-
cnrItem := storage.Get(ctx, cnrItemKey)
726-
if cnrItem == nil {
744+
745+
cnr, ok := getInfo(ctx, id)
746+
if !ok {
727747
return
728748
}
729749

730-
owner := ownerFromBinaryContainer(cnrItem.([]byte))
750+
checkLock(cnr)
751+
752+
owner := scriptHashToAddress(cnr.Owner)
731753

732754
removeContainer(ctx, id, owner)
733755

@@ -762,14 +784,23 @@ func deleteNNSRecords(ctx storage.Context, domain string) {
762784
// GetInfo reads container by ID. If the container is missing, GetInfo throws
763785
// [cst.NotFoundError] exception.
764786
func GetInfo(id interop.Hash256) Info {
765-
val := storage.Get(storage.GetReadOnlyContext(), append([]byte{infoPrefix}, id...))
787+
cnr, ok := getInfo(storage.GetReadOnlyContext(), id)
788+
if !ok {
789+
panic(cst.NotFoundError)
790+
}
791+
792+
return cnr
793+
}
794+
795+
func getInfo(ctx storage.Context, id interop.Hash256) (Info, bool) {
796+
val := storage.Get(ctx, append([]byte{infoPrefix}, id...))
766797
if val == nil {
767-
if val = storage.Get(storage.GetReadOnlyContext(), append([]byte{containerKeyPrefix}, id...)); val != nil {
768-
return fromBytes(val.([]byte))
798+
if val = storage.Get(ctx, append([]byte{containerKeyPrefix}, id...)); val != nil {
799+
return fromBytes(val.([]byte)), true
769800
}
770-
panic(cst.NotFoundError)
801+
return Info{}, false
771802
}
772-
return std.Deserialize(val.([]byte)).(Info)
803+
return std.Deserialize(val.([]byte)).(Info), true
773804
}
774805

775806
// Get method returns a structure that contains a stable marshaled Container structure,
@@ -1775,6 +1806,46 @@ func Properties(tokenID []byte) map[string]any {
17751806
return props
17761807
}
17771808

1809+
// TODO: docs.
1810+
func Lock(id interop.Hash256, until int) {
1811+
if until <= 0 {
1812+
panic("non-positive until " + std.Itoa10(until))
1813+
}
1814+
1815+
ctx := storage.GetContext()
1816+
1817+
cnr, ok := getInfo(ctx, id)
1818+
if !ok {
1819+
panic(cst.NotFoundError)
1820+
}
1821+
1822+
if !runtime.CheckWitness(cnr.Owner) {
1823+
panic("missing owner witness")
1824+
}
1825+
1826+
if now := runtime.GetTime() / 1000; now > until {
1827+
panic("until has already passed " + std.Itoa10(now) + " > " + std.Itoa10(until))
1828+
}
1829+
1830+
was := false
1831+
for i := range cnr.Attributes {
1832+
if cnr.Attributes[i].Key == attributeLock {
1833+
cnr.Attributes[i].Value = std.Itoa10(until)
1834+
was = true
1835+
break
1836+
}
1837+
}
1838+
if !was {
1839+
cnr.Attributes = append(cnr.Attributes, Attribute{
1840+
Key: attributeLock,
1841+
Value: std.Itoa10(until),
1842+
})
1843+
}
1844+
1845+
storage.Put(ctx, append([]byte{infoPrefix}, id...), std.Serialize(cnr))
1846+
storage.Put(ctx, append([]byte{containerKeyPrefix}, id...), toBytes(cnr))
1847+
}
1848+
17781849
func notifyNEP11Transfer(tokenID []byte, from, to interop.Hash160) {
17791850
runtime.Notify("Transfer", from, to, nep11TransferAmount, tokenID)
17801851
}
@@ -1785,15 +1856,15 @@ func onNEP11Payment(tokenID []byte, from, to interop.Hash160, data any) {
17851856
}
17861857
}
17871858

1788-
func addContainer(ctx storage.Context, id, owner, container []byte) {
1789-
containerListKey := append([]byte{ownerKeyPrefix}, owner...)
1859+
func addContainer(ctx storage.Context, id, container []byte, cnr Info) {
1860+
containerListKey := append([]byte{ownerKeyPrefix}, scriptHashToAddress(cnr.Owner)...)
17901861
containerListKey = append(containerListKey, id...)
17911862
storage.Put(ctx, containerListKey, id)
17921863

17931864
idKey := append([]byte{containerKeyPrefix}, id...)
17941865
storage.Put(ctx, idKey, container)
17951866

1796-
storage.Put(ctx, append([]byte{infoPrefix}, id...), std.Serialize(fromBytes(container)))
1867+
storage.Put(ctx, append([]byte{infoPrefix}, id...), std.Serialize(cnr))
17971868
}
17981869

17991870
func removeContainer(ctx storage.Context, id []byte, owner []byte) {
@@ -1924,6 +1995,27 @@ func scriptHashToAddress(h interop.Hash160) []byte {
19241995
return addr
19251996
}
19261997

1998+
func checkLock(cnr Info) {
1999+
for i := range cnr.Attributes {
2000+
if cnr.Attributes[i].Key == attributeLock {
2001+
until := parseLockAttribute(cnr.Attributes[i].Value)
2002+
2003+
if now := runtime.GetTime() / 1000; now <= until {
2004+
panic(cst.ErrorLocked + " until " + cnr.Attributes[i].Value + ", now " + std.Itoa10(now))
2005+
}
2006+
}
2007+
}
2008+
}
2009+
2010+
func parseLockAttribute(s string) int {
2011+
v, e := istd.Atoi(s)
2012+
if e {
2013+
panic("invalid lock attribute " + s)
2014+
}
2015+
2016+
return v
2017+
}
2018+
19272019
const (
19282020
fieldAPIVersion = 1
19292021
fieldOwner = 2

contracts/container/contract.nef

821 Bytes
Binary file not shown.

contracts/container/manifest.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

internal/std/std.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package std
2+
3+
import "github.com/nspcc-dev/neo-go/pkg/interop/native/std"
4+
5+
// Atoi works like [std.Atoi10] but returns true instead of panic.
6+
func Atoi(s string) (v int, e bool) {
7+
defer func() {
8+
if recover() != nil {
9+
e = true
10+
}
11+
}()
12+
v = std.Atoi10(s)
13+
return
14+
}

rpc/container/const.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,6 @@ const (
1212

1313
// NotFoundError is returned if container is missing.
1414
NotFoundError = containerconst.NotFoundError
15+
// ErrorLocked is returned if container is locked.
16+
ErrorLocked = containerconst.ErrorLocked
1517
)

rpc/container/rpcbinding.go

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)