Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

container: add container lists to the contract #438

Merged
merged 1 commit into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
roman-khimov marked this conversation as resolved.
Show resolved Hide resolved
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:]...)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
newKey := append([]byte{nodesPrefix}, newNode.key[1:]...)
newKey := append([]byte{nodesPrefix}, newNode.key[len(nextEpochNodesPrefix):]...)

or also make a const

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not possible for now (it is a char literally, no length), likely be resolved automatically when we resolve other threads

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
Loading