forked from TrueCloudLab/frostfs-contract
8b1c8eb689
With `NetmapCandidate` storage nodes can monitor if they are expected to be in the netmap at next epoch or not. This is also important for monitoring when some nodes does not bootstrap immediately. Signed-off-by: Alex Vanin <alexey@nspcc.ru>
594 lines
13 KiB
Go
594 lines
13 KiB
Go
package netmapcontract
|
|
|
|
import (
|
|
"github.com/nspcc-dev/neo-go/pkg/interop"
|
|
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
|
|
"github.com/nspcc-dev/neo-go/pkg/interop/iterator"
|
|
"github.com/nspcc-dev/neo-go/pkg/interop/native/crypto"
|
|
"github.com/nspcc-dev/neo-go/pkg/interop/native/management"
|
|
"github.com/nspcc-dev/neo-go/pkg/interop/native/std"
|
|
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
|
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
|
|
"github.com/nspcc-dev/neofs-contract/common"
|
|
)
|
|
|
|
type (
|
|
storageNode struct {
|
|
info []byte
|
|
}
|
|
|
|
netmapNode struct {
|
|
node storageNode
|
|
state nodeState
|
|
}
|
|
|
|
nodeState int
|
|
|
|
record struct {
|
|
key []byte
|
|
val []byte
|
|
}
|
|
)
|
|
|
|
const (
|
|
version = 1
|
|
|
|
netmapKey = "netmap"
|
|
configuredKey = "initconfig"
|
|
notaryDisabledKey = "notary"
|
|
innerRingKey = "innerring"
|
|
|
|
snapshot0Key = "snapshotCurrent"
|
|
snapshot1Key = "snapshotPrevious"
|
|
snapshotEpoch = "snapshotEpoch"
|
|
|
|
containerContractKey = "containerScriptHash"
|
|
balanceContractKey = "balanceScriptHash"
|
|
|
|
cleanupEpochMethod = "newEpoch"
|
|
)
|
|
|
|
const (
|
|
_ nodeState = iota
|
|
onlineState
|
|
offlineState
|
|
)
|
|
|
|
var (
|
|
configPrefix = []byte("config")
|
|
)
|
|
|
|
// _deploy function sets up initial list of inner ring public keys.
|
|
func _deploy(data interface{}, isUpdate bool) {
|
|
args := data.([]interface{})
|
|
notaryDisabled := args[0].(bool)
|
|
owner := args[1].(interop.Hash160)
|
|
addrBalance := args[2].(interop.Hash160)
|
|
addrContainer := args[3].(interop.Hash160)
|
|
keys := args[4].([]interop.PublicKey)
|
|
|
|
ctx := storage.GetContext()
|
|
|
|
if !common.HasUpdateAccess(ctx) {
|
|
panic("only owner can reinitialize contract")
|
|
}
|
|
|
|
if len(addrBalance) != 20 || len(addrContainer) != 20 {
|
|
panic("init: incorrect length of contract script hash")
|
|
}
|
|
|
|
storage.Put(ctx, common.OwnerKey, owner)
|
|
|
|
// epoch number is a little endian int, it doesn't need to be serialized
|
|
storage.Put(ctx, snapshotEpoch, 0)
|
|
|
|
// simplified: this used for const sysfee in AddPeer method
|
|
common.SetSerialized(ctx, netmapKey, []netmapNode{})
|
|
common.SetSerialized(ctx, snapshot0Key, []netmapNode{})
|
|
common.SetSerialized(ctx, snapshot1Key, []netmapNode{})
|
|
|
|
storage.Put(ctx, balanceContractKey, addrBalance)
|
|
storage.Put(ctx, containerContractKey, addrContainer)
|
|
|
|
// initialize the way to collect signatures
|
|
storage.Put(ctx, notaryDisabledKey, notaryDisabled)
|
|
if notaryDisabled {
|
|
var irList []common.IRNode
|
|
|
|
for i := 0; i < len(keys); i++ {
|
|
key := keys[i]
|
|
irList = append(irList, common.IRNode{PublicKey: key})
|
|
}
|
|
|
|
common.SetSerialized(ctx, innerRingKey, irList)
|
|
common.InitVote(ctx)
|
|
runtime.Log("netmap contract notary disabled")
|
|
}
|
|
|
|
runtime.Log("netmap contract initialized")
|
|
}
|
|
|
|
func Migrate(script []byte, manifest []byte) bool {
|
|
ctx := storage.GetReadOnlyContext()
|
|
|
|
if !common.HasUpdateAccess(ctx) {
|
|
runtime.Log("only owner can update contract")
|
|
return false
|
|
}
|
|
|
|
management.Update(script, manifest)
|
|
runtime.Log("netmap contract updated")
|
|
|
|
return true
|
|
}
|
|
|
|
func InnerRingList() []common.IRNode {
|
|
ctx := storage.GetReadOnlyContext()
|
|
return getIRNodes(ctx)
|
|
}
|
|
|
|
func UpdateInnerRing(keys []interop.PublicKey) {
|
|
ctx := storage.GetContext()
|
|
notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool)
|
|
|
|
var ( // for invocation collection without notary
|
|
alphabet []common.IRNode
|
|
nodeKey []byte
|
|
)
|
|
|
|
if notaryDisabled {
|
|
alphabet = common.AlphabetNodes()
|
|
nodeKey = common.InnerRingInvoker(alphabet)
|
|
if len(nodeKey) == 0 {
|
|
panic("updateInnerRing: this method must be invoked by alphabet nodes")
|
|
}
|
|
} else {
|
|
multiaddr := common.AlphabetAddress()
|
|
if !runtime.CheckWitness(multiaddr) {
|
|
panic("updateInnerRing: this method must be invoked by alphabet nodes")
|
|
}
|
|
}
|
|
|
|
var irList []common.IRNode
|
|
|
|
for i := 0; i < len(keys); i++ {
|
|
key := keys[i]
|
|
irList = append(irList, common.IRNode{PublicKey: key})
|
|
}
|
|
|
|
if notaryDisabled {
|
|
threshold := len(alphabet)*2/3 + 1
|
|
id := keysID(keys, []byte("updateIR"))
|
|
|
|
n := common.Vote(ctx, id, nodeKey)
|
|
if n < threshold {
|
|
return
|
|
}
|
|
|
|
common.RemoveVotes(ctx, id)
|
|
}
|
|
|
|
runtime.Log("updateInnerRing: inner ring list updated")
|
|
common.SetSerialized(ctx, innerRingKey, irList)
|
|
}
|
|
|
|
func AddPeer(nodeInfo []byte) {
|
|
ctx := storage.GetContext()
|
|
notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool)
|
|
|
|
var ( // for invocation collection without notary
|
|
alphabet []common.IRNode
|
|
nodeKey []byte
|
|
alphabetCall bool
|
|
)
|
|
|
|
if notaryDisabled {
|
|
alphabet = common.AlphabetNodes()
|
|
nodeKey = common.InnerRingInvoker(alphabet)
|
|
alphabetCall = len(nodeKey) != 0
|
|
} else {
|
|
multiaddr := common.AlphabetAddress()
|
|
alphabetCall = runtime.CheckWitness(multiaddr)
|
|
}
|
|
|
|
if !alphabetCall {
|
|
publicKey := nodeInfo[2:35] // offset:2, len:33
|
|
if !runtime.CheckWitness(publicKey) {
|
|
panic("addPeer: witness check failed")
|
|
}
|
|
runtime.Notify("AddPeer", nodeInfo)
|
|
return
|
|
}
|
|
|
|
candidate := storageNode{
|
|
info: nodeInfo,
|
|
}
|
|
|
|
nm := addToNetmap(ctx, candidate)
|
|
|
|
if notaryDisabled {
|
|
threshold := len(alphabet)*2/3 + 1
|
|
rawCandidate := std.Serialize(candidate)
|
|
id := crypto.Sha256(rawCandidate)
|
|
|
|
n := common.Vote(ctx, id, nodeKey)
|
|
if n < threshold {
|
|
return
|
|
}
|
|
|
|
common.RemoveVotes(ctx, id)
|
|
}
|
|
|
|
if nm == nil {
|
|
runtime.Log("addPeer: storage node already in the netmap")
|
|
} else {
|
|
common.SetSerialized(ctx, netmapKey, nm)
|
|
runtime.Log("addPeer: add storage node to the network map")
|
|
}
|
|
}
|
|
|
|
func UpdateState(state int, publicKey interop.PublicKey) {
|
|
if len(publicKey) != 33 {
|
|
panic("updateState: incorrect public key")
|
|
}
|
|
|
|
ctx := storage.GetContext()
|
|
notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool)
|
|
|
|
var ( // for invocation collection without notary
|
|
alphabet []common.IRNode
|
|
nodeKey []byte
|
|
alphabetCall bool
|
|
)
|
|
|
|
if notaryDisabled {
|
|
alphabet = common.AlphabetNodes()
|
|
nodeKey = common.InnerRingInvoker(alphabet)
|
|
alphabetCall = len(nodeKey) != 0
|
|
} else {
|
|
multiaddr := common.AlphabetAddress()
|
|
alphabetCall = runtime.CheckWitness(multiaddr)
|
|
}
|
|
|
|
if !alphabetCall {
|
|
if !runtime.CheckWitness(publicKey) {
|
|
panic("updateState: witness check failed")
|
|
}
|
|
|
|
runtime.Notify("UpdateState", state, publicKey)
|
|
|
|
return
|
|
}
|
|
|
|
if notaryDisabled {
|
|
threshold := len(alphabet)*2/3 + 1
|
|
id := common.InvokeID([]interface{}{state, publicKey}, []byte("update"))
|
|
|
|
n := common.Vote(ctx, id, nodeKey)
|
|
if n < threshold {
|
|
return
|
|
}
|
|
|
|
common.RemoveVotes(ctx, id)
|
|
}
|
|
|
|
switch nodeState(state) {
|
|
case offlineState:
|
|
newNetmap := removeFromNetmap(ctx, publicKey)
|
|
runtime.Log("updateState: remove storage node from the network map")
|
|
common.SetSerialized(ctx, netmapKey, newNetmap)
|
|
default:
|
|
panic("updateState: unsupported state")
|
|
}
|
|
}
|
|
|
|
func NewEpoch(epochNum int) {
|
|
ctx := storage.GetContext()
|
|
notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool)
|
|
|
|
var ( // for invocation collection without notary
|
|
alphabet []common.IRNode
|
|
nodeKey []byte
|
|
)
|
|
|
|
if notaryDisabled {
|
|
alphabet = common.AlphabetNodes()
|
|
nodeKey = common.InnerRingInvoker(alphabet)
|
|
if len(nodeKey) == 0 {
|
|
panic("newEpoch: this method must be invoked by inner ring nodes")
|
|
}
|
|
} else {
|
|
multiaddr := common.AlphabetAddress()
|
|
if !runtime.CheckWitness(multiaddr) {
|
|
panic("newEpoch: this method must be invoked by inner ring nodes")
|
|
}
|
|
}
|
|
|
|
if notaryDisabled {
|
|
threshold := len(alphabet)*2/3 + 1
|
|
id := common.InvokeID([]interface{}{epochNum}, []byte("epoch"))
|
|
|
|
n := common.Vote(ctx, id, nodeKey)
|
|
if n < threshold {
|
|
return
|
|
}
|
|
|
|
common.RemoveVotes(ctx, id)
|
|
}
|
|
|
|
currentEpoch := storage.Get(ctx, snapshotEpoch).(int)
|
|
if epochNum <= currentEpoch {
|
|
panic("invalid epoch") // ignore invocations with invalid epoch
|
|
}
|
|
|
|
data0snapshot := getSnapshot(ctx, snapshot0Key)
|
|
dataOnlineState := filterNetmap(ctx, onlineState)
|
|
|
|
runtime.Log("newEpoch: process new epoch")
|
|
|
|
// todo: check if provided epoch number is bigger than current
|
|
storage.Put(ctx, snapshotEpoch, epochNum)
|
|
|
|
// put actual snapshot into previous snapshot
|
|
common.SetSerialized(ctx, snapshot1Key, data0snapshot)
|
|
|
|
// put netmap into actual snapshot
|
|
common.SetSerialized(ctx, snapshot0Key, dataOnlineState)
|
|
|
|
// make clean up routines in other contracts
|
|
cleanup(ctx, epochNum)
|
|
|
|
runtime.Notify("NewEpoch", epochNum)
|
|
}
|
|
|
|
func Epoch() int {
|
|
ctx := storage.GetReadOnlyContext()
|
|
return storage.Get(ctx, snapshotEpoch).(int)
|
|
}
|
|
|
|
func Netmap() []storageNode {
|
|
ctx := storage.GetReadOnlyContext()
|
|
return getSnapshot(ctx, snapshot0Key)
|
|
}
|
|
|
|
// NetmapCandidates returns all node candidates for next epoch and their
|
|
// status codes.
|
|
func NetmapCandidates() []netmapNode {
|
|
ctx := storage.GetReadOnlyContext()
|
|
return getNetmapNodes(ctx)
|
|
}
|
|
|
|
func Snapshot(diff int) []storageNode {
|
|
var key string
|
|
|
|
switch diff {
|
|
case 0:
|
|
key = snapshot0Key
|
|
case 1:
|
|
key = snapshot1Key
|
|
default:
|
|
panic("snapshot: incorrect diff")
|
|
}
|
|
|
|
ctx := storage.GetReadOnlyContext()
|
|
return getSnapshot(ctx, key)
|
|
}
|
|
|
|
func SnapshotByEpoch(epoch int) []storageNode {
|
|
ctx := storage.GetReadOnlyContext()
|
|
currentEpoch := storage.Get(ctx, snapshotEpoch).(int)
|
|
|
|
return Snapshot(currentEpoch - epoch)
|
|
}
|
|
|
|
func Config(key []byte) interface{} {
|
|
ctx := storage.GetReadOnlyContext()
|
|
return getConfig(ctx, key)
|
|
}
|
|
|
|
func SetConfig(id, key, val []byte) {
|
|
ctx := storage.GetContext()
|
|
notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool)
|
|
|
|
var ( // for invocation collection without notary
|
|
alphabet []common.IRNode
|
|
nodeKey []byte
|
|
)
|
|
|
|
if notaryDisabled {
|
|
alphabet = common.AlphabetNodes()
|
|
nodeKey = common.InnerRingInvoker(alphabet)
|
|
if len(nodeKey) == 0 {
|
|
panic("setConfig: invoked by non inner ring node")
|
|
}
|
|
} else {
|
|
multiaddr := common.AlphabetAddress()
|
|
if !runtime.CheckWitness(multiaddr) {
|
|
panic("setConfig: invoked by non inner ring node")
|
|
}
|
|
}
|
|
|
|
if notaryDisabled {
|
|
threshold := len(alphabet)*2/3 + 1
|
|
|
|
n := common.Vote(ctx, id, nodeKey)
|
|
if n < threshold {
|
|
return
|
|
}
|
|
|
|
common.RemoveVotes(ctx, id)
|
|
}
|
|
|
|
setConfig(ctx, key, val)
|
|
|
|
runtime.Log("setConfig: configuration has been updated")
|
|
}
|
|
|
|
func InitConfig(args [][]byte) {
|
|
ctx := storage.GetContext()
|
|
|
|
if storage.Get(ctx, configuredKey) != nil {
|
|
panic("netmap: configuration already installed")
|
|
}
|
|
|
|
ln := len(args)
|
|
if ln%2 != 0 {
|
|
panic("initConfig: bad arguments")
|
|
}
|
|
|
|
for i := 0; i < ln/2; i++ {
|
|
key := args[i*2]
|
|
val := args[i*2+1]
|
|
|
|
setConfig(ctx, key, val)
|
|
}
|
|
|
|
storage.Put(ctx, configuredKey, true)
|
|
runtime.Log("netmap: config has been installed")
|
|
}
|
|
|
|
func ListConfig() []record {
|
|
ctx := storage.GetReadOnlyContext()
|
|
|
|
var config []record
|
|
|
|
it := storage.Find(ctx, configPrefix, storage.None)
|
|
for iterator.Next(it) {
|
|
pair := iterator.Value(it).([]interface{})
|
|
key := pair[0].([]byte)
|
|
val := pair[1].([]byte)
|
|
r := record{key: key[len(configPrefix):], val: val}
|
|
|
|
config = append(config, r)
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
func Version() int {
|
|
return version
|
|
}
|
|
|
|
func addToNetmap(ctx storage.Context, n storageNode) []netmapNode {
|
|
var (
|
|
newNode = n.info
|
|
newNodeKey = newNode[2:35]
|
|
|
|
netmap = getNetmapNodes(ctx)
|
|
node = netmapNode{
|
|
node: n,
|
|
state: onlineState,
|
|
}
|
|
)
|
|
|
|
for i := range netmap {
|
|
netmapNode := netmap[i].node.info
|
|
netmapNodeKey := netmapNode[2:35]
|
|
|
|
if common.BytesEqual(newNodeKey, netmapNodeKey) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
netmap = append(netmap, node)
|
|
|
|
return netmap
|
|
}
|
|
|
|
func removeFromNetmap(ctx storage.Context, key interop.PublicKey) []netmapNode {
|
|
var (
|
|
netmap = getNetmapNodes(ctx)
|
|
newNetmap = []netmapNode{}
|
|
)
|
|
|
|
for i := 0; i < len(netmap); i++ {
|
|
item := netmap[i]
|
|
node := item.node.info
|
|
publicKey := node[2:35] // offset:2, len:33
|
|
|
|
if !common.BytesEqual(publicKey, key) {
|
|
newNetmap = append(newNetmap, item)
|
|
}
|
|
}
|
|
|
|
return newNetmap
|
|
}
|
|
|
|
func filterNetmap(ctx storage.Context, st nodeState) []storageNode {
|
|
var (
|
|
netmap = getNetmapNodes(ctx)
|
|
result = []storageNode{}
|
|
)
|
|
|
|
for i := 0; i < len(netmap); i++ {
|
|
item := netmap[i]
|
|
if item.state == st {
|
|
result = append(result, item.node)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func getNetmapNodes(ctx storage.Context) []netmapNode {
|
|
data := storage.Get(ctx, netmapKey)
|
|
if data != nil {
|
|
return std.Deserialize(data.([]byte)).([]netmapNode)
|
|
}
|
|
|
|
return []netmapNode{}
|
|
}
|
|
|
|
func getSnapshot(ctx storage.Context, key string) []storageNode {
|
|
data := storage.Get(ctx, key)
|
|
if data != nil {
|
|
return std.Deserialize(data.([]byte)).([]storageNode)
|
|
}
|
|
|
|
return []storageNode{}
|
|
}
|
|
|
|
func getConfig(ctx storage.Context, key interface{}) interface{} {
|
|
postfix := key.([]byte)
|
|
storageKey := append(configPrefix, postfix...)
|
|
|
|
return storage.Get(ctx, storageKey)
|
|
}
|
|
|
|
func setConfig(ctx storage.Context, key, val interface{}) {
|
|
postfix := key.([]byte)
|
|
storageKey := append(configPrefix, postfix...)
|
|
|
|
storage.Put(ctx, storageKey, val)
|
|
}
|
|
|
|
func cleanup(ctx storage.Context, epoch int) {
|
|
balanceContractAddr := storage.Get(ctx, balanceContractKey).(interop.Hash160)
|
|
contract.Call(balanceContractAddr, cleanupEpochMethod, contract.All, epoch)
|
|
|
|
containerContractAddr := storage.Get(ctx, containerContractKey).(interop.Hash160)
|
|
contract.Call(containerContractAddr, cleanupEpochMethod, contract.All, epoch)
|
|
}
|
|
|
|
func getIRNodes(ctx storage.Context) []common.IRNode {
|
|
data := storage.Get(ctx, innerRingKey)
|
|
if data != nil {
|
|
return std.Deserialize(data.([]byte)).([]common.IRNode)
|
|
}
|
|
|
|
return []common.IRNode{}
|
|
}
|
|
|
|
func keysID(args []interop.PublicKey, prefix []byte) []byte {
|
|
var (
|
|
result []byte
|
|
)
|
|
|
|
result = append(result, prefix...)
|
|
|
|
for i := range args {
|
|
result = append(result, args[i]...)
|
|
}
|
|
|
|
return crypto.Sha256(result)
|
|
}
|