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
increase. 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 <carpawell@nspcc.ru>
  • Loading branch information
carpawell committed Oct 4, 2024
1 parent a8d5e00 commit 948f97b
Show file tree
Hide file tree
Showing 8 changed files with 345 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", "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
6 changes: 6 additions & 0 deletions contracts/container/containerconst/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,10 @@ const (

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

// 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"
)
92 changes: 92 additions & 0 deletions contracts/container/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ const (
containerKeyPrefix = 'x'
ownerKeyPrefix = 'o'
deletedKeyPrefix = 'd'
nodesPrefix = 'n'
nextEpochNodesPrefix = 'u'
estimatePostfixSize = 10

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

// AddNextEpochNodes accumulates passed nodes as container members for the next
// epoch to be committed using [CommitContainerListUpdate]. 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, publicKeys []interop.PublicKey) {
if len(cID) != interop.Hash256Len {
panic(cst.ErrorInvalidContainerID + ": length: " + std.Itoa10(len(cID)))
}

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

counter := 0
counterKey := append([]byte{nextEpochNodesPrefix, 'i'}, cID...)
if counterRaw := storage.Get(ctx, counterKey); counterRaw != nil {
counter = counterRaw.(int)
}

commonPrefix := append([]byte{nextEpochNodesPrefix}, cID...)
for _, publicKey := range publicKeys {
if len(publicKey) != interop.PublicKeyCompressedLen {
panic(cst.ErrorInvalidPublicKey + ": length: " + std.Itoa10(len(publicKey)))
}

counter++

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

storage.Put(ctx, counterKey, counter)
}

// CommitContainerListUpdate commits container list changes made by
// [AddNextEpochNodes] calls in advance. 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) {
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...)
newCounterKey := append([]byte{nextEpochNodesPrefix, 'i'}, 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)
}
if newCounter := storage.Get(ctx, newCounterKey); newCounter != nil {
storage.Delete(ctx, newCounterKey)
}

runtime.Notify("NodesUpdate", cID)
}

// 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) iterator.Iterator {
if len(cID) != interop.Hash256Len {
panic(cst.ErrorInvalidContainerID + ": length: " + std.Itoa10(len(cID)))
}

ctx := storage.GetReadOnlyContext()
return storage.Find(ctx, append([]byte{nodesPrefix}, cID...), 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.
14 changes: 14 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,13 @@ 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><counter>' -> interop.PublicKey
one of the container nodes' public key
- 'ui<cid>' -> int
current counter for new epoch container list
- 'u<cid><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.
# 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":"publicKeys","type":"Array"}],"returntype":"Void","safe":false},{"name":"alias","offset":3697,"parameters":[{"name":"cid","type":"ByteArray"}],"returntype":"String","safe":true},{"name":"commitContainerListUpdate","offset":4250,"parameters":[{"name":"cID","type":"Hash256"}],"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":4937,"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":5197,"parameters":[{"name":"id","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"iterateAllContainerSizes","offset":5570,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"InteropInterface","safe":true},{"name":"iterateContainerSizes","offset":5472,"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":5311,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"Array","safe":true},{"name":"newEpoch","offset":5622,"parameters":[{"name":"epochNum","type":"Integer"}],"returntype":"Void","safe":false},{"name":"nodes","offset":4577,"parameters":[{"name":"cID","type":"Hash256"}],"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":4995,"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":4675,"parameters":[{"name":"eACL","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"startContainerEstimation","offset":5652,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"Void","safe":false},{"name":"stopContainerEstimation","offset":5733,"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":5813,"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 948f97b

Please sign in to comment.