Skip to content

Commit

Permalink
container: add container lists to the contract
Browse files Browse the repository at this point in the history
It must be handled by the Alphabet and be updated after every epoch counter is
increased. Updating is done in two stage (filling a container list and commiting
it) to prevent any stack size/memory restrictions. Closes #412.

Signed-off-by: Pavel Karpy <[email protected]>
  • Loading branch information
carpawell committed Oct 13, 2024
1 parent a8d5e00 commit 660756f
Show file tree
Hide file tree
Showing 8 changed files with 498 additions and 2 deletions.
6 changes: 5 additions & 1 deletion contracts/container/config.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: "NeoFS Container"
safemethods: ["alias", "count", "containersOf", "get", "owner", "list", "eACL", "getContainerSize", "listContainerSizes", "iterateContainerSizes", "iterateAllContainerSizes", "version"]
safemethods: ["alias", "count", "containersOf", "get", "owner", "list", "nodes", "replicasNumbers", "eACL", "getContainerSize", "listContainerSizes", "iterateContainerSizes", "iterateAllContainerSizes", "version"]
permissions:
- methods: ["update", "addKey", "transferX",
"register", "registerTLD", "addRecord", "deleteRecords", "subscribeForNewEpoch"]
Expand Down Expand Up @@ -28,3 +28,7 @@ events:
parameters:
- name: epoch
type: Integer
- name: NodesUpdate
parameters:
- name: ContainerID
type: hash256
10 changes: 10 additions & 0 deletions contracts/container/containerconst/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,14 @@ const (

// ErrorDeleted is returned on attempt to create previously deleted container.
ErrorDeleted = "container was previously deleted"

// ErrorTooBigNumberOfNodes is returned if it is assumed that REP or number of REPS
// in container's placement policy is bigger than 255.
ErrorTooBigNumberOfNodes = "number of container nodes exceeds limits"

// ErrorInvalidContainerID is returned on an attempt to work with incorrect container ID.
ErrorInvalidContainerID = "invalid container id"

// ErrorInvalidPublicKey is returned on an attempt to work with an incorrect public key.
ErrorInvalidPublicKey = "invalid public key"
)
176 changes: 176 additions & 0 deletions contracts/container/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ const (
containerKeyPrefix = 'x'
ownerKeyPrefix = 'o'
deletedKeyPrefix = 'd'
nodesPrefix = 'n'
replicasNumberPrefix = 'r'
nextEpochNodesPrefix = 'u'
estimatePostfixSize = 10

// default SOA record field values.
Expand Down Expand Up @@ -491,6 +494,179 @@ func List(owner []byte) [][]byte {
return list
}

// maxNumOfREPs is a max supported number of REP value in container's policy
// and also a max number of REPs.
const maxNumOfREPs = 255

// AddNextEpochNodes accumulates passed nodes as container members for the next
// epoch to be committed using [CommitContainerListUpdate]. Nodes must be
// grouped by selector index from placement policy (SELECT clauses). Results of
// the call operation can be received via [Nodes]. This method must be called
// only when a container list is changed, otherwise nothing should be done.
// Call must be signed by the Alphabet nodes.
func AddNextEpochNodes(cID interop.Hash256, placementVector uint8, publicKeys []interop.PublicKey) {
if len(cID) != interop.Hash256Len {
panic(cst.ErrorInvalidContainerID + ": length: " + std.Itoa10(len(cID)))
}
// nolint:staticcheck
if placementVector >= maxNumOfREPs {
panic(cst.ErrorTooBigNumberOfNodes + ": " + std.Itoa10(int(placementVector)))
}
ctx := storage.GetContext()
validatePlacementIndex(ctx, cID, placementVector)

multiaddr := common.AlphabetAddress()
common.CheckAlphabetWitness(multiaddr)

commonPrefix := append([]byte{nextEpochNodesPrefix}, cID...)
commonPrefix = append(commonPrefix, placementVector)

counter := 0
c := storage.Find(ctx, commonPrefix, storage.RemovePrefix|storage.KeysOnly|storage.Backwards)
if iterator.Next(c) {
counter = counterFromBytes(iterator.Value(c).([]byte))
}

for _, publicKey := range publicKeys {
if len(publicKey) != interop.PublicKeyCompressedLen {
panic(cst.ErrorInvalidPublicKey + ": length: " + std.Itoa10(len(publicKey)))
}

counter++

storageKey := append(commonPrefix, counterToBytes(counter)...)
storage.Put(ctx, storageKey, publicKey)
}
}

func counterToBytes(counter int) []byte {
var anyCounter any = counter
res := anyCounter.([]byte)

switch len(res) {
case 0:
return []byte{0, 0}
case 1:
return []byte{0, res[0]}
default:
// only 2 bytes are expected, it should be ensured on the upper levels
}

// BE for correct sorting
first := res[0]
res[0] = res[1]
res[1] = first

return res
}

func counterFromBytes(counter []byte) int {
first := counter[0]
counter[0] = counter[1]
counter[1] = first

var anyCounter any = counter

return anyCounter.(int)
}

func validatePlacementIndex(ctx storage.Context, cID interop.Hash256, inx uint8) {
if inx == 0 {
return
}

commonPrefix := append([]byte{nextEpochNodesPrefix}, cID...)
iter := storage.Find(ctx, append(commonPrefix, inx-1), storage.None)
if !iterator.Next(iter) {
panic("invalid placement vector: " + std.Itoa10(int(inx-1)) + " index not found but " + std.Itoa10(int(inx)) + " requested")
}
}

// CommitContainerListUpdate commits container list changes made by
// [AddNextEpochNodes] calls in advance. Replicas must correspond to
// ordered placement policy (REP clauses). If no [AddNextEpochNodes]
// have been made, it clears container list. Makes "ContainerUpdate"
// notification with container ID after successful list change.
// Call must be signed by the Alphabet nodes.
func CommitContainerListUpdate(cID interop.Hash256, replicas []uint8) {
if len(cID) != interop.Hash256Len {
panic(cst.ErrorInvalidContainerID + ": length: " + std.Itoa10(len(cID)))
}

ctx := storage.GetContext()
multiaddr := common.AlphabetAddress()
common.CheckAlphabetWitness(multiaddr)

oldNodesPrefix := append([]byte{nodesPrefix}, cID...)
newNodesPrefix := append([]byte{nextEpochNodesPrefix}, cID...)
replicasPrefix := append([]byte{replicasNumberPrefix}, cID...)

oldNodes := storage.Find(ctx, oldNodesPrefix, storage.KeysOnly)
for iterator.Next(oldNodes) {
oldNode := iterator.Value(oldNodes).(string)
storage.Delete(ctx, oldNode)
}

newNodes := storage.Find(ctx, newNodesPrefix, storage.None)
for iterator.Next(newNodes) {
newNode := iterator.Value(newNodes).(struct {
key []byte
val []byte
})

storage.Delete(ctx, newNode.key)

newKey := append([]byte{nodesPrefix}, newNode.key[1:]...)
storage.Put(ctx, newKey, newNode.val)
}

rr := storage.Find(ctx, replicasPrefix, storage.KeysOnly)
for iterator.Next(rr) {
oldReplicasNumber := iterator.Value(rr).([]byte)
storage.Delete(ctx, oldReplicasNumber)
}

// nolint:gosimple // https://github.com/nspcc-dev/neo-go/issues/3608
if replicas != nil {
for i, replica := range replicas {
if replica > maxNumOfREPs {
panic(cst.ErrorTooBigNumberOfNodes + ": " + std.Itoa10(int(replica)))
}

storage.Put(ctx, append(replicasPrefix, uint8(i)), replica)
}
}

runtime.Notify("NodesUpdate", cID)
}

// ReplicasNumbers returns iterator over saved by [CommitContainerListUpdate]
// container's replicas from placement policy.
func ReplicasNumbers(cID interop.Hash256) iterator.Iterator {
if len(cID) != interop.Hash256Len {
panic(cst.ErrorInvalidContainerID + ": length: " + std.Itoa10(len(cID)))
}

ctx := storage.GetReadOnlyContext()

return storage.Find(ctx, append([]byte{replicasNumberPrefix}, cID...), storage.ValuesOnly)
}

// Nodes returns iterator over members of the container. The list is handled
// by the Alphabet nodes and must be updated via [AddNextEpochNodes] and
// [CommitContainerListUpdate] calls.
func Nodes(cID interop.Hash256, placementVector uint8) iterator.Iterator {
if len(cID) != interop.Hash256Len {
panic(cst.ErrorInvalidContainerID + ": length: " + std.Itoa10(len(cID)))
}

ctx := storage.GetReadOnlyContext()
key := append([]byte{nodesPrefix}, cID...)
key = append(key, placementVector)

return storage.Find(ctx, key, storage.ValuesOnly)
}

// 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.
Expand Down
Binary file modified contracts/container/contract.nef
Binary file not shown.
15 changes: 15 additions & 0 deletions contracts/container/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ and validate container ownership, signature and token if present.
- name: token
type: ByteArray
nodesUpdate notification. This notification is produced when a container roster
is changed. Triggered only by the Alphabet at the beginning of epoch.
name: NodesUpdate
- name: ContainerID
type: hash256
setEACL notification. This notification is produced when a container owner wants
to update an extended ACL of a container. Alphabet nodes of the Inner Ring catch
the notification and validate container ownership, signature and token if
Expand Down Expand Up @@ -103,6 +110,14 @@ Key-value storage format:
- 'est' + [20]byte -> []<epoch>
list of NeoFS epochs when particular storage node sent estimations. Suffix is
RIPEMD-160 hash of the storage node's public key (interop.PublicKey).
- 'n<cid><placement_index><counter>' -> interop.PublicKey
one of the container nodes' public key, counter is 2-bytes long BE
- 'u<cid><placement_index><counter>' -> interop.PublicKey
one of the container nodes' public key _for the next epoch_, they will become
the current ones (with the 'n' prefix) once the Alphabet handles epoch update.
Counter is 2-bytes long BE
- 'r'<cid><placement_index> -> int (not bigger than uint8)
REP clause from placement policy for <placement index>
# Setting
To handle some events, the contract refers to other contracts.
Expand Down
2 changes: 1 addition & 1 deletion contracts/container/manifest.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"name":"NeoFS Container","abi":{"methods":[{"name":"_initialize","offset":0,"parameters":[],"returntype":"Void","safe":false},{"name":"_deploy","offset":83,"parameters":[{"name":"data","type":"Any"},{"name":"isUpdate","type":"Boolean"}],"returntype":"Void","safe":false},{"name":"alias","offset":3697,"parameters":[{"name":"cid","type":"ByteArray"}],"returntype":"String","safe":true},{"name":"containersOf","offset":3837,"parameters":[{"name":"owner","type":"ByteArray"}],"returntype":"InteropInterface","safe":true},{"name":"count","offset":3792,"parameters":[],"returntype":"Integer","safe":true},{"name":"delete","offset":3187,"parameters":[{"name":"containerID","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"token","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"eACL","offset":4249,"parameters":[{"name":"containerID","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"get","offset":3584,"parameters":[{"name":"containerID","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"getContainerSize","offset":4509,"parameters":[{"name":"id","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"iterateAllContainerSizes","offset":4882,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"InteropInterface","safe":true},{"name":"iterateContainerSizes","offset":4784,"parameters":[{"name":"epoch","type":"Integer"},{"name":"cid","type":"Hash256"}],"returntype":"InteropInterface","safe":true},{"name":"list","offset":3891,"parameters":[{"name":"owner","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"listContainerSizes","offset":4623,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"Array","safe":true},{"name":"newEpoch","offset":4934,"parameters":[{"name":"epochNum","type":"Integer"}],"returntype":"Void","safe":false},{"name":"onNEP11Payment","offset":1646,"parameters":[{"name":"a","type":"Hash160"},{"name":"b","type":"Integer"},{"name":"c","type":"ByteArray"},{"name":"d","type":"Any"}],"returntype":"Void","safe":false},{"name":"owner","offset":3646,"parameters":[{"name":"containerID","type":"ByteArray"}],"returntype":"ByteArray","safe":true},{"name":"put","offset":2037,"parameters":[{"name":"container","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"putContainerSize","offset":4307,"parameters":[{"name":"epoch","type":"Integer"},{"name":"cid","type":"ByteArray"},{"name":"usedSize","type":"Integer"},{"name":"pubKey","type":"PublicKey"}],"returntype":"Void","safe":false},{"name":"putNamed","offset":2053,"parameters":[{"name":"container","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"},{"name":"name","type":"String"},{"name":"zone","type":"String"}],"returntype":"Void","safe":false},{"name":"setEACL","offset":3987,"parameters":[{"name":"eACL","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"startContainerEstimation","offset":4964,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"Void","safe":false},{"name":"stopContainerEstimation","offset":5045,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"Void","safe":false},{"name":"update","offset":1904,"parameters":[{"name":"script","type":"ByteArray"},{"name":"manifest","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"version","offset":5125,"parameters":[],"returntype":"Integer","safe":true}],"events":[{"name":"PutSuccess","parameters":[{"name":"containerID","type":"Hash256"},{"name":"publicKey","type":"PublicKey"}]},{"name":"DeleteSuccess","parameters":[{"name":"containerID","type":"ByteArray"}]},{"name":"SetEACLSuccess","parameters":[{"name":"containerID","type":"ByteArray"},{"name":"publicKey","type":"PublicKey"}]},{"name":"StartEstimation","parameters":[{"name":"epoch","type":"Integer"}]},{"name":"StopEstimation","parameters":[{"name":"epoch","type":"Integer"}]}]},"features":{},"groups":[],"permissions":[{"contract":"*","methods":["update","addKey","transferX","register","registerTLD","addRecord","deleteRecords","subscribeForNewEpoch"]}],"supportedstandards":[],"trusts":[],"extra":null}
{"name":"NeoFS Container","abi":{"methods":[{"name":"_initialize","offset":0,"parameters":[],"returntype":"Void","safe":false},{"name":"_deploy","offset":83,"parameters":[{"name":"data","type":"Any"},{"name":"isUpdate","type":"Boolean"}],"returntype":"Void","safe":false},{"name":"addNextEpochNodes","offset":3987,"parameters":[{"name":"cID","type":"Hash256"},{"name":"placementVector","type":"Integer"},{"name":"publicKeys","type":"Array"}],"returntype":"Void","safe":false},{"name":"alias","offset":3697,"parameters":[{"name":"cid","type":"ByteArray"}],"returntype":"String","safe":true},{"name":"commitContainerListUpdate","offset":4592,"parameters":[{"name":"cID","type":"Hash256"},{"name":"replicas","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"containersOf","offset":3837,"parameters":[{"name":"owner","type":"ByteArray"}],"returntype":"InteropInterface","safe":true},{"name":"count","offset":3792,"parameters":[],"returntype":"Integer","safe":true},{"name":"delete","offset":3187,"parameters":[{"name":"containerID","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"token","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"eACL","offset":5547,"parameters":[{"name":"containerID","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"get","offset":3584,"parameters":[{"name":"containerID","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"getContainerSize","offset":5807,"parameters":[{"name":"id","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"iterateAllContainerSizes","offset":6180,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"InteropInterface","safe":true},{"name":"iterateContainerSizes","offset":6082,"parameters":[{"name":"epoch","type":"Integer"},{"name":"cid","type":"Hash256"}],"returntype":"InteropInterface","safe":true},{"name":"list","offset":3891,"parameters":[{"name":"owner","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"listContainerSizes","offset":5921,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"Array","safe":true},{"name":"newEpoch","offset":6232,"parameters":[{"name":"epochNum","type":"Integer"}],"returntype":"Void","safe":false},{"name":"nodes","offset":5161,"parameters":[{"name":"cID","type":"Hash256"},{"name":"placementVector","type":"Integer"}],"returntype":"InteropInterface","safe":true},{"name":"onNEP11Payment","offset":1646,"parameters":[{"name":"a","type":"Hash160"},{"name":"b","type":"Integer"},{"name":"c","type":"ByteArray"},{"name":"d","type":"Any"}],"returntype":"Void","safe":false},{"name":"owner","offset":3646,"parameters":[{"name":"containerID","type":"ByteArray"}],"returntype":"ByteArray","safe":true},{"name":"put","offset":2037,"parameters":[{"name":"container","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"putContainerSize","offset":5605,"parameters":[{"name":"epoch","type":"Integer"},{"name":"cid","type":"ByteArray"},{"name":"usedSize","type":"Integer"},{"name":"pubKey","type":"PublicKey"}],"returntype":"Void","safe":false},{"name":"putNamed","offset":2053,"parameters":[{"name":"container","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"},{"name":"name","type":"String"},{"name":"zone","type":"String"}],"returntype":"Void","safe":false},{"name":"replicasNumbers","offset":5063,"parameters":[{"name":"cID","type":"Hash256"}],"returntype":"InteropInterface","safe":true},{"name":"setEACL","offset":5285,"parameters":[{"name":"eACL","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"startContainerEstimation","offset":6262,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"Void","safe":false},{"name":"stopContainerEstimation","offset":6343,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"Void","safe":false},{"name":"update","offset":1904,"parameters":[{"name":"script","type":"ByteArray"},{"name":"manifest","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"version","offset":6423,"parameters":[],"returntype":"Integer","safe":true}],"events":[{"name":"PutSuccess","parameters":[{"name":"containerID","type":"Hash256"},{"name":"publicKey","type":"PublicKey"}]},{"name":"DeleteSuccess","parameters":[{"name":"containerID","type":"ByteArray"}]},{"name":"SetEACLSuccess","parameters":[{"name":"containerID","type":"ByteArray"},{"name":"publicKey","type":"PublicKey"}]},{"name":"StartEstimation","parameters":[{"name":"epoch","type":"Integer"}]},{"name":"StopEstimation","parameters":[{"name":"epoch","type":"Integer"}]},{"name":"NodesUpdate","parameters":[{"name":"ContainerID","type":"Hash256"}]}]},"features":{},"groups":[],"permissions":[{"contract":"*","methods":["update","addKey","transferX","register","registerTLD","addRecord","deleteRecords","subscribeForNewEpoch"]}],"supportedstandards":[],"trusts":[],"extra":null}
Loading

0 comments on commit 660756f

Please sign in to comment.