diff --git a/container/config.yml b/container/config.yml index 735f9de..ccde0be 100644 --- a/container/config.yml +++ b/container/config.yml @@ -2,6 +2,7 @@ name: "Container" safemethods: - "count" - "containersOf" + - "deletionInfo" - "eACL" - "get" - "getContainerSize" diff --git a/container/container_contract.go b/container/container_contract.go index 5cde579..68d5b8f 100644 --- a/container/container_contract.go +++ b/container/container_contract.go @@ -64,6 +64,7 @@ const ( estimateKeyPrefix = "cnr" containerKeyPrefix = 'x' ownerKeyPrefix = 'o' + graveKeyPrefix = 'g' estimatePostfixSize = 10 // CleanupDelta contains the number of the last epochs for which container estimations are present. CleanupDelta = 3 @@ -337,6 +338,25 @@ func Delete(containerID []byte, signature interop.Signature, publicKey interop.P runtime.Notify("DeleteSuccess", containerID) } +type DelInfo struct { + Owner interop.Hash160 + Epoch int +} + +// DeletionInfo method returns container deletion info. +// If the container had never existed, NotFoundError is throwed. +// It can be used to check whether non-existing container was indeed deleted +// or does not yet exist at some height. +func DeletionInfo(containerID []byte) DelInfo { + ctx := storage.GetReadOnlyContext() + graveKey := append([]byte{graveKeyPrefix}, containerID...) + data := storage.Get(ctx, graveKey).([]byte) + if data == nil { + panic(NotFoundError) + } + return std.Deserialize(data).(DelInfo) +} + // 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. @@ -596,6 +616,9 @@ func addContainer(ctx storage.Context, id, owner []byte, container Container) { idKey := append([]byte{containerKeyPrefix}, id...) common.SetSerialized(ctx, idKey, container) + + graveKey := append([]byte{graveKeyPrefix}, id...) + storage.Delete(ctx, graveKey) } func removeContainer(ctx storage.Context, id []byte, owner []byte) { @@ -604,6 +627,14 @@ func removeContainer(ctx storage.Context, id []byte, owner []byte) { storage.Delete(ctx, containerListKey) storage.Delete(ctx, append([]byte{containerKeyPrefix}, id...)) + + graveKey := append([]byte{graveKeyPrefix}, id...) + netmapContractAddr := storage.Get(ctx, netmapContractKey).(interop.Hash160) + epoch := contract.Call(netmapContractAddr, "epoch", contract.ReadOnly).(int) + common.SetSerialized(ctx, graveKey, DelInfo{ + Owner: owner, + Epoch: epoch, + }) } func getAllContainers(ctx storage.Context) [][]byte { diff --git a/tests/container_test.go b/tests/container_test.go index 70c6bdc..aa98f13 100644 --- a/tests/container_test.go +++ b/tests/container_test.go @@ -243,19 +243,43 @@ func addContainer(t *testing.T, c, cBal *neotest.ContractInvoker) (neotest.Signe } func TestContainerDelete(t *testing.T) { - c, cBal, _ := newContainerInvoker(t) + c, cBal, cNm := newContainerInvoker(t) acc, cnt := addContainer(t, c, cBal) cAcc := c.WithSigners(acc) cAcc.InvokeFail(t, common.ErrAlphabetWitnessFailed, "delete", cnt.id[:], cnt.sig, cnt.pub, cnt.token) + newDelInfo := func(acc neotest.Signer, epoch int64) *stackitem.Struct { + return stackitem.NewStruct([]stackitem.Item{ + stackitem.NewBuffer([]byte(signerToOwner(acc))), + stackitem.NewBigInteger(big.NewInt(epoch)), + }) + } + + c.InvokeFail(t, container.NotFoundError, "deletionInfo", cnt.id[:]) c.Invoke(t, stackitem.Null{}, "delete", cnt.id[:], cnt.sig, cnt.pub, cnt.token) + c.Invoke(t, newDelInfo(acc, 0), "deletionInfo", cnt.id[:]) + + t.Run("multi-epoch", func(t *testing.T) { + cNm.Invoke(t, stackitem.Null{}, "newEpoch", 1) + + t.Run("epoch tick does not change deletion info", func(t *testing.T) { + c.Invoke(t, stackitem.Null{}, "delete", cnt.id[:], cnt.sig, cnt.pub, cnt.token) + c.Invoke(t, newDelInfo(acc, 0), "deletionInfo", cnt.id[:]) + }) + + acc1, cnt1 := addContainer(t, c, cBal) + c.Invoke(t, stackitem.Null{}, "delete", cnt1.id[:], cnt1.sig, cnt1.pub, cnt1.token) + c.Invoke(t, newDelInfo(acc, 0), "deletionInfo", cnt.id[:]) + c.Invoke(t, newDelInfo(acc1, 1), "deletionInfo", cnt1.id[:]) + }) t.Run("missing container", func(t *testing.T) { id := cnt.id id[0] ^= 0xFF c.Invoke(t, stackitem.Null{}, "delete", cnt.id[:], cnt.sig, cnt.pub, cnt.token) + c.InvokeFail(t, container.NotFoundError, "deletionInfo", id[:]) }) c.InvokeFail(t, container.NotFoundError, "get", cnt.id[:])