diff --git a/contracts/container/config.yml b/contracts/container/config.yml index 1d61ae7c..bd89dc1e 100644 --- a/contracts/container/config.yml +++ b/contracts/container/config.yml @@ -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"] @@ -28,3 +28,7 @@ events: parameters: - name: epoch type: Integer + - name: NodesUpdate + parameters: + - name: ContainerID + type: hash256 diff --git a/contracts/container/containerconst/const.go b/contracts/container/containerconst/const.go index 147ddec2..706b99ce 100644 --- a/contracts/container/containerconst/const.go +++ b/contracts/container/containerconst/const.go @@ -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" ) diff --git a/contracts/container/contract.go b/contracts/container/contract.go index 72d75884..bcb704bf 100644 --- a/contracts/container/contract.go +++ b/contracts/container/contract.go @@ -66,6 +66,8 @@ const ( containerKeyPrefix = 'x' ownerKeyPrefix = 'o' deletedKeyPrefix = 'd' + nodesPrefix = 'n' + nextEpochNodesPrefix = 'u' estimatePostfixSize = 10 // default SOA record field values. @@ -491,6 +493,110 @@ 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 + c := storage.Find(ctx, append([]byte{nextEpochNodesPrefix}, cID...), storage.KeysOnly|storage.Backwards) + if iterator.Next(c) { + counterRaw := iterator.Value(c).([]byte)[1+interop.Hash256Len:] + counter = counterFromBE(counterRaw) + } + + 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, counterBE(counter)...) + storage.Put(ctx, storageKey, publicKey) + } +} + +func counterBE(c int) []byte { + rawCounter := std.Serialize(c) + res := []byte{rawCounter[0], rawCounter[1]} // first is type, second is length + for i := len(rawCounter) - 1; i > 1; i-- { // LE to BE + res = append(res, rawCounter[i]) + } + + return res +} + +func counterFromBE(b []byte) int { + res := []byte{b[0], b[1]} // first is type, second is length + for i := len(b) - 1; i > 1; i-- { // BE to LE + res = append(res, b[i]) + } + + return std.Deserialize(res).(int) +} + +// 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...) + + 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) + } + + 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. diff --git a/contracts/container/contract.nef b/contracts/container/contract.nef index 1423863e..38d16119 100755 Binary files a/contracts/container/contract.nef and b/contracts/container/contract.nef differ diff --git a/contracts/container/doc.go b/contracts/container/doc.go index 6030702e..a0513a2f 100644 --- a/contracts/container/doc.go +++ b/contracts/container/doc.go @@ -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 @@ -103,6 +110,13 @@ Key-value storage format: - 'est' + [20]byte -> [] 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' -> interop.PublicKey + one of the container nodes' public key, counter is NEO serialized int _but_ LE + is converted to BE + - 'u' -> 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 NEO serialized int _but_ LE is converted to BE # Setting To handle some events, the contract refers to other contracts. diff --git a/contracts/container/manifest.json b/contracts/container/manifest.json index 40cf49f8..f2eaf4f8 100755 --- a/contracts/container/manifest.json +++ b/contracts/container/manifest.json @@ -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} \ No newline at end of file +{"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":4434,"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":5074,"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":5334,"parameters":[{"name":"id","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"iterateAllContainerSizes","offset":5707,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"InteropInterface","safe":true},{"name":"iterateContainerSizes","offset":5609,"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":5448,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"Array","safe":true},{"name":"newEpoch","offset":5759,"parameters":[{"name":"epochNum","type":"Integer"}],"returntype":"Void","safe":false},{"name":"nodes","offset":4714,"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":5132,"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":4812,"parameters":[{"name":"eACL","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"startContainerEstimation","offset":5789,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"Void","safe":false},{"name":"stopContainerEstimation","offset":5870,"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":5950,"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} \ No newline at end of file diff --git a/rpc/container/rpcbinding.go b/rpc/container/rpcbinding.go index 98bfcb5e..d574c349 100644 --- a/rpc/container/rpcbinding.go +++ b/rpc/container/rpcbinding.go @@ -79,6 +79,11 @@ type StopEstimationEvent struct { Epoch *big.Int } +// NodesUpdateEvent represents "NodesUpdate" event emitted by the contract. +type NodesUpdateEvent struct { + ContainerID util.Uint256 +} + // Invoker is used by ContractReader to call various safe methods. type Invoker interface { Call(contract util.Uint160, operation string, params ...any) (*result.Invoke, error) @@ -199,6 +204,20 @@ func (c *ContractReader) ListContainerSizes(epoch *big.Int) ([][]byte, error) { return unwrap.ArrayOfBytes(c.invoker.Call(c.hash, "listContainerSizes", epoch)) } +// Nodes invokes `nodes` method of contract. +func (c *ContractReader) Nodes(cID util.Uint256) (uuid.UUID, result.Iterator, error) { + return unwrap.SessionIterator(c.invoker.Call(c.hash, "nodes", cID)) +} + +// NodesExpanded is similar to Nodes (uses the same contract +// method), but can be useful if the server used doesn't support sessions and +// doesn't expand iterators. It creates a script that will get the specified +// number of result items from the iterator right in the VM and return them to +// you. It's only limited by VM stack and GAS available for RPC invocations. +func (c *ContractReader) NodesExpanded(cID util.Uint256, _numOfIteratorItems int) ([]stackitem.Item, error) { + return unwrap.Array(c.invoker.CallAndExpandIterator(c.hash, "nodes", _numOfIteratorItems, cID)) +} + // Owner invokes `owner` method of contract. func (c *ContractReader) Owner(containerID []byte) ([]byte, error) { return unwrap.Bytes(c.invoker.Call(c.hash, "owner", containerID)) @@ -209,6 +228,50 @@ func (c *ContractReader) Version() (*big.Int, error) { return unwrap.BigInt(c.invoker.Call(c.hash, "version")) } +// AddNextEpochNodes creates a transaction invoking `addNextEpochNodes` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) AddNextEpochNodes(cID util.Uint256, publicKeys keys.PublicKeys) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "addNextEpochNodes", cID, publicKeys) +} + +// AddNextEpochNodesTransaction creates a transaction invoking `addNextEpochNodes` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) AddNextEpochNodesTransaction(cID util.Uint256, publicKeys keys.PublicKeys) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "addNextEpochNodes", cID, publicKeys) +} + +// AddNextEpochNodesUnsigned creates a transaction invoking `addNextEpochNodes` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) AddNextEpochNodesUnsigned(cID util.Uint256, publicKeys keys.PublicKeys) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "addNextEpochNodes", nil, cID, publicKeys) +} + +// CommitContainerListUpdate creates a transaction invoking `commitContainerListUpdate` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) CommitContainerListUpdate(cID util.Uint256) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "commitContainerListUpdate", cID) +} + +// CommitContainerListUpdateTransaction creates a transaction invoking `commitContainerListUpdate` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) CommitContainerListUpdateTransaction(cID util.Uint256) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "commitContainerListUpdate", cID) +} + +// CommitContainerListUpdateUnsigned creates a transaction invoking `commitContainerListUpdate` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) CommitContainerListUpdateUnsigned(cID util.Uint256) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "commitContainerListUpdate", nil, cID) +} + // Delete creates a transaction invoking `delete` method of the contract. // This transaction is signed and immediately sent to the network. // The values returned are its hash, ValidUntilBlock value and error if any. @@ -1004,3 +1067,65 @@ func (e *StopEstimationEvent) FromStackItem(item *stackitem.Array) error { return nil } + +// NodesUpdateEventsFromApplicationLog retrieves a set of all emitted events +// with "NodesUpdate" name from the provided [result.ApplicationLog]. +func NodesUpdateEventsFromApplicationLog(log *result.ApplicationLog) ([]*NodesUpdateEvent, error) { + if log == nil { + return nil, errors.New("nil application log") + } + + var res []*NodesUpdateEvent + for i, ex := range log.Executions { + for j, e := range ex.Events { + if e.Name != "NodesUpdate" { + continue + } + event := new(NodesUpdateEvent) + err := event.FromStackItem(e.Item) + if err != nil { + return nil, fmt.Errorf("failed to deserialize NodesUpdateEvent from stackitem (execution #%d, event #%d): %w", i, j, err) + } + res = append(res, event) + } + } + + return res, nil +} + +// FromStackItem converts provided [stackitem.Array] to NodesUpdateEvent or +// returns an error if it's not possible to do to so. +func (e *NodesUpdateEvent) FromStackItem(item *stackitem.Array) error { + if item == nil { + return errors.New("nil item") + } + arr, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not an array") + } + if len(arr) != 1 { + return errors.New("wrong number of structure elements") + } + + var ( + index = -1 + err error + ) + index++ + e.ContainerID, err = func(item stackitem.Item) (util.Uint256, error) { + b, err := item.TryBytes() + if err != nil { + return util.Uint256{}, err + } + u, err := util.Uint256DecodeBytesBE(b) + if err != nil { + return util.Uint256{}, err + } + return u, nil + }(arr[index]) + if err != nil { + return fmt.Errorf("field ContainerID: %w", err) + } + + return nil +} diff --git a/tests/container_test.go b/tests/container_test.go index bd615db5..aee5c646 100644 --- a/tests/container_test.go +++ b/tests/container_test.go @@ -2,7 +2,9 @@ package tests import ( "bytes" + "crypto/rand" "crypto/sha256" + "fmt" "math/big" "path" "testing" @@ -12,6 +14,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neofs-contract/common" "github.com/nspcc-dev/neofs-contract/contracts/container/containerconst" @@ -560,3 +563,103 @@ func requireEstimationsMatch(t *testing.T, expected []estimation, actual []estim require.True(t, found, "expected estimation from %x to be present", e.from) } } + +func TestContainerList(t *testing.T) { + c, _, _ := newContainerInvoker(t, false) + + t.Run("happy path", func(t *testing.T) { + cID := make([]byte, sha256.Size) + _, err := rand.Read(cID) + require.NoError(t, err) + + const nodesNumber = 1024 + var containerList []any + for range nodesNumber { + s := c.NewAccount(t).(neotest.SingleSigner) + containerList = append(containerList, s.Account().PublicKey().Bytes()) + } + + c.Invoke(t, stackitem.Null{}, "addNextEpochNodes", cID, containerList[:nodesNumber/2]) + c.Invoke(t, stackitem.Null{}, "addNextEpochNodes", cID, containerList[nodesNumber/2:]) + c.Invoke(t, stackitem.Null{}, "commitContainerListUpdate", cID) + stack, err := c.TestInvoke(t, "nodes", cID) + require.NoError(t, err) + + fromContract := stackIteratorToBytesArray(t, stack) + require.ElementsMatch(t, containerList, fromContract) + + // change container + for i := nodesNumber / 2; i < nodesNumber; i++ { + s := c.NewAccount(t).(neotest.SingleSigner) + containerList[i] = s.Account().PublicKey().Bytes() + } + + c.Invoke(t, stackitem.Null{}, "addNextEpochNodes", cID, containerList) + c.Invoke(t, stackitem.Null{}, "commitContainerListUpdate", cID) + stack, err = c.TestInvoke(t, "nodes", cID) + require.NoError(t, err) + + fromContract = stackIteratorToBytesArray(t, stack) + require.ElementsMatch(t, containerList, fromContract) + + // clean container up + c.Invoke(t, stackitem.Null{}, "commitContainerListUpdate", cID) + + stack, err = c.TestInvoke(t, "nodes", cID) + require.NoError(t, err) + + fromContract = stackIteratorToBytesArray(t, stack) + require.Empty(t, fromContract) + + // cleaning cleaned container + c.Invoke(t, stackitem.Null{}, "commitContainerListUpdate", cID) + + stack, err = c.TestInvoke(t, "nodes", cID) + require.NoError(t, err) + + fromContract = stackIteratorToBytesArray(t, stack) + require.Empty(t, fromContract) + }) + + t.Run("invalid container ID", func(t *testing.T) { + badCID := []byte{1, 2, 3, 4, 5} + + c.InvokeFail(t, fmt.Sprintf("%s: length: %d", containerconst.ErrorInvalidContainerID, len(badCID)), "addNextEpochNodes", badCID, nil) + c.InvokeFail(t, fmt.Sprintf("%s: length: %d", containerconst.ErrorInvalidContainerID, len(badCID)), "commitContainerListUpdate", badCID) + c.InvokeFail(t, fmt.Sprintf("%s: length: %d", containerconst.ErrorInvalidContainerID, len(badCID)), "nodes", badCID) + }) + + t.Run("invalid public key", func(t *testing.T) { + cID := make([]byte, sha256.Size) + _, err := rand.Read(cID) + require.NoError(t, err) + + badPublicKey := []byte{1, 2, 3, 4, 5} + + c.InvokeFail(t, fmt.Sprintf("%s: length: %d", containerconst.ErrorInvalidPublicKey, len(badPublicKey)), "addNextEpochNodes", cID, []any{badPublicKey}) + }) + + t.Run("not alphabet", func(t *testing.T) { + cID := make([]byte, sha256.Size) + _, err := rand.Read(cID) + require.NoError(t, err) + + notAlphabet := c.Executor.NewInvoker(c.Hash, c.NewAccount(t)) + notAlphabet.InvokeFail(t, common.ErrAlphabetWitnessFailed, "addNextEpochNodes", cID, nil) + notAlphabet.InvokeFail(t, common.ErrAlphabetWitnessFailed, "commitContainerListUpdate", cID) + }) +} + +func stackIteratorToBytesArray(t *testing.T, stack *vm.Stack) [][]byte { + i, ok := stack.Pop().Value().(*storage.Iterator) + require.True(t, ok) + + res := make([][]byte, 0) + for i.Next() { + b, err := i.Value().TryBytes() + require.NoError(t, err) + + res = append(res, b) + } + return res +}