[#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 <ctulhurider@gmail.com>
This commit is contained in:
Leonard Lyubich 2022-09-28 11:41:51 +04:00 committed by LeL
parent 4d487a069e
commit d95bc53589
3 changed files with 215 additions and 140 deletions

View file

@ -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

View file

@ -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.
//
// If the candidate already exists, its info is updated.
// NodeInfo argument contains a stable marshaled version of netmap.NodeInfo
// structure.
// 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
//
// 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)
}
// 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`.
// updates state of the network map candidate by its public key in the contract
// storage, and throws UpdateStateSuccess notification after this.
//
// 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.
// 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 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.
//
// 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.
// Contract settings:
//
// Method panics when invoked with unsupported states.
func UpdateState(state int, publicKey interop.PublicKey) {
// (1) notary-enabled
// (2) notary-disabled
//
// Signers:
//
// (a) candidate himself only, if provided public key corresponds to the signer
// (b) Alphabet member only
// (ab) both candidate and Alphabet member
// (c) others
//
// UpdateState case-by-case behavior:
//
// (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.
//
// 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 {
// 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.
//
// 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{} {

View file

@ -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)
})
}