From 1af936eb55ee7f49161879711caadf4be7cf3f71 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Wed, 28 Sep 2022 11:41:51 +0400 Subject: [PATCH] [#269] netmap: Return same type from reading methods There is a need to return similar structure of information about the storage nodes from the contract storage readers. In previous implementation some methods didn't return node state which can differ with the one encoded in the node's BLOB. Define `Node` structure of the information about the storage nodes recorded in the contract storage. Return `[]Node` from all related methods. Also improve docs of touched contract methods. Signed-off-by: Leonard Lyubich --- CHANGELOG.md | 10 ++ netmap/netmap_contract.go | 303 +++++++++++++++++++++++--------------- tests/netmap_test.go | 42 +++--- 3 files changed, 215 insertions(+), 140 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e005dec..e59c9533 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,22 @@ Changelog for NeoFS Contract ## Unrelease +### Added +- Support `MAINTENANCE` state of storage nodes (#269) + +### Changed +- `netmap.Snapshot` and all similar methods return (#269) + ### Updated - NNS contract now sets domain expiration based on `register` arguments (#262) ### Fixed - NNS `renew` now can only be done by the domain owner +### Updating from v0.15.x +Update deployed `Netmap` contract using `Update` method: storage of the contract +has been incompatibly changed. + ## [0.15.5] - 2022-08-23 ### Updated diff --git a/netmap/netmap_contract.go b/netmap/netmap_contract.go index 8c25e4cc..61120325 100644 --- a/netmap/netmap_contract.go +++ b/netmap/netmap_contract.go @@ -13,25 +13,36 @@ import ( "github.com/nspcc-dev/neofs-contract/common" ) -type ( - storageNode struct { - info []byte - } +// NodeState is an enumeration for node states. +type NodeState int - netmapNode struct { - node storageNode - state NodeState - } +// Various Node states +const ( + _ NodeState = iota - // NodeState is an enumeration for node states. - NodeState int + // NodeStateOnline stands for nodes that are in full network and + // operational availability. + NodeStateOnline - record struct { - key []byte - val []byte - } + // NodeStateOffline stands for nodes that are in network unavailability. + NodeStateOffline + + // NodeStateMaintenance stands for nodes under maintenance with partial + // network availability. + NodeStateMaintenance ) +// Node groups data related to NeoFS storage nodes registered in the NeoFS +// network. The information is stored in the current contract. +type Node struct { + // Information about the node encoded according to the NeoFS binary + // protocol. + BLOB []byte + + // Current node state. + State NodeState +} + const ( notaryDisabledKey = "notary" innerRingKey = "innerring" @@ -51,14 +62,6 @@ const ( cleanupEpochMethod = "newEpoch" ) -const ( - // V2 format - _ NodeState = iota - OnlineState - OfflineState - MaintenanceState -) - var ( configPrefix = []byte("config") candidatePrefix = []byte("candidate") @@ -105,7 +108,7 @@ func _deploy(data interface{}, isUpdate bool) { prefix := []byte(snapshotKeyPrefix) for i := 0; i < DefaultSnapshotCount; i++ { - common.SetSerialized(ctx, append(prefix, byte(i)), []storageNode{}) + common.SetSerialized(ctx, append(prefix, byte(i)), []Node{}) } storage.Put(ctx, snapshotCurrentIDKey, 0) @@ -191,8 +194,11 @@ func UpdateInnerRing(keys []interop.PublicKey) { common.SetSerialized(ctx, innerRingKey, keys) } -// AddPeerIR method tries to add a new candidate to the network map. -// It should only be invoked in notary-enabled environment by the alphabet. +// AddPeerIR accepts Alphabet calls in the notary-enabled contract setting and +// behaves similar to AddPeer in the notary-disabled one. +// +// AddPeerIR MUST NOT be called in notary-disabled contract setting. +// AddPeerIR MUST be called by the Alphabet member only. func AddPeerIR(nodeInfo []byte) { ctx := storage.GetContext() notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) @@ -202,19 +208,40 @@ func AddPeerIR(nodeInfo []byte) { common.CheckAlphabetWitness(common.AlphabetAddress()) - addToNetmap(ctx, storageNode{info: nodeInfo}) - publicKey := nodeInfo[2:35] // V2 format: offset:2, len:33 - runtime.Notify("AddPeerSuccess", interop.PublicKey(publicKey)) + + addToNetmap(ctx, publicKey, Node{ + BLOB: nodeInfo, + State: NodeStateOnline, + }) } -// AddPeer method adds a new candidate to the next network map if it was invoked -// by Alphabet node. If it was invoked by a node candidate, it produces AddPeer -// notification. Otherwise, the method throws panic. +// AddPeer accepts information about the network map candidate in the NeoFS +// binary protocol format, identifies the caller and behaves depending on different +// conditions listed below. +// +// Contract settings: +// +// (1) notary-enabled +// (2) notary-disabled +// +// Callers: +// +// (a) candidate himself, if node's public key corresponds to the signer +// (b) Alphabet member +// (c) others // -// If the candidate already exists, its info is updated. -// NodeInfo argument contains a stable marshaled version of netmap.NodeInfo -// structure. +// AddPeer case-by-case behavior: +// +// (1a) does nothing +// (1b) panics. Notice that AddPeerIR MUST be used for this purpose. +// (2a) throws AddPeer notification with the provided BLOB +// (2b) accepts Alphabet vote. If the threshold of votes is reached, adds +// new element to the candidate set, and throws AddPeerSuccess notification. +// (c) panics +// +// Candidate MUST call AddPeer with "online" state in its descriptor. Alphabet +// members MUST NOT call AddPeer with any other states. func AddPeer(nodeInfo []byte) { ctx := storage.GetContext() notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) @@ -242,8 +269,9 @@ func AddPeer(nodeInfo []byte) { return } - candidate := storageNode{ - info: nodeInfo, + candidate := Node{ + BLOB: nodeInfo, + State: NodeStateOnline, } if notaryDisabled { @@ -259,28 +287,63 @@ func AddPeer(nodeInfo []byte) { common.RemoveVotes(ctx, id) } - addToNetmap(ctx, candidate) - runtime.Notify("AddPeerSuccess", interop.PublicKey(publicKey)) + addToNetmap(ctx, publicKey, candidate) +} + +// updates state of the network map candidate by its public key in the contract +// storage, and throws UpdateStateSuccess notification after this. +// +// State MUST be from the NodeState enum. +func updateCandidateState(ctx storage.Context, publicKey interop.PublicKey, state NodeState) { + switch state { + case NodeStateOffline: + removeFromNetmap(ctx, publicKey) + runtime.Log("remove storage node from the network map") + case NodeStateOnline, NodeStateMaintenance: + updateNetmapState(ctx, publicKey, state) + runtime.Log("update state of the network map candidate") + default: + panic("unsupported state") + } + + runtime.Notify("UpdateStateSuccess", publicKey, state) } -// UpdateState method updates the state of a node from the network map candidate list. -// For notary-ENABLED environment, tx must be signed by both storage node and alphabet. -// To force update without storage node signature, see `UpdateStateIR`. +// UpdateState accepts new state to be assigned to network map candidate +// identified by the given public key, identifies the signer and behaves +// depending on different conditions listed below. +// +// Contract settings: +// +// (1) notary-enabled +// (2) notary-disabled +// +// Signers: // -// For notary-DISABLED environment, the behaviour depends on who signed the transaction: -// 1. If it was signed by alphabet, go into voting. -// 2. If it was signed by a storage node, emit `UpdateState` notification. -// 2. Fail in any other case. +// (a) candidate himself only, if provided public key corresponds to the signer +// (b) Alphabet member only +// (ab) both candidate and Alphabet member +// (c) others // -// The behaviour can be summarized in the following table: -// | notary \ Signer | Storage node | Alphabet | Both | -// | ENABLED | FAIL | FAIL | OK | -// | DISABLED | NOTIFICATION | OK | OK (same as alphabet) | -// State argument defines node state. The only supported state now is (2) -- -// offline state. Node is removed from the network map candidate list. +// UpdateState case-by-case behavior: // -// Method panics when invoked with unsupported states. -func UpdateState(state int, publicKey interop.PublicKey) { +// (1a) panics +// (1b) like (1a) +// (1ab) updates candidate's state in the contract storage (*), and throws +// UpdateStateSuccess with the provided key and new state +// (2a) throws UpdateState notification with the provided key and new state +// (2b) accepts Alphabet vote. If the threshold of votes is reached, behaves +// like (1ab). +// (c) panics +// +// (*) Candidate is removed from the candidate set if state is NodeStateOffline. +// Any other state is written into candidate's descriptor in the contract storage. +// If requested candidate is missing, panic occurs. Throws UpdateStateSuccess +// notification on success. +// +// State MUST be from the NodeState enum. Public key MUST be +// interop.PublicKeyCompressedLen bytes. +func UpdateState(state NodeState, publicKey interop.PublicKey) { if len(publicKey) != interop.PublicKeyCompressedLen { panic("incorrect public key") } @@ -314,23 +377,15 @@ func UpdateState(state int, publicKey interop.PublicKey) { common.CheckAlphabetWitness(common.AlphabetAddress()) } - st := NodeState(state) - switch st { - case OfflineState: - removeFromNetmap(ctx, publicKey) - runtime.Log("remove storage node from the network map") - case MaintenanceState, OnlineState: - updateNetmapState(ctx, publicKey, st) - runtime.Log("move storage node to a maintenance state") - default: - panic("unsupported state") - } - - runtime.Notify("UpdateStateSuccess", publicKey, state) + updateCandidateState(ctx, publicKey, state) } -// UpdateStateIR method tries to change the node state in the network map. -// Should only be invoked in notary-enabled environment by alphabet. +// UpdateStateIR accepts Alphabet calls in the notary-enabled contract setting +// and behaves similar to UpdateState, but does not require candidate's +// signature presence. +// +// UpdateStateIR MUST NOT be called in notary-disabled contract setting. +// UpdateStateIR MUST be called by the Alphabet member only. func UpdateStateIR(state NodeState, publicKey interop.PublicKey) { ctx := storage.GetContext() notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) @@ -340,15 +395,7 @@ func UpdateStateIR(state NodeState, publicKey interop.PublicKey) { common.CheckAlphabetWitness(common.AlphabetAddress()) - switch state { - case OfflineState: - removeFromNetmap(ctx, publicKey) - case MaintenanceState, OnlineState: - updateNetmapState(ctx, publicKey, state) - default: - panic("unsupported state") - } - runtime.Notify("UpdateStateSuccess", publicKey, state) + updateCandidateState(ctx, publicKey, state) } // NewEpoch method changes the epoch number up to the provided epochNum argument. It can @@ -397,7 +444,7 @@ func NewEpoch(epochNum int) { panic("invalid epoch") // ignore invocations with invalid epoch } - dataOnlineState := filterNetmap(ctx, OnlineState) + dataOnlineState := filterNetmap(ctx) runtime.Log("process new epoch") @@ -430,30 +477,40 @@ func LastEpochBlock() int { return storage.Get(ctx, snapshotBlockKey).(int) } -// Netmap method returns a list of structures that contain a byte array of a stable -// marshalled netmap.NodeInfo structure. These structures contain Storage nodes -// of the current epoch. -func Netmap() []storageNode { +// Netmap returns set of information about the storage nodes representing a network +// map in the current epoch. +// +// Current state of each node is represented in the State field. It MAY differ +// with the state encoded into BLOB field, in this case binary encoded state +// MUST NOT be processed. +func Netmap() []Node { ctx := storage.GetReadOnlyContext() id := storage.Get(ctx, snapshotCurrentIDKey).(int) return getSnapshot(ctx, snapshotKeyPrefix+string([]byte{byte(id)})) } -// NetmapCandidates method returns a list of structures that contain the node state -// and a byte array of a stable marshalled netmap.NodeInfo structure. -// These structures contain Storage node candidates for the next epoch. -func NetmapCandidates() []netmapNode { +// NetmapCandidates returns set of information about the storage nodes +// representing candidates for the network map in the coming epoch. +// +// Current state of each node is represented in the State field. It MAY differ +// with the state encoded into BLOB field, in this case binary encoded state +// MUST NOT be processed. +func NetmapCandidates() []Node { ctx := storage.GetReadOnlyContext() return getNetmapNodes(ctx) } -// Snapshot method returns a list of structures that contain the node state -// (online: 1) and a byte array of a stable marshalled netmap.NodeInfo structure. -// These structures contain Storage nodes of the specified epoch. +// Snapshot returns set of information about the storage nodes representing a network +// map in (current-diff)-th epoch. +// +// Diff MUST NOT be negative. Diff MUST be less than maximum number of network +// map snapshots stored in the contract. The limit is a contract setting, +// DefaultSnapshotCount by default. See UpdateSnapshotCount for details. // -// Netmap contract contains only two recent network map snapshots: current and -// previous epoch. For diff bigger than 1 or less than 0, the method throws panic. -func Snapshot(diff int) []storageNode { +// Current state of each node is represented in the State field. It MAY differ +// with the state encoded into BLOB field, in this case binary encoded state +// MUST NOT be processed. +func Snapshot(diff int) []Node { ctx := storage.GetReadOnlyContext() count := getSnapshotCount(ctx) if diff < 0 || count <= diff { @@ -475,6 +532,8 @@ func getSnapshotCount(ctx storage.Context) int { // Otherwise, history is extended with empty snapshots, so // `Snapshot` method can return invalid results for `diff = new-old` epochs // until `diff` epochs have passed. +// +// Count MUST NOT be negative. func UpdateSnapshotCount(count int) { common.CheckAlphabetWitness(common.AlphabetAddress()) if count < 0 { @@ -552,13 +611,12 @@ func moveSnapshot(ctx storage.Context, from, to int) { storage.Put(ctx, keyTo, data) } -// SnapshotByEpoch method returns a list of structures that contain the node state -// (online: 1) and a byte array of a stable marshalled netmap.NodeInfo structure. -// These structures contain Storage nodes of the specified epoch. +// SnapshotByEpoch returns set of information about the storage nodes representing +// a network map in the given epoch. // -// Netmap contract contains only two recent network map snapshot: current and -// previous epoch. For all others epoch method throws panic. -func SnapshotByEpoch(epoch int) []storageNode { +// Behaves like Snapshot: it is called after difference with the current epoch is +// calculated. +func SnapshotByEpoch(epoch int) []Node { ctx := storage.GetReadOnlyContext() currentEpoch := storage.Get(ctx, snapshotEpoch).(int) @@ -610,6 +668,11 @@ func SetConfig(id, key, val []byte) { runtime.Log("configuration has been updated") } +type record struct { + key []byte + val []byte +} + // ListConfig returns an array of structures that contain key and value of all // NeoFS configuration records. Key and value are both byte arrays. func ListConfig() []record { @@ -636,19 +699,15 @@ func Version() int { return common.Version } -func addToNetmap(ctx storage.Context, n storageNode) { - var ( - newNode = n.info - newNodeKey = newNode[2:35] - storageKey = append(candidatePrefix, newNodeKey...) - - node = netmapNode{ - node: n, - state: OnlineState, - } - ) - +// serializes and stores the given Node by its public key in the contract storage, +// and throws AddPeerSuccess notification after this. +// +// Public key MUST match the one encoded in BLOB field. +func addToNetmap(ctx storage.Context, publicKey []byte, node Node) { + storageKey := append(candidatePrefix, publicKey...) storage.Put(ctx, storageKey, std.Serialize(node)) + + runtime.Notify("AddPeerSuccess", interop.PublicKey(publicKey)) } func removeFromNetmap(ctx storage.Context, key interop.PublicKey) { @@ -662,46 +721,46 @@ func updateNetmapState(ctx storage.Context, key interop.PublicKey, state NodeSta if raw == nil { panic("peer is missing") } - node := std.Deserialize(raw).(netmapNode) - node.state = state + node := std.Deserialize(raw).(Node) + node.State = state storage.Put(ctx, storageKey, std.Serialize(node)) } -func filterNetmap(ctx storage.Context, st NodeState) []storageNode { +func filterNetmap(ctx storage.Context) []Node { var ( netmap = getNetmapNodes(ctx) - result = []storageNode{} + result = []Node{} ) for i := 0; i < len(netmap); i++ { item := netmap[i] - if item.state == st { - result = append(result, item.node) + if item.State != NodeStateOffline { + result = append(result, item) } } return result } -func getNetmapNodes(ctx storage.Context) []netmapNode { - result := []netmapNode{} +func getNetmapNodes(ctx storage.Context) []Node { + result := []Node{} it := storage.Find(ctx, candidatePrefix, storage.ValuesOnly|storage.DeserializeValues) for iterator.Next(it) { - node := iterator.Value(it).(netmapNode) + node := iterator.Value(it).(Node) result = append(result, node) } return result } -func getSnapshot(ctx storage.Context, key string) []storageNode { +func getSnapshot(ctx storage.Context, key string) []Node { data := storage.Get(ctx, key) if data != nil { - return std.Deserialize(data.([]byte)).([]storageNode) + return std.Deserialize(data.([]byte)).([]Node) } - return []storageNode{} + return []Node{} } func getConfig(ctx storage.Context, key interface{}) interface{} { diff --git a/tests/netmap_test.go b/tests/netmap_test.go index e56ce044..d28624cb 100644 --- a/tests/netmap_test.go +++ b/tests/netmap_test.go @@ -62,6 +62,7 @@ type testNodeInfo struct { signer neotest.SingleSigner pub []byte raw []byte + state netmap.NodeState } func dummyNodeInfo(acc neotest.Signer) testNodeInfo { @@ -75,6 +76,7 @@ func dummyNodeInfo(acc neotest.Signer) testNodeInfo { signer: s, pub: pub, raw: ni, + state: netmap.NodeStateOnline, } } @@ -136,7 +138,7 @@ func TestNewEpoch(t *testing.T) { for j := range nodes[i-1] { if rand.Int()%3 == 0 { cNm.Invoke(t, stackitem.Null{}, "updateStateIR", - int64(netmap.OfflineState), nodes[i-1][j].pub) + int64(netmap.NodeStateOffline), nodes[i-1][j].pub) } else { current = append(current, nodes[i-1][j]) } @@ -289,18 +291,22 @@ func checkSnapshot(t *testing.T, s *vm.Stack, nodes []testNodeInfo) { require.True(t, ok, "expected array") require.Equal(t, len(nodes), len(arr), "expected %d nodes", len(nodes)) - actual := make([][]byte, len(nodes)) - expected := make([][]byte, len(nodes)) + actual := make([]netmap.Node, len(nodes)) + expected := make([]netmap.Node, len(nodes)) for i := range nodes { n, ok := arr[i].Value().([]stackitem.Item) require.True(t, ok, "expected node struct") - require.Equal(t, 1, len(n), "expected single field") + require.Equalf(t, 2, len(n), "expected %d field(s)", 2) - raw, ok := n[0].Value().([]byte) - require.True(t, ok, "expected bytes") + require.IsType(t, []byte{}, n[0].Value()) - actual[i] = raw - expected[i] = nodes[i].raw + state, err := n[1].TryInteger() + require.NoError(t, err) + + actual[i].BLOB = n[0].Value().([]byte) + actual[i].State = netmap.NodeState(state.Int64()) + expected[i].BLOB = nodes[i].raw + expected[i].State = nodes[i].state } require.ElementsMatch(t, expected, actual, "snapshot is different") @@ -313,7 +319,7 @@ func TestUpdateStateIR(t *testing.T) { pub := acc.(neotest.SingleSigner).Account().PrivateKey().PublicKey().Bytes() t.Run("can't move online, need addPeerIR", func(t *testing.T) { - cNm.InvokeFail(t, "peer is missing", "updateStateIR", int64(netmap.OnlineState), pub) + cNm.InvokeFail(t, "peer is missing", "updateStateIR", int64(netmap.NodeStateOnline), pub) }) dummyInfo := dummyNodeInfo(acc) @@ -325,7 +331,7 @@ func TestUpdateStateIR(t *testing.T) { t.Run("must be signed by the alphabet", func(t *testing.T) { cAcc := cNm.WithSigners(acc) - cAcc.InvokeFail(t, common.ErrAlphabetWitnessFailed, "updateStateIR", int64(netmap.OfflineState), pub) + cAcc.InvokeFail(t, common.ErrAlphabetWitnessFailed, "updateStateIR", int64(netmap.NodeStateOffline), pub) }) t.Run("invalid state", func(t *testing.T) { cNm.InvokeFail(t, "unsupported state", "updateStateIR", int64(42), pub) @@ -334,7 +340,7 @@ func TestUpdateStateIR(t *testing.T) { checkNetmapCandidates(t, cNm, 2) // Move the first node offline. - cNm.Invoke(t, stackitem.Null{}, "updateStateIR", int64(netmap.OfflineState), pub) + cNm.Invoke(t, stackitem.Null{}, "updateStateIR", int64(netmap.NodeStateOffline), pub) checkNetmapCandidates(t, cNm, 1) checkState := func(expected netmap.NodeState) { @@ -348,16 +354,16 @@ func TestUpdateStateIR(t *testing.T) { // Move the second node in the maintenance state. pub1 := acc1.(neotest.SingleSigner).Account().PrivateKey().PublicKey().Bytes() t.Run("maintenance -> add peer", func(t *testing.T) { - cNm.Invoke(t, stackitem.Null{}, "updateStateIR", int64(netmap.MaintenanceState), pub1) - checkState(netmap.MaintenanceState) + cNm.Invoke(t, stackitem.Null{}, "updateStateIR", int64(netmap.NodeStateMaintenance), pub1) + checkState(netmap.NodeStateMaintenance) cNm.Invoke(t, stackitem.Null{}, "addPeerIR", dummyInfo1.raw) - checkState(netmap.OnlineState) + checkState(netmap.NodeStateOnline) }) t.Run("maintenance -> online", func(t *testing.T) { - cNm.Invoke(t, stackitem.Null{}, "updateStateIR", int64(netmap.MaintenanceState), pub1) - checkState(netmap.MaintenanceState) - cNm.Invoke(t, stackitem.Null{}, "updateStateIR", int64(netmap.OnlineState), pub1) - checkState(netmap.OnlineState) + cNm.Invoke(t, stackitem.Null{}, "updateStateIR", int64(netmap.NodeStateMaintenance), pub1) + checkState(netmap.NodeStateMaintenance) + cNm.Invoke(t, stackitem.Null{}, "updateStateIR", int64(netmap.NodeStateOnline), pub1) + checkState(netmap.NodeStateOnline) }) }