frostfs-contract/container/container_contract.go
Vladimir Domnich 03bff785d2 [#293] container: Add IterateContainerSizes method
Add method that allows to iterate over estimation records.

Update tests to assert that list of estimations built with existing methods
is identical to estimations from iterator.

Signed-off-by: Vladimir Domnich <v.domnich@yadro.com>
2023-01-16 14:40:21 +03:00

851 lines
24 KiB
Go

package container
import (
"github.com/TrueCloudLab/frostfs-contract/common"
"github.com/nspcc-dev/neo-go/pkg/interop"
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
"github.com/nspcc-dev/neo-go/pkg/interop/convert"
"github.com/nspcc-dev/neo-go/pkg/interop/iterator"
"github.com/nspcc-dev/neo-go/pkg/interop/native/crypto"
"github.com/nspcc-dev/neo-go/pkg/interop/native/management"
"github.com/nspcc-dev/neo-go/pkg/interop/native/std"
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
)
type (
storageNode struct {
info []byte
}
Container struct {
value []byte
sig interop.Signature
pub interop.PublicKey
token []byte
}
ExtendedACL struct {
value []byte
sig interop.Signature
pub interop.PublicKey
token []byte
}
estimation struct {
from interop.PublicKey
size int
}
containerSizes struct {
cid []byte
estimations []estimation
}
)
const (
frostfsIDContractKey = "identityScriptHash"
balanceContractKey = "balanceScriptHash"
netmapContractKey = "netmapScriptHash"
nnsContractKey = "nnsScriptHash"
nnsRootKey = "nnsRoot"
nnsHasAliasKey = "nnsHasAlias"
notaryDisabledKey = "notary"
// RegistrationFeeKey is a key in netmap config which contains fee for container registration.
RegistrationFeeKey = "ContainerFee"
// AliasFeeKey is a key in netmap config which contains fee for nice-name registration.
AliasFeeKey = "ContainerAliasFee"
// V2 format
containerIDSize = 32 // SHA256 size
singleEstimatePrefix = "est"
estimateKeyPrefix = "cnr"
estimatePostfixSize = 10
// CleanupDelta contains the number of the last epochs for which container estimations are present.
CleanupDelta = 3
// TotalCleanupDelta contains the number of the epochs after which estimation
// will be removed by epoch tick cleanup if any of the nodes hasn't updated
// container size and/or container has been removed. It must be greater than CleanupDelta.
TotalCleanupDelta = CleanupDelta + 1
// NotFoundError is returned if container is missing.
NotFoundError = "container does not exist"
// default SOA record field values
defaultRefresh = 3600 // 1 hour
defaultRetry = 600 // 10 min
defaultExpire = 3600 * 24 * 365 * 10 // 10 years
defaultTTL = 3600 // 1 hour
)
var (
eACLPrefix = []byte("eACL")
)
// OnNEP11Payment is needed for registration with contract as the owner to work.
func OnNEP11Payment(a interop.Hash160, b int, c []byte, d interface{}) {
}
func _deploy(data interface{}, isUpdate bool) {
ctx := storage.GetContext()
if isUpdate {
args := data.([]interface{})
common.CheckVersion(args[len(args)-1].(int))
return
}
args := data.(struct {
notaryDisabled bool
addrNetmap interop.Hash160
addrBalance interop.Hash160
addrID interop.Hash160
addrNNS interop.Hash160
nnsRoot string
})
if len(args.addrNetmap) != interop.Hash160Len ||
len(args.addrBalance) != interop.Hash160Len ||
len(args.addrID) != interop.Hash160Len {
panic("incorrect length of contract script hash")
}
storage.Put(ctx, netmapContractKey, args.addrNetmap)
storage.Put(ctx, balanceContractKey, args.addrBalance)
storage.Put(ctx, frostfsIDContractKey, args.addrID)
storage.Put(ctx, nnsContractKey, args.addrNNS)
storage.Put(ctx, nnsRootKey, args.nnsRoot)
// initialize the way to collect signatures
storage.Put(ctx, notaryDisabledKey, args.notaryDisabled)
if args.notaryDisabled {
common.InitVote(ctx)
runtime.Log("container contract notary disabled")
}
// add NNS root for container alias domains
registerNiceNameTLD(args.addrNNS, args.nnsRoot)
runtime.Log("container contract initialized")
}
func registerNiceNameTLD(addrNNS interop.Hash160, nnsRoot string) {
isAvail := contract.Call(addrNNS, "isAvailable", contract.AllowCall|contract.ReadStates,
"container").(bool)
if !isAvail {
return
}
res := contract.Call(addrNNS, "register", contract.All,
nnsRoot, runtime.GetExecutingScriptHash(), "ops@nspcc.ru",
defaultRefresh, defaultRetry, defaultExpire, defaultTTL).(bool)
if !res {
panic("can't register NNS TLD")
}
}
// Update method updates contract source code and manifest. It can be invoked
// by committee only.
func Update(script []byte, manifest []byte, data interface{}) {
if !common.HasUpdateAccess() {
panic("only committee can update contract")
}
contract.Call(interop.Hash160(management.Hash), "update",
contract.All, script, manifest, common.AppendVersion(data))
runtime.Log("container contract updated")
}
// Put method creates a new container if it has been invoked by Alphabet nodes
// of the Inner Ring. Otherwise, it produces containerPut notification.
//
// Container should be a stable marshaled Container structure from API.
// Signature is a RFC6979 signature of the Container.
// PublicKey contains the public key of the signer.
// Token is optional and should be a stable marshaled SessionToken structure from
// API.
func Put(container []byte, signature interop.Signature, publicKey interop.PublicKey, token []byte) {
PutNamed(container, signature, publicKey, token, "", "")
}
// PutNamed is similar to put but also sets a TXT record in nns contract.
// Note that zone must exist.
func PutNamed(container []byte, signature interop.Signature,
publicKey interop.PublicKey, token []byte,
name, zone string) {
ctx := storage.GetContext()
notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool)
ownerID := ownerFromBinaryContainer(container)
containerID := crypto.Sha256(container)
frostfsIDContractAddr := storage.Get(ctx, frostfsIDContractKey).(interop.Hash160)
cnr := Container{
value: container,
sig: signature,
pub: publicKey,
token: token,
}
var (
needRegister bool
nnsContractAddr interop.Hash160
domain string
)
if name != "" {
if zone == "" {
zone = storage.Get(ctx, nnsRootKey).(string)
}
nnsContractAddr = storage.Get(ctx, nnsContractKey).(interop.Hash160)
domain = name + "." + zone
needRegister = checkNiceNameAvailable(nnsContractAddr, domain)
}
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, RegistrationFeeKey).(int)
balance := contract.Call(balanceContractAddr, "balanceOf", contract.ReadOnly, from).(int)
if name != "" {
aliasFee := contract.Call(netmapContractAddr, "config", contract.ReadOnly, AliasFeeKey).(int)
containerFee += aliasFee
}
if balance < containerFee*len(alphabet) {
panic("insufficient balance to create container")
}
if notaryDisabled {
nodeKey := common.InnerRingInvoker(alphabet)
if len(nodeKey) == 0 {
runtime.Notify("containerPut", container, signature, publicKey, token)
return
}
threshold := len(alphabet)*2/3 + 1
id := common.InvokeID([]interface{}{container, signature, publicKey}, []byte("put"))
n := common.Vote(ctx, id, nodeKey)
if n < threshold {
return
}
common.RemoveVotes(ctx, id)
} else {
multiaddr := common.AlphabetAddress()
common.CheckAlphabetWitness(multiaddr)
}
// todo: check if new container with unique container id
details := common.ContainerFeeTransferDetails(containerID)
for i := 0; i < len(alphabet); i++ {
node := alphabet[i]
to := contract.CreateStandardAccount(node)
contract.Call(balanceContractAddr, "transferX",
contract.All,
from,
to,
containerFee,
details,
)
}
addContainer(ctx, containerID, ownerID, cnr)
if name != "" {
if needRegister {
res := contract.Call(nnsContractAddr, "register", contract.All,
domain, runtime.GetExecutingScriptHash(), "ops@nspcc.ru",
defaultRefresh, defaultRetry, defaultExpire, defaultTTL).(bool)
if !res {
panic("can't register the domain " + domain)
}
}
contract.Call(nnsContractAddr, "addRecord", contract.All,
domain, 16 /* TXT */, std.Base58Encode(containerID))
key := append([]byte(nnsHasAliasKey), containerID...)
storage.Put(ctx, key, domain)
}
if len(token) == 0 { // if container created directly without session
contract.Call(frostfsIDContractAddr, "addKey", contract.All, ownerID, [][]byte{publicKey})
}
runtime.Log("added new container")
runtime.Notify("PutSuccess", containerID, publicKey)
}
// checkNiceNameAvailable checks if the nice name is available for the container.
// It panics if the name is taken. Returned value specifies if new domain registration is needed.
func checkNiceNameAvailable(nnsContractAddr interop.Hash160, domain string) bool {
isAvail := contract.Call(nnsContractAddr, "isAvailable",
contract.ReadStates|contract.AllowCall, domain).(bool)
if isAvail {
return true
}
owner := contract.Call(nnsContractAddr, "ownerOf",
contract.ReadStates|contract.AllowCall, domain).(string)
if owner != string(common.CommitteeAddress()) && owner != string(runtime.GetExecutingScriptHash()) {
panic("committee or container contract must own registered domain")
}
res := contract.Call(nnsContractAddr, "getRecords",
contract.ReadStates|contract.AllowCall, domain, 16 /* TXT */)
if res != nil {
panic("name is already taken")
}
return false
}
// Delete method removes a container from the contract storage if it has been
// invoked by Alphabet nodes of the Inner Ring. Otherwise, it produces
// containerDelete notification.
//
// Signature is a RFC6979 signature of the container ID.
// Token is optional and should be a stable marshaled SessionToken structure from
// API.
//
// If the container doesn't exist, it panics with NotFoundError.
func Delete(containerID []byte, signature interop.Signature, token []byte) {
ctx := storage.GetContext()
notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool)
ownerID := getOwnerByID(ctx, containerID)
if ownerID == nil {
return
}
if notaryDisabled {
alphabet := common.AlphabetNodes()
nodeKey := common.InnerRingInvoker(alphabet)
if len(nodeKey) == 0 {
runtime.Notify("containerDelete", containerID, signature, token)
return
}
threshold := len(alphabet)*2/3 + 1
id := common.InvokeID([]interface{}{containerID, signature}, []byte("delete"))
n := common.Vote(ctx, id, nodeKey)
if n < threshold {
return
}
common.RemoveVotes(ctx, id)
} else {
multiaddr := common.AlphabetAddress()
common.CheckAlphabetWitness(multiaddr)
}
key := append([]byte(nnsHasAliasKey), containerID...)
domain := storage.Get(ctx, key).(string)
if len(domain) != 0 {
storage.Delete(ctx, key)
// We should do `getRecord` first because NNS record could be deleted
// by other means (expiration, manual), thus leading to failing `deleteRecord`
// and inability to delete a container. We should also check if we own the record in case.
nnsContractAddr := storage.Get(ctx, nnsContractKey).(interop.Hash160)
res := contract.Call(nnsContractAddr, "getRecords", contract.ReadStates|contract.AllowCall, domain, 16 /* TXT */)
if res != nil && std.Base58Encode(containerID) == string(res.([]interface{})[0].(string)) {
contract.Call(nnsContractAddr, "deleteRecords", contract.All, domain, 16 /* TXT */)
}
}
removeContainer(ctx, containerID, ownerID)
runtime.Log("remove container")
runtime.Notify("DeleteSuccess", containerID)
}
// 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.
//
// If the container doesn't exist, it panics with NotFoundError.
func Get(containerID []byte) Container {
ctx := storage.GetReadOnlyContext()
cnt := getContainer(ctx, containerID)
if len(cnt.value) == 0 {
panic(NotFoundError)
}
return cnt
}
// Owner method returns a 25 byte Owner ID of the container.
//
// If the container doesn't exist, it panics with NotFoundError.
func Owner(containerID []byte) []byte {
ctx := storage.GetReadOnlyContext()
owner := getOwnerByID(ctx, containerID)
if owner == nil {
panic(NotFoundError)
}
return owner
}
// Count method returns the number of registered containers.
func Count() int {
count := 0
ctx := storage.GetReadOnlyContext()
it := storage.Find(ctx, []byte{}, storage.KeysOnly)
for iterator.Next(it) {
key := iterator.Value(it).([]byte)
// V2 format
if len(key) == containerIDSize {
count++
}
}
return count
}
// List method returns a list of all container IDs owned by the specified owner.
func List(owner []byte) [][]byte {
ctx := storage.GetReadOnlyContext()
if len(owner) == 0 {
return getAllContainers(ctx)
}
var list [][]byte
it := storage.Find(ctx, owner, storage.ValuesOnly)
for iterator.Next(it) {
id := iterator.Value(it).([]byte)
list = append(list, id)
}
return list
}
// SetEACL method sets a new extended ACL table related to the contract
// if it was invoked by Alphabet nodes of the Inner Ring. Otherwise, it produces
// setEACL notification.
//
// EACL should be a stable marshaled EACLTable structure from API.
// Signature is a RFC6979 signature of the Container.
// PublicKey contains the public key of the signer.
// Token is optional and should be a stable marshaled SessionToken structure from
// API.
//
// If the container doesn't exist, it panics with NotFoundError.
func SetEACL(eACL []byte, signature interop.Signature, publicKey interop.PublicKey, token []byte) {
ctx := storage.GetContext()
notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool)
// V2 format
// get container ID
offset := int(eACL[1])
offset = 2 + offset + 4
containerID := eACL[offset : offset+32]
ownerID := getOwnerByID(ctx, containerID)
if ownerID == nil {
panic(NotFoundError)
}
if notaryDisabled {
alphabet := common.AlphabetNodes()
nodeKey := common.InnerRingInvoker(alphabet)
if len(nodeKey) == 0 {
runtime.Notify("setEACL", eACL, signature, publicKey, token)
return
}
threshold := len(alphabet)*2/3 + 1
id := common.InvokeID([]interface{}{eACL}, []byte("setEACL"))
n := common.Vote(ctx, id, nodeKey)
if n < threshold {
return
}
common.RemoveVotes(ctx, id)
} else {
multiaddr := common.AlphabetAddress()
common.CheckAlphabetWitness(multiaddr)
}
rule := ExtendedACL{
value: eACL,
sig: signature,
pub: publicKey,
token: token,
}
key := append(eACLPrefix, containerID...)
common.SetSerialized(ctx, key, rule)
runtime.Log("success")
runtime.Notify("SetEACLSuccess", containerID, publicKey)
}
// EACL method returns a structure that contains a stable marshaled EACLTable structure,
// the signature, the public key of the extended ACL setter and a stable marshaled SessionToken
// structure if it was provided.
//
// If the container doesn't exist, it panics with NotFoundError.
func EACL(containerID []byte) ExtendedACL {
ctx := storage.GetReadOnlyContext()
ownerID := getOwnerByID(ctx, containerID)
if ownerID == nil {
panic(NotFoundError)
}
return getEACL(ctx, containerID)
}
// PutContainerSize method saves container size estimation in contract
// memory. It can be invoked only by Storage nodes from the network map. This method
// checks witness based on the provided public key of the Storage node.
//
// If the container doesn't exist, it panics with NotFoundError.
func PutContainerSize(epoch int, cid []byte, usedSize int, pubKey interop.PublicKey) {
ctx := storage.GetContext()
if getOwnerByID(ctx, cid) == nil {
panic(NotFoundError)
}
common.CheckWitness(pubKey)
if !isStorageNode(ctx, pubKey) {
panic("method must be invoked by storage node from network map")
}
key := estimationKey(epoch, cid, pubKey)
s := estimation{
from: pubKey,
size: usedSize,
}
storage.Put(ctx, key, std.Serialize(s))
updateEstimations(ctx, epoch, cid, pubKey, false)
runtime.Log("saved container size estimation")
}
// GetContainerSize method returns the container ID and a slice of container
// estimations. Container estimation includes the public key of the Storage Node
// that registered estimation and value of estimation.
//
// Use the ID obtained from ListContainerSizes method. Estimations are removed
// from contract storage every epoch, see NewEpoch method; therefore, this method
// can return different results during different epochs.
func GetContainerSize(id []byte) containerSizes {
ctx := storage.GetReadOnlyContext()
// V2 format
// this `id` expected to be from `ListContainerSizes`
// therefore it is not contains postfix, we ignore it in the cut.
ln := len(id)
cid := id[ln-containerIDSize : ln]
return getContainerSizeEstimation(ctx, id, cid)
}
// ListContainerSizes method returns the IDs of container size estimations
// that have been registered for the specified epoch.
func ListContainerSizes(epoch int) [][]byte {
ctx := storage.GetReadOnlyContext()
var buf interface{} = epoch
key := []byte(estimateKeyPrefix)
key = append(key, buf.([]byte)...)
it := storage.Find(ctx, key, storage.KeysOnly)
uniq := map[string]struct{}{}
for iterator.Next(it) {
storageKey := iterator.Value(it).([]byte)
ln := len(storageKey)
storageKey = storageKey[:ln-estimatePostfixSize]
uniq[string(storageKey)] = struct{}{}
}
var result [][]byte
for k := range uniq {
result = append(result, []byte(k))
}
return result
}
// IterateContainerSizes method returns iterator over container size estimations
// that have been registered for the specified epoch.
func IterateContainerSizes(epoch int) iterator.Iterator {
ctx := storage.GetReadOnlyContext()
var buf interface{} = epoch
key := []byte(estimateKeyPrefix)
key = append(key, buf.([]byte)...)
return storage.Find(ctx, key, storage.DeserializeValues)
}
// NewEpoch method removes all container size estimations from epoch older than
// epochNum + 3. It can be invoked only by NewEpoch method of the Netmap contract.
func NewEpoch(epochNum int) {
ctx := storage.GetContext()
notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool)
if notaryDisabled {
indirectCall := common.FromKnownContract(
ctx,
runtime.GetCallingScriptHash(),
netmapContractKey,
)
if !indirectCall {
panic("method must be invoked by inner ring")
}
} else {
multiaddr := common.AlphabetAddress()
common.CheckAlphabetWitness(multiaddr)
}
cleanupContainers(ctx, epochNum)
}
// StartContainerEstimation method produces StartEstimation notification.
// It can be invoked only by Alphabet nodes of the Inner Ring.
func StartContainerEstimation(epoch int) {
ctx := storage.GetContext()
notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool)
var ( // for invocation collection without notary
alphabet []interop.PublicKey
nodeKey []byte
)
if notaryDisabled {
alphabet = common.AlphabetNodes()
nodeKey = common.InnerRingInvoker(alphabet)
if len(nodeKey) == 0 {
panic("method must be invoked by inner ring")
}
} else {
multiaddr := common.AlphabetAddress()
common.CheckAlphabetWitness(multiaddr)
}
if notaryDisabled {
threshold := len(alphabet)*2/3 + 1
id := common.InvokeID([]interface{}{epoch}, []byte("startEstimation"))
n := common.Vote(ctx, id, nodeKey)
if n < threshold {
return
}
common.RemoveVotes(ctx, id)
}
runtime.Notify("StartEstimation", epoch)
runtime.Log("notification has been produced")
}
// StopContainerEstimation method produces StopEstimation notification.
// It can be invoked only by Alphabet nodes of the Inner Ring.
func StopContainerEstimation(epoch int) {
ctx := storage.GetContext()
notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool)
var ( // for invocation collection without notary
alphabet []interop.PublicKey
nodeKey []byte
)
if notaryDisabled {
alphabet = common.AlphabetNodes()
nodeKey = common.InnerRingInvoker(alphabet)
if len(nodeKey) == 0 {
panic("method must be invoked by inner ring")
}
} else {
multiaddr := common.AlphabetAddress()
common.CheckAlphabetWitness(multiaddr)
}
if notaryDisabled {
threshold := len(alphabet)*2/3 + 1
id := common.InvokeID([]interface{}{epoch}, []byte("stopEstimation"))
n := common.Vote(ctx, id, nodeKey)
if n < threshold {
return
}
common.RemoveVotes(ctx, id)
}
runtime.Notify("StopEstimation", epoch)
runtime.Log("notification has been produced")
}
// Version returns the version of the contract.
func Version() int {
return common.Version
}
func addContainer(ctx storage.Context, id, owner []byte, container Container) {
containerListKey := append(owner, id...)
storage.Put(ctx, containerListKey, id)
common.SetSerialized(ctx, id, container)
}
func removeContainer(ctx storage.Context, id []byte, owner []byte) {
containerListKey := append(owner, id...)
storage.Delete(ctx, containerListKey)
storage.Delete(ctx, id)
}
func getAllContainers(ctx storage.Context) [][]byte {
var list [][]byte
it := storage.Find(ctx, []byte{}, storage.KeysOnly)
for iterator.Next(it) {
key := iterator.Value(it).([]byte) // it MUST BE `storage.KeysOnly`
// V2 format
if len(key) == containerIDSize {
list = append(list, key)
}
}
return list
}
func getEACL(ctx storage.Context, cid []byte) ExtendedACL {
key := append(eACLPrefix, cid...)
data := storage.Get(ctx, key)
if data != nil {
return std.Deserialize(data.([]byte)).(ExtendedACL)
}
return ExtendedACL{value: []byte{}, sig: interop.Signature{}, pub: interop.PublicKey{}, token: []byte{}}
}
func getContainer(ctx storage.Context, cid []byte) Container {
data := storage.Get(ctx, cid)
if data != nil {
return std.Deserialize(data.([]byte)).(Container)
}
return Container{value: []byte{}, sig: interop.Signature{}, pub: interop.PublicKey{}, token: []byte{}}
}
func getOwnerByID(ctx storage.Context, cid []byte) []byte {
container := getContainer(ctx, cid)
if len(container.value) == 0 {
return nil
}
return ownerFromBinaryContainer(container.value)
}
func ownerFromBinaryContainer(container []byte) []byte {
// V2 format
offset := int(container[1])
offset = 2 + offset + 4 // version prefix + version size + owner prefix
return container[offset : offset+25] // offset + size of owner
}
func estimationKey(epoch int, cid []byte, key interop.PublicKey) []byte {
var buf interface{} = epoch
hash := crypto.Ripemd160(key)
result := []byte(estimateKeyPrefix)
result = append(result, buf.([]byte)...)
result = append(result, cid...)
return append(result, hash[:estimatePostfixSize]...)
}
func getContainerSizeEstimation(ctx storage.Context, key, cid []byte) containerSizes {
var estimations []estimation
it := storage.Find(ctx, key, storage.ValuesOnly|storage.DeserializeValues)
for iterator.Next(it) {
est := iterator.Value(it).(estimation)
estimations = append(estimations, est)
}
return containerSizes{
cid: cid,
estimations: estimations,
}
}
// isStorageNode looks into _previous_ epoch network map, because storage node
// announces container size estimation of the previous epoch.
func isStorageNode(ctx storage.Context, key interop.PublicKey) bool {
netmapContractAddr := storage.Get(ctx, netmapContractKey).(interop.Hash160)
snapshot := contract.Call(netmapContractAddr, "snapshot", contract.ReadOnly, 1).([]storageNode)
for i := range snapshot {
// V2 format
nodeInfo := snapshot[i].info
nodeKey := nodeInfo[2:35] // offset:2, len:33
if common.BytesEqual(key, nodeKey) {
return true
}
}
return false
}
func updateEstimations(ctx storage.Context, epoch int, cid []byte, pub interop.PublicKey, isUpdate bool) {
h := crypto.Ripemd160(pub)
estKey := append([]byte(singleEstimatePrefix), cid...)
estKey = append(estKey, h...)
var newEpochs []int
rawList := storage.Get(ctx, estKey).([]byte)
if rawList != nil {
epochs := std.Deserialize(rawList).([]int)
for _, oldEpoch := range epochs {
if !isUpdate && epoch-oldEpoch > CleanupDelta {
key := append([]byte(estimateKeyPrefix), convert.ToBytes(oldEpoch)...)
key = append(key, cid...)
key = append(key, h[:estimatePostfixSize]...)
storage.Delete(ctx, key)
} else {
newEpochs = append(newEpochs, oldEpoch)
}
}
}
newEpochs = append(newEpochs, epoch)
common.SetSerialized(ctx, estKey, newEpochs)
}
func cleanupContainers(ctx storage.Context, epoch int) {
it := storage.Find(ctx, []byte(estimateKeyPrefix), storage.KeysOnly)
for iterator.Next(it) {
k := iterator.Value(it).([]byte)
// V2 format
nbytes := k[len(estimateKeyPrefix) : len(k)-containerIDSize-estimatePostfixSize]
var n interface{} = nbytes
if epoch-n.(int) > TotalCleanupDelta {
storage.Delete(ctx, k)
}
}
}