From bf391b57ddf084de01936c8138c3edd233382603 Mon Sep 17 00:00:00 2001 From: Alex Vanin Date: Tue, 27 Oct 2020 15:14:06 +0300 Subject: [PATCH] [#18] Add sidechain contracts Sidechain contracts include alphabet contracts for governance and audit, balance, container, neofsid, netmap, reputation contracts. Signed-off-by: Alex Vanin --- alphabet/alphabet.go | 54 +++ alphabet/alphabet.tpl | 116 ++++++ alphabet/az/az_contract.go | 116 ++++++ alphabet/buky/buky_contract.go | 116 ++++++ alphabet/config.yml | 2 + alphabet/dobro/dobro_contract.go | 116 ++++++ alphabet/glagoli/glagoli_contract.go | 116 ++++++ alphabet/jest/jest_contract.go | 116 ++++++ alphabet/vedi/vedi_contract.go | 116 ++++++ alphabet/zhivete/zhivete_contract.go | 116 ++++++ audit/audit_contract.go | 256 +++++++++++++ audit/config.yml | 2 + balance/balance_contract.go | 537 ++++++++++++++++++++++++++ balance/config.yml | 45 +++ container/config.yml | 17 + container/container_contract.go | 471 +++++++++++++++++++++++ neofsid/config.yml | 2 + neofsid/neofsid_contract.go | 314 ++++++++++++++++ netmap/config.yml | 17 + netmap/netmap_contract.go | 540 +++++++++++++++++++++++++++ reputation/config.yml | 2 + reputation/reputation_contract.go | 92 +++++ 22 files changed, 3279 insertions(+) create mode 100644 alphabet/alphabet.go create mode 100644 alphabet/alphabet.tpl create mode 100644 alphabet/az/az_contract.go create mode 100644 alphabet/buky/buky_contract.go create mode 100644 alphabet/config.yml create mode 100644 alphabet/dobro/dobro_contract.go create mode 100644 alphabet/glagoli/glagoli_contract.go create mode 100644 alphabet/jest/jest_contract.go create mode 100644 alphabet/vedi/vedi_contract.go create mode 100644 alphabet/zhivete/zhivete_contract.go create mode 100644 audit/audit_contract.go create mode 100644 audit/config.yml create mode 100644 balance/balance_contract.go create mode 100644 balance/config.yml create mode 100644 container/config.yml create mode 100644 container/container_contract.go create mode 100644 neofsid/config.yml create mode 100644 neofsid/neofsid_contract.go create mode 100644 netmap/config.yml create mode 100644 netmap/netmap_contract.go create mode 100644 reputation/config.yml create mode 100644 reputation/reputation_contract.go diff --git a/alphabet/alphabet.go b/alphabet/alphabet.go new file mode 100644 index 0000000..606003d --- /dev/null +++ b/alphabet/alphabet.go @@ -0,0 +1,54 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "strings" + "text/template" +) + +const ( + root = "alphabet" + templateName = "alphabet.tpl" +) + +func main() { + glagolic := []string{ + "Az", "Buky", "Vedi", "Glagoli", "Dobro", "Jest", "Zhivete", + } + + data, err := ioutil.ReadFile(path.Join(root, templateName)) + die("can't read template file", err) + + tmpl := template.Must(template.New("").Parse(string(data))) + + for index, name := range glagolic { + lowercaseName := strings.ToLower(name) + + if _, err := os.Stat(path.Join(root, lowercaseName)); os.IsNotExist(err) { + os.Mkdir(path.Join(root, lowercaseName), 0755) + } + + dst, err := os.Create(path.Join(root, lowercaseName, lowercaseName+"_contract.go")) + die("can't create file", err) + + err = tmpl.Execute(dst, map[string]interface{}{ + "Name": name, + "Index": index, + }) + die("can't generate code from template", err) + + die("can't close generated file", dst.Close()) + } + + os.Exit(0) +} + +func die(msg string, err error) { + if err != nil { + fmt.Printf(msg+": %v\n", err) + os.Exit(1) + } +} diff --git a/alphabet/alphabet.tpl b/alphabet/alphabet.tpl new file mode 100644 index 0000000..f27365c --- /dev/null +++ b/alphabet/alphabet.tpl @@ -0,0 +1,116 @@ +package alphabetcontract + +import ( + "github.com/nspcc-dev/neo-go/pkg/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/interop/engine" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" +) + +type ( + irNode struct { + key []byte + } +) + +const ( + // native gas token script hash + gasHash = "\xbc\xaf\x41\xd6\x84\xc7\xd4\xad\x6e\xe0\xd9\x9d\xa9\x70\x7b\x9d\x1f\x0c\x8e\x66" + + // native neo token script hash + neoHash = "\x25\x05\x9e\xcb\x48\x78\xd3\xa8\x75\xf9\x1c\x51\xce\xde\xd3\x30\xd4\x57\x5f\xde" + + name = "{{ .Name }}" + index = {{ .Index }} + + netmapContractKey = "netmapScriptHash" +) + +var ( + ctx storage.Context +) + +func init() { + if runtime.GetTrigger() != runtime.Application { + panic("contract has not been called in application node") + } + + ctx = storage.GetContext() +} + +func Init(addrNetmap []byte) { + if storage.Get(ctx, netmapContractKey) != nil { + panic("contract already deployed") + } + + if len(addrNetmap) != 20 { + panic("incorrect length of contract script hash") + } + + storage.Put(ctx, netmapContractKey, addrNetmap) + + runtime.Log(name + " contract initialized") +} + +func Gas() int { + contractHash := runtime.GetExecutingScriptHash() + return balance(gasHash, contractHash) +} + +func Neo() int { + contractHash := runtime.GetExecutingScriptHash() + return balance(neoHash, contractHash) +} + +func balance(hash string, addr []byte) int { + balance := engine.AppCall([]byte(hash), "balanceOf", addr) + return balance.(int) +} + +func irList() []irNode { + netmapContractAddr := storage.Get(ctx, netmapContractKey).([]byte) + return engine.AppCall(netmapContractAddr, "innerRingList").([]irNode) +} + +func checkPermission(ir []irNode) bool { + if len(ir) <= index { + return false + } + + node := ir[index] + return runtime.CheckWitness(node.key) +} + +func Emit() bool { + innerRingKeys := irList() + if !checkPermission(innerRingKeys) { + panic("invalid invoker") + } + + contractHash := runtime.GetExecutingScriptHash() + neo := balance(neoHash, contractHash) + + _ = engine.AppCall([]byte(neoHash), "transfer", contractHash, contractHash, neo) + + gas := balance(gasHash, contractHash) + gasPerNode := gas * 7 / 8 / len(innerRingKeys) + + if gasPerNode == 0 { + runtime.Log("no gas to emit") + return false + } + + for i := range innerRingKeys { + node := innerRingKeys[i] + address := contract.CreateStandardAccount(node.key) + + _ = engine.AppCall([]byte(gasHash), "transfer", contractHash, address, gasPerNode) + } + + runtime.Log("utility token has been emitted to inner ring nodes") + return true +} + +func Name() string { + return name +} diff --git a/alphabet/az/az_contract.go b/alphabet/az/az_contract.go new file mode 100644 index 0000000..f2475af --- /dev/null +++ b/alphabet/az/az_contract.go @@ -0,0 +1,116 @@ +package alphabetcontract + +import ( + "github.com/nspcc-dev/neo-go/pkg/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/interop/engine" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" +) + +type ( + irNode struct { + key []byte + } +) + +const ( + // native gas token script hash + gasHash = "\xbc\xaf\x41\xd6\x84\xc7\xd4\xad\x6e\xe0\xd9\x9d\xa9\x70\x7b\x9d\x1f\x0c\x8e\x66" + + // native neo token script hash + neoHash = "\x25\x05\x9e\xcb\x48\x78\xd3\xa8\x75\xf9\x1c\x51\xce\xde\xd3\x30\xd4\x57\x5f\xde" + + name = "Az" + index = 0 + + netmapContractKey = "netmapScriptHash" +) + +var ( + ctx storage.Context +) + +func init() { + if runtime.GetTrigger() != runtime.Application { + panic("contract has not been called in application node") + } + + ctx = storage.GetContext() +} + +func Init(addrNetmap []byte) { + if storage.Get(ctx, netmapContractKey) != nil { + panic("contract already deployed") + } + + if len(addrNetmap) != 20 { + panic("incorrect length of contract script hash") + } + + storage.Put(ctx, netmapContractKey, addrNetmap) + + runtime.Log(name + " contract initialized") +} + +func Gas() int { + contractHash := runtime.GetExecutingScriptHash() + return balance(gasHash, contractHash) +} + +func Neo() int { + contractHash := runtime.GetExecutingScriptHash() + return balance(neoHash, contractHash) +} + +func balance(hash string, addr []byte) int { + balance := engine.AppCall([]byte(hash), "balanceOf", addr) + return balance.(int) +} + +func irList() []irNode { + netmapContractAddr := storage.Get(ctx, netmapContractKey).([]byte) + return engine.AppCall(netmapContractAddr, "innerRingList").([]irNode) +} + +func checkPermission(ir []irNode) bool { + if len(ir) <= index { + return false + } + + node := ir[index] + return runtime.CheckWitness(node.key) +} + +func Emit() bool { + innerRingKeys := irList() + if !checkPermission(innerRingKeys) { + panic("invalid invoker") + } + + contractHash := runtime.GetExecutingScriptHash() + neo := balance(neoHash, contractHash) + + _ = engine.AppCall([]byte(neoHash), "transfer", contractHash, contractHash, neo) + + gas := balance(gasHash, contractHash) + gasPerNode := gas * 7 / 8 / len(innerRingKeys) + + if gasPerNode == 0 { + runtime.Log("no gas to emit") + return false + } + + for i := range innerRingKeys { + node := innerRingKeys[i] + address := contract.CreateStandardAccount(node.key) + + _ = engine.AppCall([]byte(gasHash), "transfer", contractHash, address, gasPerNode) + } + + runtime.Log("utility token has been emitted to inner ring nodes") + return true +} + +func Name() string { + return name +} diff --git a/alphabet/buky/buky_contract.go b/alphabet/buky/buky_contract.go new file mode 100644 index 0000000..e8b3539 --- /dev/null +++ b/alphabet/buky/buky_contract.go @@ -0,0 +1,116 @@ +package alphabetcontract + +import ( + "github.com/nspcc-dev/neo-go/pkg/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/interop/engine" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" +) + +type ( + irNode struct { + key []byte + } +) + +const ( + // native gas token script hash + gasHash = "\xbc\xaf\x41\xd6\x84\xc7\xd4\xad\x6e\xe0\xd9\x9d\xa9\x70\x7b\x9d\x1f\x0c\x8e\x66" + + // native neo token script hash + neoHash = "\x25\x05\x9e\xcb\x48\x78\xd3\xa8\x75\xf9\x1c\x51\xce\xde\xd3\x30\xd4\x57\x5f\xde" + + name = "Buky" + index = 1 + + netmapContractKey = "netmapScriptHash" +) + +var ( + ctx storage.Context +) + +func init() { + if runtime.GetTrigger() != runtime.Application { + panic("contract has not been called in application node") + } + + ctx = storage.GetContext() +} + +func Init(addrNetmap []byte) { + if storage.Get(ctx, netmapContractKey) != nil { + panic("contract already deployed") + } + + if len(addrNetmap) != 20 { + panic("incorrect length of contract script hash") + } + + storage.Put(ctx, netmapContractKey, addrNetmap) + + runtime.Log(name + " contract initialized") +} + +func Gas() int { + contractHash := runtime.GetExecutingScriptHash() + return balance(gasHash, contractHash) +} + +func Neo() int { + contractHash := runtime.GetExecutingScriptHash() + return balance(neoHash, contractHash) +} + +func balance(hash string, addr []byte) int { + balance := engine.AppCall([]byte(hash), "balanceOf", addr) + return balance.(int) +} + +func irList() []irNode { + netmapContractAddr := storage.Get(ctx, netmapContractKey).([]byte) + return engine.AppCall(netmapContractAddr, "innerRingList").([]irNode) +} + +func checkPermission(ir []irNode) bool { + if len(ir) <= index { + return false + } + + node := ir[index] + return runtime.CheckWitness(node.key) +} + +func Emit() bool { + innerRingKeys := irList() + if !checkPermission(innerRingKeys) { + panic("invalid invoker") + } + + contractHash := runtime.GetExecutingScriptHash() + neo := balance(neoHash, contractHash) + + _ = engine.AppCall([]byte(neoHash), "transfer", contractHash, contractHash, neo) + + gas := balance(gasHash, contractHash) + gasPerNode := gas * 7 / 8 / len(innerRingKeys) + + if gasPerNode == 0 { + runtime.Log("no gas to emit") + return false + } + + for i := range innerRingKeys { + node := innerRingKeys[i] + address := contract.CreateStandardAccount(node.key) + + _ = engine.AppCall([]byte(gasHash), "transfer", contractHash, address, gasPerNode) + } + + runtime.Log("utility token has been emitted to inner ring nodes") + return true +} + +func Name() string { + return name +} diff --git a/alphabet/config.yml b/alphabet/config.yml new file mode 100644 index 0000000..8f518e6 --- /dev/null +++ b/alphabet/config.yml @@ -0,0 +1,2 @@ +hasstorage: true +ispayable: false \ No newline at end of file diff --git a/alphabet/dobro/dobro_contract.go b/alphabet/dobro/dobro_contract.go new file mode 100644 index 0000000..4fb8999 --- /dev/null +++ b/alphabet/dobro/dobro_contract.go @@ -0,0 +1,116 @@ +package alphabetcontract + +import ( + "github.com/nspcc-dev/neo-go/pkg/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/interop/engine" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" +) + +type ( + irNode struct { + key []byte + } +) + +const ( + // native gas token script hash + gasHash = "\xbc\xaf\x41\xd6\x84\xc7\xd4\xad\x6e\xe0\xd9\x9d\xa9\x70\x7b\x9d\x1f\x0c\x8e\x66" + + // native neo token script hash + neoHash = "\x25\x05\x9e\xcb\x48\x78\xd3\xa8\x75\xf9\x1c\x51\xce\xde\xd3\x30\xd4\x57\x5f\xde" + + name = "Dobro" + index = 4 + + netmapContractKey = "netmapScriptHash" +) + +var ( + ctx storage.Context +) + +func init() { + if runtime.GetTrigger() != runtime.Application { + panic("contract has not been called in application node") + } + + ctx = storage.GetContext() +} + +func Init(addrNetmap []byte) { + if storage.Get(ctx, netmapContractKey) != nil { + panic("contract already deployed") + } + + if len(addrNetmap) != 20 { + panic("incorrect length of contract script hash") + } + + storage.Put(ctx, netmapContractKey, addrNetmap) + + runtime.Log(name + " contract initialized") +} + +func Gas() int { + contractHash := runtime.GetExecutingScriptHash() + return balance(gasHash, contractHash) +} + +func Neo() int { + contractHash := runtime.GetExecutingScriptHash() + return balance(neoHash, contractHash) +} + +func balance(hash string, addr []byte) int { + balance := engine.AppCall([]byte(hash), "balanceOf", addr) + return balance.(int) +} + +func irList() []irNode { + netmapContractAddr := storage.Get(ctx, netmapContractKey).([]byte) + return engine.AppCall(netmapContractAddr, "innerRingList").([]irNode) +} + +func checkPermission(ir []irNode) bool { + if len(ir) <= index { + return false + } + + node := ir[index] + return runtime.CheckWitness(node.key) +} + +func Emit() bool { + innerRingKeys := irList() + if !checkPermission(innerRingKeys) { + panic("invalid invoker") + } + + contractHash := runtime.GetExecutingScriptHash() + neo := balance(neoHash, contractHash) + + _ = engine.AppCall([]byte(neoHash), "transfer", contractHash, contractHash, neo) + + gas := balance(gasHash, contractHash) + gasPerNode := gas * 7 / 8 / len(innerRingKeys) + + if gasPerNode == 0 { + runtime.Log("no gas to emit") + return false + } + + for i := range innerRingKeys { + node := innerRingKeys[i] + address := contract.CreateStandardAccount(node.key) + + _ = engine.AppCall([]byte(gasHash), "transfer", contractHash, address, gasPerNode) + } + + runtime.Log("utility token has been emitted to inner ring nodes") + return true +} + +func Name() string { + return name +} diff --git a/alphabet/glagoli/glagoli_contract.go b/alphabet/glagoli/glagoli_contract.go new file mode 100644 index 0000000..043a00f --- /dev/null +++ b/alphabet/glagoli/glagoli_contract.go @@ -0,0 +1,116 @@ +package alphabetcontract + +import ( + "github.com/nspcc-dev/neo-go/pkg/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/interop/engine" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" +) + +type ( + irNode struct { + key []byte + } +) + +const ( + // native gas token script hash + gasHash = "\xbc\xaf\x41\xd6\x84\xc7\xd4\xad\x6e\xe0\xd9\x9d\xa9\x70\x7b\x9d\x1f\x0c\x8e\x66" + + // native neo token script hash + neoHash = "\x25\x05\x9e\xcb\x48\x78\xd3\xa8\x75\xf9\x1c\x51\xce\xde\xd3\x30\xd4\x57\x5f\xde" + + name = "Glagoli" + index = 3 + + netmapContractKey = "netmapScriptHash" +) + +var ( + ctx storage.Context +) + +func init() { + if runtime.GetTrigger() != runtime.Application { + panic("contract has not been called in application node") + } + + ctx = storage.GetContext() +} + +func Init(addrNetmap []byte) { + if storage.Get(ctx, netmapContractKey) != nil { + panic("contract already deployed") + } + + if len(addrNetmap) != 20 { + panic("incorrect length of contract script hash") + } + + storage.Put(ctx, netmapContractKey, addrNetmap) + + runtime.Log(name + " contract initialized") +} + +func Gas() int { + contractHash := runtime.GetExecutingScriptHash() + return balance(gasHash, contractHash) +} + +func Neo() int { + contractHash := runtime.GetExecutingScriptHash() + return balance(neoHash, contractHash) +} + +func balance(hash string, addr []byte) int { + balance := engine.AppCall([]byte(hash), "balanceOf", addr) + return balance.(int) +} + +func irList() []irNode { + netmapContractAddr := storage.Get(ctx, netmapContractKey).([]byte) + return engine.AppCall(netmapContractAddr, "innerRingList").([]irNode) +} + +func checkPermission(ir []irNode) bool { + if len(ir) <= index { + return false + } + + node := ir[index] + return runtime.CheckWitness(node.key) +} + +func Emit() bool { + innerRingKeys := irList() + if !checkPermission(innerRingKeys) { + panic("invalid invoker") + } + + contractHash := runtime.GetExecutingScriptHash() + neo := balance(neoHash, contractHash) + + _ = engine.AppCall([]byte(neoHash), "transfer", contractHash, contractHash, neo) + + gas := balance(gasHash, contractHash) + gasPerNode := gas * 7 / 8 / len(innerRingKeys) + + if gasPerNode == 0 { + runtime.Log("no gas to emit") + return false + } + + for i := range innerRingKeys { + node := innerRingKeys[i] + address := contract.CreateStandardAccount(node.key) + + _ = engine.AppCall([]byte(gasHash), "transfer", contractHash, address, gasPerNode) + } + + runtime.Log("utility token has been emitted to inner ring nodes") + return true +} + +func Name() string { + return name +} diff --git a/alphabet/jest/jest_contract.go b/alphabet/jest/jest_contract.go new file mode 100644 index 0000000..ed0f695 --- /dev/null +++ b/alphabet/jest/jest_contract.go @@ -0,0 +1,116 @@ +package alphabetcontract + +import ( + "github.com/nspcc-dev/neo-go/pkg/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/interop/engine" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" +) + +type ( + irNode struct { + key []byte + } +) + +const ( + // native gas token script hash + gasHash = "\xbc\xaf\x41\xd6\x84\xc7\xd4\xad\x6e\xe0\xd9\x9d\xa9\x70\x7b\x9d\x1f\x0c\x8e\x66" + + // native neo token script hash + neoHash = "\x25\x05\x9e\xcb\x48\x78\xd3\xa8\x75\xf9\x1c\x51\xce\xde\xd3\x30\xd4\x57\x5f\xde" + + name = "Jest" + index = 5 + + netmapContractKey = "netmapScriptHash" +) + +var ( + ctx storage.Context +) + +func init() { + if runtime.GetTrigger() != runtime.Application { + panic("contract has not been called in application node") + } + + ctx = storage.GetContext() +} + +func Init(addrNetmap []byte) { + if storage.Get(ctx, netmapContractKey) != nil { + panic("contract already deployed") + } + + if len(addrNetmap) != 20 { + panic("incorrect length of contract script hash") + } + + storage.Put(ctx, netmapContractKey, addrNetmap) + + runtime.Log(name + " contract initialized") +} + +func Gas() int { + contractHash := runtime.GetExecutingScriptHash() + return balance(gasHash, contractHash) +} + +func Neo() int { + contractHash := runtime.GetExecutingScriptHash() + return balance(neoHash, contractHash) +} + +func balance(hash string, addr []byte) int { + balance := engine.AppCall([]byte(hash), "balanceOf", addr) + return balance.(int) +} + +func irList() []irNode { + netmapContractAddr := storage.Get(ctx, netmapContractKey).([]byte) + return engine.AppCall(netmapContractAddr, "innerRingList").([]irNode) +} + +func checkPermission(ir []irNode) bool { + if len(ir) <= index { + return false + } + + node := ir[index] + return runtime.CheckWitness(node.key) +} + +func Emit() bool { + innerRingKeys := irList() + if !checkPermission(innerRingKeys) { + panic("invalid invoker") + } + + contractHash := runtime.GetExecutingScriptHash() + neo := balance(neoHash, contractHash) + + _ = engine.AppCall([]byte(neoHash), "transfer", contractHash, contractHash, neo) + + gas := balance(gasHash, contractHash) + gasPerNode := gas * 7 / 8 / len(innerRingKeys) + + if gasPerNode == 0 { + runtime.Log("no gas to emit") + return false + } + + for i := range innerRingKeys { + node := innerRingKeys[i] + address := contract.CreateStandardAccount(node.key) + + _ = engine.AppCall([]byte(gasHash), "transfer", contractHash, address, gasPerNode) + } + + runtime.Log("utility token has been emitted to inner ring nodes") + return true +} + +func Name() string { + return name +} diff --git a/alphabet/vedi/vedi_contract.go b/alphabet/vedi/vedi_contract.go new file mode 100644 index 0000000..9cd53a9 --- /dev/null +++ b/alphabet/vedi/vedi_contract.go @@ -0,0 +1,116 @@ +package alphabetcontract + +import ( + "github.com/nspcc-dev/neo-go/pkg/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/interop/engine" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" +) + +type ( + irNode struct { + key []byte + } +) + +const ( + // native gas token script hash + gasHash = "\xbc\xaf\x41\xd6\x84\xc7\xd4\xad\x6e\xe0\xd9\x9d\xa9\x70\x7b\x9d\x1f\x0c\x8e\x66" + + // native neo token script hash + neoHash = "\x25\x05\x9e\xcb\x48\x78\xd3\xa8\x75\xf9\x1c\x51\xce\xde\xd3\x30\xd4\x57\x5f\xde" + + name = "Vedi" + index = 2 + + netmapContractKey = "netmapScriptHash" +) + +var ( + ctx storage.Context +) + +func init() { + if runtime.GetTrigger() != runtime.Application { + panic("contract has not been called in application node") + } + + ctx = storage.GetContext() +} + +func Init(addrNetmap []byte) { + if storage.Get(ctx, netmapContractKey) != nil { + panic("contract already deployed") + } + + if len(addrNetmap) != 20 { + panic("incorrect length of contract script hash") + } + + storage.Put(ctx, netmapContractKey, addrNetmap) + + runtime.Log(name + " contract initialized") +} + +func Gas() int { + contractHash := runtime.GetExecutingScriptHash() + return balance(gasHash, contractHash) +} + +func Neo() int { + contractHash := runtime.GetExecutingScriptHash() + return balance(neoHash, contractHash) +} + +func balance(hash string, addr []byte) int { + balance := engine.AppCall([]byte(hash), "balanceOf", addr) + return balance.(int) +} + +func irList() []irNode { + netmapContractAddr := storage.Get(ctx, netmapContractKey).([]byte) + return engine.AppCall(netmapContractAddr, "innerRingList").([]irNode) +} + +func checkPermission(ir []irNode) bool { + if len(ir) <= index { + return false + } + + node := ir[index] + return runtime.CheckWitness(node.key) +} + +func Emit() bool { + innerRingKeys := irList() + if !checkPermission(innerRingKeys) { + panic("invalid invoker") + } + + contractHash := runtime.GetExecutingScriptHash() + neo := balance(neoHash, contractHash) + + _ = engine.AppCall([]byte(neoHash), "transfer", contractHash, contractHash, neo) + + gas := balance(gasHash, contractHash) + gasPerNode := gas * 7 / 8 / len(innerRingKeys) + + if gasPerNode == 0 { + runtime.Log("no gas to emit") + return false + } + + for i := range innerRingKeys { + node := innerRingKeys[i] + address := contract.CreateStandardAccount(node.key) + + _ = engine.AppCall([]byte(gasHash), "transfer", contractHash, address, gasPerNode) + } + + runtime.Log("utility token has been emitted to inner ring nodes") + return true +} + +func Name() string { + return name +} diff --git a/alphabet/zhivete/zhivete_contract.go b/alphabet/zhivete/zhivete_contract.go new file mode 100644 index 0000000..236af74 --- /dev/null +++ b/alphabet/zhivete/zhivete_contract.go @@ -0,0 +1,116 @@ +package alphabetcontract + +import ( + "github.com/nspcc-dev/neo-go/pkg/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/interop/engine" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" +) + +type ( + irNode struct { + key []byte + } +) + +const ( + // native gas token script hash + gasHash = "\xbc\xaf\x41\xd6\x84\xc7\xd4\xad\x6e\xe0\xd9\x9d\xa9\x70\x7b\x9d\x1f\x0c\x8e\x66" + + // native neo token script hash + neoHash = "\x25\x05\x9e\xcb\x48\x78\xd3\xa8\x75\xf9\x1c\x51\xce\xde\xd3\x30\xd4\x57\x5f\xde" + + name = "Zhivete" + index = 6 + + netmapContractKey = "netmapScriptHash" +) + +var ( + ctx storage.Context +) + +func init() { + if runtime.GetTrigger() != runtime.Application { + panic("contract has not been called in application node") + } + + ctx = storage.GetContext() +} + +func Init(addrNetmap []byte) { + if storage.Get(ctx, netmapContractKey) != nil { + panic("contract already deployed") + } + + if len(addrNetmap) != 20 { + panic("incorrect length of contract script hash") + } + + storage.Put(ctx, netmapContractKey, addrNetmap) + + runtime.Log(name + " contract initialized") +} + +func Gas() int { + contractHash := runtime.GetExecutingScriptHash() + return balance(gasHash, contractHash) +} + +func Neo() int { + contractHash := runtime.GetExecutingScriptHash() + return balance(neoHash, contractHash) +} + +func balance(hash string, addr []byte) int { + balance := engine.AppCall([]byte(hash), "balanceOf", addr) + return balance.(int) +} + +func irList() []irNode { + netmapContractAddr := storage.Get(ctx, netmapContractKey).([]byte) + return engine.AppCall(netmapContractAddr, "innerRingList").([]irNode) +} + +func checkPermission(ir []irNode) bool { + if len(ir) <= index { + return false + } + + node := ir[index] + return runtime.CheckWitness(node.key) +} + +func Emit() bool { + innerRingKeys := irList() + if !checkPermission(innerRingKeys) { + panic("invalid invoker") + } + + contractHash := runtime.GetExecutingScriptHash() + neo := balance(neoHash, contractHash) + + _ = engine.AppCall([]byte(neoHash), "transfer", contractHash, contractHash, neo) + + gas := balance(gasHash, contractHash) + gasPerNode := gas * 7 / 8 / len(innerRingKeys) + + if gasPerNode == 0 { + runtime.Log("no gas to emit") + return false + } + + for i := range innerRingKeys { + node := innerRingKeys[i] + address := contract.CreateStandardAccount(node.key) + + _ = engine.AppCall([]byte(gasHash), "transfer", contractHash, address, gasPerNode) + } + + runtime.Log("utility token has been emitted to inner ring nodes") + return true +} + +func Name() string { + return name +} diff --git a/audit/audit_contract.go b/audit/audit_contract.go new file mode 100644 index 0000000..c624f22 --- /dev/null +++ b/audit/audit_contract.go @@ -0,0 +1,256 @@ +package auditcontract + +import ( + "github.com/nspcc-dev/neo-go/pkg/interop/binary" + "github.com/nspcc-dev/neo-go/pkg/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/interop/engine" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" + "github.com/nspcc-dev/neo-go/pkg/interop/util" +) + +type ( + irNode struct { + key []byte + } + + CheckedNode struct { + Key []byte // 33 bytes + Pair int // 2 bytes + Reward int // ? up to 32 byte + } + + AuditResult struct { + InnerRingNode []byte // 33 bytes + Epoch int // 8 bytes + ContainerID []byte // 32 bytes + StorageGroupID []byte // 16 bytes + PoR bool // 1 byte + PDP bool // 1 byte + // --- 91 bytes -- // + // --- 2 more bytes to size of the []CheckedNode // + Nodes []CheckedNode // <= 67 bytes per node + // about 1400 nodes may be presented in container + } +) + +const ( + version = 1 + + // 1E-8 GAS in precision of balance container. + // This value may be calculated in runtime based on decimal value of + // balance contract. We can also provide methods to change fee + // in runtime. + auditFee = 1 * 100_000_000 + + ownerIDLength = 25 + + journalKey = "auditJournal" + balanceContractKey = "balanceScriptHash" + containerContractKey = "containerScriptHash" + netmapContractKey = "netmapScriptHash" +) + +var ( + auditFeeTransferMsg = []byte("audit execution fee") + auditRewardTransferMsg = []byte("data audit reward") + + ctx storage.Context +) + +func init() { + if runtime.GetTrigger() != runtime.Application { + panic("contract has not been called in application node") + } + + ctx = storage.GetContext() +} + +func Init(addrNetmap, addrBalance, addrContainer []byte) { + if storage.Get(ctx, netmapContractKey) != nil && + storage.Get(ctx, balanceContractKey) != nil && + storage.Get(ctx, containerContractKey) != nil { + panic("init: contract already deployed") + } + + if len(addrNetmap) != 20 || len(addrBalance) != 20 || len(addrContainer) != 20 { + panic("init: incorrect length of contract script hash") + } + + storage.Put(ctx, netmapContractKey, addrNetmap) + storage.Put(ctx, balanceContractKey, addrBalance) + storage.Put(ctx, containerContractKey, addrContainer) + + setSerialized(ctx, journalKey, []AuditResult{}) + + runtime.Log("audit contract initialized") +} + +func Put(rawAuditResult []byte) bool { + netmapContractAddr := storage.Get(ctx, netmapContractKey).([]byte) + innerRing := engine.AppCall(netmapContractAddr, "innerRingList").([]irNode) + + auditResult, err := newAuditResult(rawAuditResult) + if err { + panic("put: can't parse audit result") + } + + var presented = false + + for i := range innerRing { + ir := innerRing[i] + if bytesEqual(ir.key, auditResult.InnerRingNode) { + presented = true + break + } + } + + if !runtime.CheckWitness(auditResult.InnerRingNode) || !presented { + panic("put: access denied") + } + + // todo: limit size of the audit journal: + // history will be stored in chain (args or notifies) + // contract storage will be used as a cache if needed + journal := getAuditResult(ctx) + journal = append(journal, auditResult) + + setSerialized(ctx, journalKey, journal) + + if auditResult.PDP && auditResult.PoR { + // find who is the ownerID + containerContract := storage.Get(ctx, containerContractKey).([]byte) + + // todo: implement easy way to get owner from the container id + ownerID := engine.AppCall(containerContract, "owner", auditResult.ContainerID).([]byte) + if len(ownerID) != ownerIDLength { + runtime.Log("put: can't get owner id of the container") + + return false + } + + ownerScriptHash := walletToScripHash(ownerID) + + // transfer fee to the inner ring node + balanceContract := storage.Get(ctx, balanceContractKey).([]byte) + irScriptHash := contract.CreateStandardAccount(auditResult.InnerRingNode) + + tx := engine.AppCall(balanceContract, "transferX", + ownerScriptHash, + irScriptHash, + auditFee, + auditFeeTransferMsg, // todo: add epoch, container and storage group info + ) + if !tx.(bool) { + panic("put: can't transfer inner ring fee") + } + + for i := 0; i < len(auditResult.Nodes); i++ { + node := auditResult.Nodes[i] + nodeScriptHash := contract.CreateStandardAccount(node.Key) + + tx := engine.AppCall(balanceContract, "transferX", + ownerScriptHash, + nodeScriptHash, + node.Reward, + auditRewardTransferMsg, // todo: add epoch, container and storage group info + ) + if !tx.(bool) { + runtime.Log("put: can't transfer storage payment") + + return false + } + } + } + + return true +} + +func Version() int { + return version +} + +func newAuditResult(data []byte) (AuditResult, bool) { + var ( + tmp interface{} + ln = len(data) + result = AuditResult{ + InnerRingNode: nil, // neo-go#949 + ContainerID: nil, + StorageGroupID: nil, + Nodes: []CheckedNode{}, + } + ) + + if len(data) < 91 { // all required headers + runtime.Log("newAuditResult: can't parse audit result header") + return result, true + } + + result.InnerRingNode = data[0:33] + + epoch := data[33:41] + tmp = epoch + result.Epoch = tmp.(int) + + result.ContainerID = data[41:73] + result.StorageGroupID = data[73:89] + result.PoR = util.Equals(data[90], 0x01) + result.PDP = util.Equals(data[91], 0x01) + + // if there are nodes, that were checked + if len(data) > 93 { + rawCounter := data[91:93] + tmp = rawCounter + counter := tmp.(int) + + ptr := 93 + + for i := 0; i < counter; i++ { + if ptr+33+2+32 > ln { + runtime.Log("newAuditResult: broken node") + return result, false + } + + node := CheckedNode{ + Key: nil, // neo-go#949 + } + node.Key = data[ptr : ptr+33] + + pair := data[ptr+33 : ptr+35] + tmp = pair + node.Pair = tmp.(int) + + reward := data[ptr+35 : ptr+67] + tmp = reward + node.Reward = tmp.(int) + + result.Nodes = append(result.Nodes, node) + } + } + + return result, false +} + +func getAuditResult(ctx storage.Context) []AuditResult { + data := storage.Get(ctx, journalKey) + if data != nil { + return binary.Deserialize(data.([]byte)).([]AuditResult) + } + + return []AuditResult{} +} + +func setSerialized(ctx storage.Context, key interface{}, value interface{}) { + data := binary.Serialize(value) + storage.Put(ctx, key, data) +} + +func walletToScripHash(wallet []byte) []byte { + return wallet[1 : len(wallet)-4] +} + +// neo-go#1176 +func bytesEqual(a []byte, b []byte) bool { + return util.Equals(string(a), string(b)) +} diff --git a/audit/config.yml b/audit/config.yml new file mode 100644 index 0000000..8f518e6 --- /dev/null +++ b/audit/config.yml @@ -0,0 +1,2 @@ +hasstorage: true +ispayable: false \ No newline at end of file diff --git a/balance/balance_contract.go b/balance/balance_contract.go new file mode 100644 index 0000000..5ba60fe --- /dev/null +++ b/balance/balance_contract.go @@ -0,0 +1,537 @@ +package balancecontract + +import ( + "github.com/nspcc-dev/neo-go/pkg/interop/binary" + "github.com/nspcc-dev/neo-go/pkg/interop/blockchain" + "github.com/nspcc-dev/neo-go/pkg/interop/crypto" + "github.com/nspcc-dev/neo-go/pkg/interop/engine" + "github.com/nspcc-dev/neo-go/pkg/interop/iterator" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" + "github.com/nspcc-dev/neo-go/pkg/interop/util" +) + +type ( + irNode struct { + key []byte + } + + ballot struct { + id []byte // id of the voting decision + n [][]byte // already voted inner ring nodes + block int // block with the last vote + } + + // Token holds all token info. + Token struct { + // Token name + Name string + // Ticker symbol + Symbol string + // Amount of decimals + Decimals int + // Storage key for circulation value + CirculationKey string + } + + Account struct { + // Active balance + Balance int + // Until valid for lock accounts + Until int + // Parent field used in lock accounts, used to return assets back if + // account wasn't burnt. + Parent []byte + } +) + +const ( + name = "NeoFS Balance" + symbol = "NEOFS" + decimals = 12 + circulation = "MainnetGAS" + version = 1 + + voteKey = "ballots" + blockDiff = 20 // change base on performance evaluation + + netmapContractKey = "netmapScriptHash" + containerContractKey = "containerScriptHash" +) + +var ( + lockTransferMsg = []byte("lock assets to withdraw") + unlockTransferMsg = []byte("asset lock expired") + + ctx storage.Context + token Token +) + +// CreateToken initializes the Token Interface for the Smart Contract to operate with. +func CreateToken() Token { + return Token{ + Name: name, + Symbol: symbol, + Decimals: decimals, + CirculationKey: circulation, + } +} + +func init() { + if runtime.GetTrigger() != runtime.Application { + panic("contract has not been called in application node") + } + + ctx = storage.GetContext() + token = CreateToken() +} + +func Init(addrNetmap, addrContainer []byte) { + if storage.Get(ctx, netmapContractKey) != nil { + panic("init: contract already deployed") + } + + if len(addrNetmap) != 20 || len(addrContainer) != 20 { + panic("init: incorrect length of contract script hash") + } + + storage.Put(ctx, netmapContractKey, addrNetmap) + storage.Put(ctx, containerContractKey, addrContainer) + + runtime.Log("balance contract initialized") +} + +func Name() string { + return token.Name +} + +func Symbol() string { + return token.Symbol +} + +func Decimals() int { + return token.Decimals +} + +func TotalSupply() int { + return token.getSupply(ctx) +} + +func BalanceOf(holder []byte) interface{} { + return token.balanceOf(ctx, holder) +} + +func Transfer(from, to []byte, amount int) bool { + return token.transfer(ctx, from, to, amount, false, nil) +} + +func TransferX(from, to []byte, amount int, details []byte) bool { + var ( + n int // number of votes for inner ring invoke + hashTxID []byte // ballot key of the inner ring invocation + ) + + netmapContractAddr := storage.Get(ctx, netmapContractKey).([]byte) + innerRing := engine.AppCall(netmapContractAddr, "innerRingList").([]irNode) + threshold := len(innerRing)/3*2 + 1 + + irKey := innerRingInvoker(innerRing) + if len(irKey) == 0 { + panic("transferX: this method must be invoked from inner ring") + } + + fromKnownContract := fromKnownContract(runtime.GetCallingScriptHash()) + if fromKnownContract { + n = threshold + runtime.Log("transferX: processed indirect invoke") + } else { + hashTxID = invokeID([]interface{}{from, to, amount}, []byte("transfer")) + n = vote(ctx, hashTxID, irKey) + } + + if n >= threshold { + removeVotes(ctx, hashTxID) + + result := token.transfer(ctx, from, to, amount, true, details) + if result { + runtime.Log("transferX: success") + } else { + // consider panic there + runtime.Log("transferX: fail") + } + + return result + } + + return false +} + +func Lock(txID, from, to []byte, amount, until int) bool { + netmapContractAddr := storage.Get(ctx, netmapContractKey).([]byte) + innerRing := engine.AppCall(netmapContractAddr, "innerRingList").([]irNode) + threshold := len(innerRing)/3*2 + 1 + + irKey := innerRingInvoker(innerRing) + if len(irKey) == 0 { + panic("lock: this method must be invoked from inner ring") + } + + hashTxID := invokeID([]interface{}{txID, from, to, amount, until}, []byte("lock")) + + n := vote(ctx, hashTxID, irKey) + if n >= threshold { + removeVotes(ctx, hashTxID) + + lockAccount := Account{ + Balance: 0, + Until: until, + Parent: from, + } + setSerialized(ctx, to, lockAccount) + + result := token.transfer(ctx, from, to, amount, true, lockTransferMsg) + if !result { + // consider using `return false` to remove votes + panic("lock: can't lock funds") + } + + runtime.Log("lock: created lock account") + runtime.Notify("Lock", txID, from, to, amount, until) + } + + runtime.Log("lock: processed invoke from inner ring") + + return true +} + +func NewEpoch(epochNum int) bool { + netmapContractAddr := storage.Get(ctx, netmapContractKey).([]byte) + innerRing := engine.AppCall(netmapContractAddr, "innerRingList").([]irNode) + threshold := len(innerRing)/3*2 + 1 + + irKey := innerRingInvoker(innerRing) + if len(irKey) == 0 { + panic("epochNum: this method must be invoked from inner ring") + } + + epochID := invokeID([]interface{}{epochNum}, []byte("epoch")) + + n := vote(ctx, epochID, irKey) + if n >= threshold { + removeVotes(ctx, epochID) + it := storage.Find(ctx, []byte{}) + for iterator.Next(it) { + addr := iterator.Key(it).([]byte) + if len(addr) != 20 { + continue + } + + acc := getAccount(ctx, addr) + if acc.Until == 0 { + continue + } + + if epochNum >= acc.Until { + // return assets back to the parent + token.transfer(ctx, addr, acc.Parent, acc.Balance, true, unlockTransferMsg) + } + } + } + + runtime.Log("newEpoch: processed invoke from inner ring") + + return true +} + +func Mint(to []byte, amount int, details []byte) bool { + netmapContractAddr := storage.Get(ctx, netmapContractKey).([]byte) + innerRing := engine.AppCall(netmapContractAddr, "innerRingList").([]irNode) + threshold := len(innerRing)/3*2 + 1 + + irKey := innerRingInvoker(innerRing) + if len(irKey) == 0 { + panic("burn: this method must be invoked from inner ring") + } + + mintID := invokeID([]interface{}{to, amount, details}, []byte("mint")) + + n := vote(ctx, mintID, irKey) + if n >= threshold { + removeVotes(ctx, mintID) + + ok := token.transfer(ctx, nil, to, amount, true, details) + if !ok { + panic("mint: can't transfer assets") + } + + supply := token.getSupply(ctx) + supply = supply + amount + storage.Put(ctx, token.CirculationKey, supply) + runtime.Log("mint: assets were minted") + runtime.Notify("Mint", to, amount) + } + + return true +} + +func Burn(from []byte, amount int, details []byte) bool { + netmapContractAddr := storage.Get(ctx, netmapContractKey).([]byte) + innerRing := engine.AppCall(netmapContractAddr, "innerRingList").([]irNode) + threshold := len(innerRing)/3*2 + 1 + + irKey := innerRingInvoker(innerRing) + if len(irKey) == 0 { + panic("burn: this method must be invoked from inner ring") + } + + burnID := invokeID([]interface{}{from, amount, details}, []byte("burn")) + + n := vote(ctx, burnID, irKey) + if n >= threshold { + removeVotes(ctx, burnID) + + ok := token.transfer(ctx, from, nil, amount, true, details) + if !ok { + panic("burn: can't transfer assets") + } + + supply := token.getSupply(ctx) + if supply < amount { + panic("panic, negative supply after burn") + } + + supply = supply - amount + storage.Put(ctx, token.CirculationKey, supply) + runtime.Log("burn: assets were burned") + runtime.Notify("Burn", from, amount) + } + + return true +} + +func Version() int { + return version +} + +// getSupply gets the token totalSupply value from VM storage. +func (t Token) getSupply(ctx storage.Context) int { + supply := storage.Get(ctx, t.CirculationKey) + if supply != nil { + return supply.(int) + } + + return 0 +} + +// BalanceOf gets the token balance of a specific address. +func (t Token) balanceOf(ctx storage.Context, holder []byte) interface{} { + acc := getAccount(ctx, holder) + + return acc.Balance +} + +func (t Token) transfer(ctx storage.Context, from []byte, to []byte, amount int, innerRing bool, details []byte) bool { + amountFrom, ok := t.canTransfer(ctx, from, to, amount, innerRing) + if !ok { + return false + } + + if len(from) == 20 { + if amountFrom.Balance == amount { + storage.Delete(ctx, from) + } else { + amountFrom.Balance = amountFrom.Balance - amount // neo-go#953 + setSerialized(ctx, from, amountFrom) + } + } + + if len(to) == 20 { + amountTo := getAccount(ctx, to) + amountTo.Balance = amountTo.Balance + amount // neo-go#953 + setSerialized(ctx, to, amountTo) + } + + runtime.Notify("transfer", from, to, amount) + runtime.Notify("transferX", from, to, amount, details) + + return true +} + +// canTransfer returns the amount it can transfer. +func (t Token) canTransfer(ctx storage.Context, from []byte, to []byte, amount int, innerRing bool) (Account, bool) { + var ( + emptyAcc = Account{} + ) + + if !innerRing { + if len(to) != 20 || !isUsableAddress(from) { + runtime.Log("transfer: bad script hashes") + return emptyAcc, false + } + } else if len(from) == 0 { + return emptyAcc, true + } + + amountFrom := getAccount(ctx, from) + if amountFrom.Balance < amount { + runtime.Log("transfer: not enough assets") + return emptyAcc, false + } + + // return amountFrom value back to transfer, reduces extra Get + return amountFrom, true +} + +// isUsableAddress checks if the sender is either the correct NEO address or SC address. +func isUsableAddress(addr []byte) bool { + if len(addr) == 20 { + if runtime.CheckWitness(addr) { + return true + } + + // Check if a smart contract is calling script hash + callingScriptHash := runtime.GetCallingScriptHash() + if bytesEqual(callingScriptHash, addr) { + return true + } + } + + return false +} + +func innerRingInvoker(ir []irNode) []byte { + for i := 0; i < len(ir); i++ { + node := ir[i] + if runtime.CheckWitness(node.key) { + return node.key + } + } + + return nil +} + +func vote(ctx storage.Context, id, from []byte) int { + var ( + newCandidates []ballot + candidates = getBallots(ctx) + found = -1 + blockHeight = blockchain.GetHeight() + ) + + for i := 0; i < len(candidates); i++ { + cnd := candidates[i] + if bytesEqual(cnd.id, id) { + voters := cnd.n + + for j := range voters { + if bytesEqual(voters[j], from) { + return len(voters) + } + } + + voters = append(voters, from) + cnd = ballot{id: id, n: voters, block: blockHeight} + found = len(voters) + } + + // do not add old ballots, they are invalid + if blockHeight-cnd.block <= blockDiff { + newCandidates = append(newCandidates, cnd) + } + } + + if found < 0 { + voters := [][]byte{from} + newCandidates = append(newCandidates, ballot{ + id: id, + n: voters, + block: blockHeight}) + found = 1 + } + + setSerialized(ctx, voteKey, newCandidates) + + return found +} + +func removeVotes(ctx storage.Context, id []byte) { + var ( + newCandidates []ballot + candidates = getBallots(ctx) + ) + + for i := 0; i < len(candidates); i++ { + cnd := candidates[i] + if !bytesEqual(cnd.id, id) { + newCandidates = append(newCandidates, cnd) + } + } + + setSerialized(ctx, voteKey, newCandidates) +} + +func getBallots(ctx storage.Context) []ballot { + data := storage.Get(ctx, voteKey) + if data != nil { + return binary.Deserialize(data.([]byte)).([]ballot) + } + + return []ballot{} +} + +func setSerialized(ctx storage.Context, key interface{}, value interface{}) { + data := binary.Serialize(value) + storage.Put(ctx, key, data) +} + +func getAccount(ctx storage.Context, key interface{}) Account { + data := storage.Get(ctx, key) + if data != nil { + return binary.Deserialize(data.([]byte)).(Account) + } + + return Account{} +} + +func invokeID(args []interface{}, prefix []byte) []byte { + for i := range args { + arg := args[i].([]byte) + prefix = append(prefix, arg...) + } + + return crypto.SHA256(prefix) +} + +// neo-go#1176 +func bytesEqual(a []byte, b []byte) bool { + return util.Equals(string(a), string(b)) +} + +/* + Check if invocation made from known container or audit contracts. + This is necessary because calls from these contracts require to do transfer + without signature collection (1 invoke transfer). + + IR1, IR2, IR3, IR4 -(4 invokes)-> [ Container Contract ] -(1 invoke)-> [ Balance Contract ] + + We can do 1 invoke transfer if: + - invoke happened from inner ring node, + - it is indirect invocation from other smart-contract. + + However there is a possible attack, when malicious inner ring node creates + malicious smart-contract in morph chain to do inderect call. + + MaliciousIR -(1 invoke)-> [ Malicious Contract ] -(1 invoke) -> [ Balance Contract ] + + To prevent that, we have to allow 1 invoke transfer from authorised well known + smart-contracts, that will be set up at `Init` method. +*/ + +func fromKnownContract(caller []byte) bool { + containerContractAddr := storage.Get(ctx, containerContractKey).([]byte) + if bytesEqual(caller, containerContractAddr) { + return true + } + + return false +} diff --git a/balance/config.yml b/balance/config.yml new file mode 100644 index 0000000..b2f3223 --- /dev/null +++ b/balance/config.yml @@ -0,0 +1,45 @@ +hasstorage: true +ispayable: false +events: + - name: Lock + parameters: + - name: txID + type: ByteString + - name: from + type: ByteString + - name: to + type: ByteString + - name: amount + type: Integer + - name: until + type: Integer + - name: transfer + parameters: + - name: from + type: ByteString + - name: to + type: ByteString + - name: amount + type: Integer + - name: transferX + parameters: + - name: from + type: ByteString + - name: to + type: ByteString + - name: amount + type: Integer + - name: details + type: ByteString + - name: Mint + parameters: + - name: from + type: ByteString + - name: amount + type: Integer + - name: Burn + parameters: + - name: to + type: ByteString + - name: amount + type: Integer \ No newline at end of file diff --git a/container/config.yml b/container/config.yml new file mode 100644 index 0000000..7345f3f --- /dev/null +++ b/container/config.yml @@ -0,0 +1,17 @@ +hasstorage: true +ispayable: false +events: + - name: containerPut + parameters: + - name: container + type: ByteString + - name: signature + type: ByteString + - name: publicKey + type: ByteString + - name: containerDelete + parameters: + - name: containerID + type: ByteString + - name: signature + type: ByteString diff --git a/container/container_contract.go b/container/container_contract.go new file mode 100644 index 0000000..f2fdca7 --- /dev/null +++ b/container/container_contract.go @@ -0,0 +1,471 @@ +package containercontract + +import ( + "github.com/nspcc-dev/neo-go/pkg/interop/binary" + "github.com/nspcc-dev/neo-go/pkg/interop/blockchain" + "github.com/nspcc-dev/neo-go/pkg/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/interop/crypto" + "github.com/nspcc-dev/neo-go/pkg/interop/engine" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" + "github.com/nspcc-dev/neo-go/pkg/interop/util" +) + +type ( + irNode struct { + key []byte + } + + ballot struct { + id []byte // id of the voting decision + n [][]byte // already voted inner ring nodes + block int // block with the last vote + } + + extendedACL struct { + val []byte + sig []byte + } +) + +const ( + version = 1 + voteKey = "ballots" + ownersKey = "ownersList" + blockDiff = 20 // change base on performance evaluation + + neofsIDContractKey = "identityScriptHash" + balanceContractKey = "balanceScriptHash" + netmapContractKey = "netmapScriptHash" + containerFeeKey = "ContainerFee" +) + +var ( + containerFeeTransferMsg = []byte("container creation fee") + eACLPrefix = []byte("eACL") + + ctx storage.Context +) + +func init() { + if runtime.GetTrigger() != runtime.Application { + panic("contract has not been called in application node") + } + + ctx = storage.GetContext() +} + +func Init(addrNetmap, addrBalance, addrID []byte) { + if storage.Get(ctx, netmapContractKey) != nil && + storage.Get(ctx, balanceContractKey) != nil && + storage.Get(ctx, neofsIDContractKey) != nil { + panic("init: contract already deployed") + } + + if len(addrNetmap) != 20 || len(addrBalance) != 20 || len(addrID) != 20 { + panic("init: incorrect length of contract script hash") + } + + storage.Put(ctx, netmapContractKey, addrNetmap) + storage.Put(ctx, balanceContractKey, addrBalance) + storage.Put(ctx, neofsIDContractKey, addrID) + + runtime.Log("container contract initialized") +} + +func Put(container, signature, publicKey []byte) bool { + netmapContractAddr := storage.Get(ctx, netmapContractKey).([]byte) + innerRing := engine.AppCall(netmapContractAddr, "innerRingList").([]irNode) + threshold := len(innerRing)/3*2 + 1 + + offset := int(container[1]) + offset = 2 + offset + 4 // version prefix + version size + owner prefix + ownerID := container[offset : offset+25] // offset + size of owner + containerID := crypto.SHA256(container) + neofsIDContractAddr := storage.Get(ctx, neofsIDContractKey).([]byte) + + // If invoked from storage node, ignore it. + // Inner ring will find tx, validate it and send it again. + irKey := innerRingInvoker(innerRing) + if len(irKey) == 0 { + // check provided key + if !isSignedByOwnerKey(container, signature, ownerID, publicKey) { + // check keys from NeoFSID + keys := engine.AppCall(neofsIDContractAddr, "key", ownerID).([][]byte) + if !verifySignature(container, signature, keys) { + panic("put: invalid owner signature") + } + } + + runtime.Notify("containerPut", container, signature, publicKey) + + return true + } + + from := walletToScripHash(ownerID) + balanceContractAddr := storage.Get(ctx, balanceContractKey).([]byte) + containerFee := engine.AppCall(netmapContractAddr, "config", containerFeeKey).(int) + hashCandidate := invokeID([]interface{}{container, signature, publicKey}, []byte("put")) + + n := vote(ctx, hashCandidate, irKey) + if n >= threshold { + removeVotes(ctx, hashCandidate) + // todo: check if new container with unique container id + + for i := 0; i < len(innerRing); i++ { + node := innerRing[i] + to := contract.CreateStandardAccount(node.key) + + tx := engine.AppCall(balanceContractAddr, "transferX", + from, + to, + containerFee, + containerFeeTransferMsg, // consider add container id to the message + ) + if !tx.(bool) { + // todo: consider using `return false` to remove votes + panic("put: can't transfer assets for container creation") + } + } + + addContainer(ctx, containerID, ownerID, container) + // try to remove underscore at v0.92.0 + _ = engine.AppCall(neofsIDContractAddr, "addKey", ownerID, [][]byte{publicKey}) + + runtime.Log("put: added new container") + } else { + runtime.Log("put: processed invoke from inner ring") + } + + return true +} + +func Delete(containerID, signature []byte) bool { + netmapContractAddr := storage.Get(ctx, netmapContractKey).([]byte) + innerRing := engine.AppCall(netmapContractAddr, "innerRingList").([]irNode) + threshold := len(innerRing)/3*2 + 1 + + ownerID := getOwnerByID(ctx, containerID) + if len(ownerID) == 0 { + panic("delete: container does not exist") + } + + // If invoked from storage node, ignore it. + // Inner ring will find tx, validate it and send it again. + irKey := innerRingInvoker(innerRing) + if len(irKey) == 0 { + // check provided key + neofsIDContractAddr := storage.Get(ctx, neofsIDContractKey).([]byte) + keys := engine.AppCall(neofsIDContractAddr, "key", ownerID).([][]byte) + + if !verifySignature(containerID, signature, keys) { + panic("delete: invalid owner signature") + } + + runtime.Notify("containerDelete", containerID, signature) + return true + } + + hashCandidate := invokeID([]interface{}{containerID, signature}, []byte("delete")) + + n := vote(ctx, hashCandidate, irKey) + if n >= threshold { + removeVotes(ctx, hashCandidate) + removeContainer(ctx, containerID, ownerID) + runtime.Log("delete: remove container") + } else { + runtime.Log("delete: processed invoke from inner ring") + } + + return true +} + +func Get(containerID []byte) []byte { + return storage.Get(ctx, containerID).([]byte) +} + +func Owner(containerID []byte) []byte { + return getOwnerByID(ctx, containerID) +} + +func List(owner []byte) [][]byte { + var list [][]byte + + owners := getList(ctx, ownersKey) + for i := 0; i < len(owners); i++ { + ownerID := owners[i] + if len(owner) != 0 && !bytesEqual(owner, ownerID) { + continue + } + + containers := getList(ctx, ownerID) + + for j := 0; j < len(containers); j++ { + container := containers[j] + list = append(list, container) + } + } + + return list +} + +func SetEACL(eACL, signature []byte) bool { + // get container ID + offset := int(eACL[1]) + offset = 2 + offset + 4 + containerID := eACL[offset : offset+32] + + ownerID := getOwnerByID(ctx, containerID) + if len(ownerID) == 0 { + panic("setEACL: container does not exists") + } + + neofsIDContractAddr := storage.Get(ctx, neofsIDContractKey).([]byte) + keys := engine.AppCall(neofsIDContractAddr, "key", ownerID).([][]byte) + + if !verifySignature(eACL, signature, keys) { + panic("setEACL: invalid eACL signature") + } + + rule := extendedACL{ + val: eACL, + sig: signature, + } + + key := append(eACLPrefix, containerID...) + setSerialized(ctx, key, rule) + + runtime.Log("setEACL: success") + + return true +} + +func EACL(containerID []byte) extendedACL { + return getEACL(ctx, containerID) +} + +func Version() int { + return version +} + +func addContainer(ctx storage.Context, id []byte, owner []byte, container []byte) { + addOrAppend(ctx, ownersKey, owner) + addOrAppend(ctx, owner, id) + storage.Put(ctx, id, container) +} + +func removeContainer(ctx storage.Context, id []byte, owner []byte) { + n := remove(ctx, owner, id) + + // if it was last container, remove owner from the list of owners + if n == 0 { + _ = remove(ctx, ownersKey, owner) + } + + storage.Delete(ctx, id) +} + +func addOrAppend(ctx storage.Context, key interface{}, value []byte) { + list := getList(ctx, key) + for i := 0; i < len(list); i++ { + if bytesEqual(list[i], value) { + return + } + } + + if len(list) == 0 { + list = [][]byte{value} + } else { + list = append(list, value) + } + + setSerialized(ctx, key, list) +} + +// remove returns amount of left elements in the list +func remove(ctx storage.Context, key interface{}, value []byte) int { + var ( + list = getList(ctx, key) + newList = [][]byte{} + ) + + for i := 0; i < len(list); i++ { + if !bytesEqual(list[i], value) { + newList = append(newList, list[i]) + } + } + + ln := len(newList) + if ln == 0 { + storage.Delete(ctx, key) + } else { + setSerialized(ctx, key, newList) + } + + return ln +} + +func innerRingInvoker(ir []irNode) []byte { + for i := 0; i < len(ir); i++ { + node := ir[i] + if runtime.CheckWitness(node.key) { + return node.key + } + } + + return nil +} + +func vote(ctx storage.Context, id, from []byte) int { + var ( + newCandidates []ballot + candidates = getBallots(ctx) + found = -1 + blockHeight = blockchain.GetHeight() + ) + + for i := 0; i < len(candidates); i++ { + cnd := candidates[i] + if bytesEqual(cnd.id, id) { + voters := cnd.n + + for j := range voters { + if bytesEqual(voters[j], from) { + return len(voters) + } + } + + voters = append(voters, from) + cnd = ballot{id: id, n: voters, block: blockHeight} + found = len(voters) + } + + // do not add old ballots, they are invalid + if blockHeight-cnd.block <= blockDiff { + newCandidates = append(newCandidates, cnd) + } + } + + if found < 0 { + voters := [][]byte{from} + newCandidates = append(newCandidates, ballot{ + id: id, + n: voters, + block: blockHeight}) + found = 1 + } + + setSerialized(ctx, voteKey, newCandidates) + + return found +} + +func removeVotes(ctx storage.Context, id []byte) { + var ( + newCandidates []ballot + candidates = getBallots(ctx) + ) + + for i := 0; i < len(candidates); i++ { + cnd := candidates[i] + if !bytesEqual(cnd.id, id) { + newCandidates = append(newCandidates, cnd) + } + } + + setSerialized(ctx, voteKey, newCandidates) +} + +func getList(ctx storage.Context, key interface{}) [][]byte { + data := storage.Get(ctx, key) + if data != nil { + return binary.Deserialize(data.([]byte)).([][]byte) + } + + return [][]byte{} +} + +func getBallots(ctx storage.Context) []ballot { + data := storage.Get(ctx, voteKey) + if data != nil { + return binary.Deserialize(data.([]byte)).([]ballot) + } + + return []ballot{} +} + +func getEACL(ctx storage.Context, cid []byte) extendedACL { + key := append(eACLPrefix, cid...) + data := storage.Get(ctx, key) + if data != nil { + return binary.Deserialize(data.([]byte)).(extendedACL) + } + + return extendedACL{val: []byte{}, sig: []byte{}} +} + +func setSerialized(ctx storage.Context, key, value interface{}) { + data := binary.Serialize(value) + storage.Put(ctx, key, data) +} + +func walletToScripHash(wallet []byte) []byte { + return wallet[1 : len(wallet)-4] +} + +func verifySignature(msg, sig []byte, keys [][]byte) bool { + for i := range keys { + key := keys[i] + if crypto.ECDsaSecp256r1Verify(msg, key, sig) { + return true + } + } + + return false +} + +func invokeID(args []interface{}, prefix []byte) []byte { + for i := range args { + arg := args[i].([]byte) + prefix = append(prefix, arg...) + } + + return crypto.SHA256(prefix) +} + +func getOwnerByID(ctx storage.Context, id []byte) []byte { + owners := getList(ctx, ownersKey) + for i := 0; i < len(owners); i++ { + ownerID := owners[i] + containers := getList(ctx, ownerID) + + for j := 0; j < len(containers); j++ { + container := containers[j] + if bytesEqual(container, id) { + return ownerID + } + } + } + + return nil +} + +// neo-go#1176 +func bytesEqual(a []byte, b []byte) bool { + return util.Equals(string(a), string(b)) +} + +func isSignedByOwnerKey(msg, sig, owner, key []byte) bool { + if !isOwnerFromKey(owner, key) { + return false + } + + return crypto.ECDsaSecp256r1Verify(msg, key, sig) +} + +func isOwnerFromKey(owner []byte, key []byte) bool { + ownerSH := walletToScripHash(owner) + keySH := contract.CreateStandardAccount(key) + + return bytesEqual(ownerSH, keySH) +} diff --git a/neofsid/config.yml b/neofsid/config.yml new file mode 100644 index 0000000..7c0479d --- /dev/null +++ b/neofsid/config.yml @@ -0,0 +1,2 @@ +hasstorage: true +ispayable: false diff --git a/neofsid/neofsid_contract.go b/neofsid/neofsid_contract.go new file mode 100644 index 0000000..3c7d51f --- /dev/null +++ b/neofsid/neofsid_contract.go @@ -0,0 +1,314 @@ +package neofsidcontract + +import ( + "github.com/nspcc-dev/neo-go/pkg/interop/binary" + "github.com/nspcc-dev/neo-go/pkg/interop/blockchain" + "github.com/nspcc-dev/neo-go/pkg/interop/crypto" + "github.com/nspcc-dev/neo-go/pkg/interop/engine" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" + "github.com/nspcc-dev/neo-go/pkg/interop/util" +) + +type ( + irNode struct { + key []byte + } + + ballot struct { + id []byte // id of the voting decision + n [][]byte // already voted inner ring nodes + block int // block with the last vote + } + + UserInfo struct { + Keys [][]byte + } +) + +const ( + version = 1 + blockDiff = 20 // change base on performance evaluation + + voteKey = "ballots" + + netmapContractKey = "netmapScriptHash" + containerContractKey = "containerScriptHash" +) + +var ( + ctx storage.Context +) + +func init() { + if runtime.GetTrigger() != runtime.Application { + panic("contract has not been called in application node") + } + + ctx = storage.GetContext() +} + +func Init(addrNetmap, addrContainer []byte) { + if storage.Get(ctx, netmapContractKey) != nil { + panic("init: contract already deployed") + } + + if len(addrNetmap) != 20 || len(addrContainer) != 20 { + panic("init: incorrect length of contract script hash") + } + + storage.Put(ctx, netmapContractKey, addrNetmap) + storage.Put(ctx, containerContractKey, addrContainer) + + runtime.Log("neofsid contract initialized") +} + +func AddKey(owner []byte, keys [][]byte) bool { + var ( + n int // number of votes for inner ring invoke + id []byte // ballot key of the inner ring invocation + ) + + if len(owner) != 25 { + panic("addKey: incorrect owner") + } + + netmapContractAddr := storage.Get(ctx, netmapContractKey).([]byte) + innerRing := engine.AppCall(netmapContractAddr, "innerRingList").([]irNode) + threshold := len(innerRing)/3*2 + 1 + + irKey := innerRingInvoker(innerRing) + if len(irKey) == 0 { + panic("addKey: invocation from non inner ring node") + } + + info := getUserInfo(ctx, owner) + +addLoop: + for i := 0; i < len(keys); i++ { + pubKey := keys[i] + if len(pubKey) != 33 { + panic("addKey: incorrect public key") + } + + for j := range info.Keys { + key := info.Keys[j] + if bytesEqual(key, pubKey) { + continue addLoop + } + } + + info.Keys = append(info.Keys, pubKey) + } + + fromKnownContract := fromKnownContract(runtime.GetCallingScriptHash()) + if fromKnownContract { + n = threshold + runtime.Log("addKey: processed indirect invoke") + } else { + id := invokeIDKeys(owner, keys, []byte("add")) + n = vote(ctx, id, irKey) + } + + if n < threshold { + runtime.Log("addKey: processed invoke from inner ring") + return true + } + + removeVotes(ctx, id) + setSerialized(ctx, owner, info) + runtime.Log("addKey: key bound to the owner") + + return true +} + +func RemoveKey(owner []byte, keys [][]byte) bool { + if len(owner) != 25 { + panic("removeKey: incorrect owner") + } + + netmapContractAddr := storage.Get(ctx, netmapContractKey).([]byte) + innerRing := engine.AppCall(netmapContractAddr, "innerRingList").([]irNode) + threshold := len(innerRing)/3*2 + 1 + + irKey := innerRingInvoker(innerRing) + if len(irKey) == 0 { + panic("removeKey: invocation from non inner ring node") + } + + id := invokeIDKeys(owner, keys, []byte("remove")) + + n := vote(ctx, id, irKey) + if n < threshold { + runtime.Log("removeKey: processed invoke from inner ring") + return true + } + + removeVotes(ctx, id) + + info := getUserInfo(ctx, owner) + var leftKeys [][]byte + +rmLoop: + for i := range info.Keys { + key := info.Keys[i] + + for j := 0; j < len(keys); j++ { + pubKey := keys[j] + if len(pubKey) != 33 { + panic("removeKey: incorrect public key") + } + + if bytesEqual(key, pubKey) { + continue rmLoop + } + } + + leftKeys = append(leftKeys, key) + } + + info.Keys = leftKeys + setSerialized(ctx, owner, info) + + return true +} + +func Key(owner []byte) [][]byte { + if len(owner) != 25 { + panic("key: incorrect owner") + } + + info := getUserInfo(ctx, owner) + + return info.Keys +} + +func Version() int { + return version +} + +func getUserInfo(ctx storage.Context, key interface{}) UserInfo { + data := storage.Get(ctx, key) + if data != nil { + return binary.Deserialize(data.([]byte)).(UserInfo) + } + + return UserInfo{Keys: [][]byte{}} +} + +func getBallots(ctx storage.Context) []ballot { + data := storage.Get(ctx, voteKey) + if data != nil { + return binary.Deserialize(data.([]byte)).([]ballot) + } + + return []ballot{} +} + +func setSerialized(ctx storage.Context, key interface{}, value interface{}) { + data := binary.Serialize(value) + storage.Put(ctx, key, data) +} + +func innerRingInvoker(ir []irNode) []byte { + for i := 0; i < len(ir); i++ { + node := ir[i] + if runtime.CheckWitness(node.key) { + return node.key + } + } + + return nil +} + +func vote(ctx storage.Context, id, from []byte) int { + var ( + newCandidates []ballot + candidates = getBallots(ctx) + found = -1 + blockHeight = blockchain.GetHeight() + ) + + for i := 0; i < len(candidates); i++ { + cnd := candidates[i] + if bytesEqual(cnd.id, id) { + voters := cnd.n + + for j := range voters { + if bytesEqual(voters[j], from) { + return len(voters) + } + } + + voters = append(voters, from) + cnd = ballot{id: id, n: voters, block: blockHeight} + found = len(voters) + } + + // do not add old ballots, they are invalid + if blockHeight-cnd.block <= blockDiff { + newCandidates = append(newCandidates, cnd) + } + } + + if found < 0 { + voters := [][]byte{from} + newCandidates = append(newCandidates, ballot{ + id: id, + n: voters, + block: blockHeight}) + found = 1 + } + + setSerialized(ctx, voteKey, newCandidates) + + return found +} + +func removeVotes(ctx storage.Context, id []byte) { + var ( + newCandidates []ballot + candidates = getBallots(ctx) + ) + + for i := 0; i < len(candidates); i++ { + cnd := candidates[i] + if !bytesEqual(cnd.id, id) { + newCandidates = append(newCandidates, cnd) + } + } + + setSerialized(ctx, voteKey, newCandidates) +} + +func invokeID(args []interface{}, prefix []byte) []byte { + for i := range args { + arg := args[i].([]byte) + prefix = append(prefix, arg...) + } + + return crypto.SHA256(prefix) +} + +func invokeIDKeys(owner []byte, keys [][]byte, prefix []byte) []byte { + prefix = append(prefix, owner...) + for i := range keys { + prefix = append(prefix, keys[i]...) + } + + return crypto.SHA256(prefix) +} + +// neo-go#1176 +func bytesEqual(a []byte, b []byte) bool { + return util.Equals(string(a), string(b)) +} + +func fromKnownContract(caller []byte) bool { + containerContractAddr := storage.Get(ctx, containerContractKey).([]byte) + if bytesEqual(caller, containerContractAddr) { + return true + } + + return false +} diff --git a/netmap/config.yml b/netmap/config.yml new file mode 100644 index 0000000..8899b0f --- /dev/null +++ b/netmap/config.yml @@ -0,0 +1,17 @@ +hasstorage: true +ispayable: false +events: + - name: AddPeer + parameters: + - name: nodeInfo + type: ByteString + - name: UpdateState + parameters: + - name: state + type: Integer + - name: publicKey + type: ByteString + - name: NewEpoch + parameters: + - name: epoch + type: Integer diff --git a/netmap/netmap_contract.go b/netmap/netmap_contract.go new file mode 100644 index 0000000..b510851 --- /dev/null +++ b/netmap/netmap_contract.go @@ -0,0 +1,540 @@ +package netmapcontract + +import ( + "github.com/nspcc-dev/neo-go/pkg/interop/binary" + "github.com/nspcc-dev/neo-go/pkg/interop/blockchain" + "github.com/nspcc-dev/neo-go/pkg/interop/crypto" + "github.com/nspcc-dev/neo-go/pkg/interop/iterator" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" + "github.com/nspcc-dev/neo-go/pkg/interop/util" +) + +type ( + irNode struct { + key []byte + } + + storageNode struct { + info []byte + } + + ballot struct { + id []byte // id of the voting decision + n [][]byte // already voted inner ring nodes + block int // block with the last vote + } + + netmapNode struct { + node storageNode + state nodeState + } + + nodeState int + + record struct { + key []byte + val []byte + } +) + +const ( + version = 1 + blockDiff = 20 // change base on performance evaluation + + voteKey = "ballots" + netmapKey = "netmap" + innerRingKey = "innerring" + configuredKey = "initconfig" + + snapshot0Key = "snapshotCurrent" + snapshot1Key = "snapshotPrevious" + snapshotEpoch = "snapshotEpoch" +) + +const ( + _ nodeState = iota + onlineState + offlineState +) + +var ( + configPrefix = []byte("config") + + ctx storage.Context +) + +func init() { + if runtime.GetTrigger() != runtime.Application { + panic("contract has not been called in application node") + } + + ctx = storage.GetContext() +} + +// Init function sets up initial list of inner ring public keys and should +// be invoked once at neofs infrastructure setup. +func Init(keys [][]byte) { + if storage.Get(ctx, innerRingKey) != nil { + panic("netmap: contract already initialized") + } + + var irList []irNode + + for i := 0; i < len(keys); i++ { + key := keys[i] + irList = append(irList, irNode{key: key}) + } + + setSerialized(ctx, innerRingKey, irList) + + // 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 + setSerialized(ctx, netmapKey, []netmapNode{}) + setSerialized(ctx, snapshot0Key, []netmapNode{}) + setSerialized(ctx, snapshot1Key, []netmapNode{}) + setSerialized(ctx, voteKey, []ballot{}) + + runtime.Log("netmap contract initialized") +} + +func InnerRingList() []irNode { + return getIRNodes(ctx) +} + +func UpdateInnerRing(keys [][]byte) bool { + innerRing := getIRNodes(ctx) + threshold := len(innerRing)/3*2 + 1 + + irKey := innerRingInvoker(innerRing) + if len(irKey) == 0 { + panic("updateInnerRing: this method must be invoked by inner ring nodes") + } + + var irList []irNode + + for i := 0; i < len(keys); i++ { + key := keys[i] + irList = append(irList, irNode{key: key}) + } + + rawIRList := binary.Serialize(irList) + hashIRList := crypto.SHA256(rawIRList) + + n := vote(ctx, hashIRList, irKey) + if n >= threshold { + runtime.Log("updateInnerRing: inner ring list updated") + setSerialized(ctx, innerRingKey, irList) + removeVotes(ctx, hashIRList) + } else { + runtime.Log("updateInnerRing: processed invoke from inner ring") + } + + return true +} + +func AddPeer(nodeInfo []byte) bool { + innerRing := getIRNodes(ctx) + threshold := len(innerRing)/3*2 + 1 + + irKey := innerRingInvoker(innerRing) + if len(irKey) == 0 { + publicKey := nodeInfo[2:35] // offset:2, len:33 + if !runtime.CheckWitness(publicKey) { + panic("addPeer: witness check failed") + } + runtime.Notify("AddPeer", nodeInfo) + return true + } + + candidate := storageNode{ + info: nodeInfo, + } + rawCandidate := binary.Serialize(candidate) + hashCandidate := crypto.SHA256(rawCandidate) + + nm := addToNetmap(ctx, candidate) + + n := vote(ctx, hashCandidate, irKey) + if n >= threshold { + if nm == nil { + runtime.Log("addPeer: storage node already in the netmap") + } else { + setSerialized(ctx, netmapKey, nm) + runtime.Log("addPeer: add storage node to the network map") + } + removeVotes(ctx, hashCandidate) + } else { + runtime.Log("addPeer: processed invoke from inner ring") + } + + return true +} + +func UpdateState(state int, publicKey []byte) bool { + if len(publicKey) != 33 { + panic("updateState: incorrect public key") + } + + innerRing := getIRNodes(ctx) + threshold := len(innerRing)/3*2 + 1 + + irKey := innerRingInvoker(innerRing) + if len(irKey) == 0 { + if !runtime.CheckWitness(publicKey) { + panic("updateState: witness check failed") + } + runtime.Notify("UpdateState", state, publicKey) + return true + } + + switch nodeState(state) { + case offlineState: + newNetmap := removeFromNetmap(ctx, publicKey) + + hashID := invokeID([]interface{}{publicKey}, []byte("delete")) + n := vote(ctx, hashID, irKey) + if n >= threshold { + runtime.Log("updateState: remove storage node from the network map") + setSerialized(ctx, netmapKey, newNetmap) + removeVotes(ctx, hashID) + } else { + runtime.Log("updateState: processed invoke from inner ring") + } + default: + panic("updateState: unsupported state") + } + + return true +} + +func NewEpoch(epochNum int) bool { + innerRing := getIRNodes(ctx) + threshold := len(innerRing)/3*2 + 1 + + irKey := innerRingInvoker(innerRing) + if len(irKey) == 0 { + panic("newEpoch: this method must be invoked by inner ring nodes") + } + + data0snapshot := getSnapshot(ctx, snapshot0Key) + dataOnlineState := filterNetmap(ctx, onlineState) + + hashID := invokeID([]interface{}{epochNum}, []byte("epoch")) + + n := vote(ctx, hashID, irKey) + if n >= threshold { + 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 + setSerialized(ctx, snapshot1Key, data0snapshot) + + // put netmap into actual snapshot + setSerialized(ctx, snapshot0Key, dataOnlineState) + + removeVotes(ctx, hashID) + runtime.Notify("NewEpoch", epochNum) + } else { + runtime.Log("newEpoch: processed invoke from inner ring") + } + + return true +} + +func Epoch() int { + return storage.Get(ctx, snapshotEpoch).(int) +} + +func Netmap() []storageNode { + return getSnapshot(ctx, snapshot0Key) +} + +func Snapshot(diff int) []storageNode { + var key string + switch diff { + case 0: + key = snapshot0Key + case 1: + key = snapshot1Key + default: + panic("snapshot: incorrect diff") + } + + return getSnapshot(ctx, key) +} + +func Config(key []byte) interface{} { + return getConfig(ctx, key) +} + +func SetConfig(id, key, val []byte) bool { + // check if it is inner ring invocation + innerRing := getIRNodes(ctx) + threshold := len(innerRing)/3*2 + 1 + + irKey := innerRingInvoker(innerRing) + if len(irKey) == 0 { + panic("setConfig: invoked by non inner ring node") + } + + // check unique id of the operation + hashID := invokeID([]interface{}{id, key, val}, []byte("config")) + n := vote(ctx, hashID, irKey) + + if n >= threshold { + removeVotes(ctx, hashID) + setConfig(ctx, key, val) + + runtime.Log("setConfig: configuration has been updated") + } + + return true +} + +func InitConfig(args [][]byte) bool { + 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") + + return true +} + +func ListConfig() []record { + var config []record + + it := storage.Find(ctx, configPrefix) + for iterator.Next(it) { + key := iterator.Key(it).([]byte) + val := iterator.Value(it).([]byte) + r := record{key: key[len(configPrefix):], val: val} + + config = append(config, r) + } + + return config +} + +func Version() int { + return version +} + +func innerRingInvoker(ir []irNode) []byte { + for i := 0; i < len(ir); i++ { + node := ir[i] + if runtime.CheckWitness(node.key) { + return node.key + } + } + + return nil +} + +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 bytesEqual(newNodeKey, netmapNodeKey) { + return nil + } + } + + netmap = append(netmap, node) + + return netmap +} + +func removeFromNetmap(ctx storage.Context, key []byte) []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 !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 vote(ctx storage.Context, id, from []byte) int { + var ( + newCandidates []ballot + candidates = getBallots(ctx) + found = -1 + blockHeight = blockchain.GetHeight() + ) + + for i := 0; i < len(candidates); i++ { + cnd := candidates[i] + if bytesEqual(cnd.id, id) { + voters := cnd.n + + for j := range voters { + if bytesEqual(voters[j], from) { + return len(voters) + } + } + + voters = append(voters, from) + cnd = ballot{id: id, n: voters, block: blockHeight} + found = len(voters) + } + + // do not add old ballots, they are invalid + if blockHeight-cnd.block <= blockDiff { + newCandidates = append(newCandidates, cnd) + } + } + + if found < 0 { + voters := [][]byte{from} + newCandidates = append(newCandidates, ballot{ + id: id, + n: voters, + block: blockHeight}) + found = 1 + } + + setSerialized(ctx, voteKey, newCandidates) + + return found +} + +func removeVotes(ctx storage.Context, id []byte) { + var ( + newCandidates []ballot + candidates = getBallots(ctx) + ) + + for i := 0; i < len(candidates); i++ { + cnd := candidates[i] + if !bytesEqual(cnd.id, id) { + newCandidates = append(newCandidates, cnd) + } + } + + setSerialized(ctx, voteKey, newCandidates) +} + +func getIRNodes(ctx storage.Context) []irNode { + data := storage.Get(ctx, innerRingKey) + if data != nil { + return binary.Deserialize(data.([]byte)).([]irNode) + } + + return []irNode{} +} + +func getNetmapNodes(ctx storage.Context) []netmapNode { + data := storage.Get(ctx, netmapKey) + if data != nil { + return binary.Deserialize(data.([]byte)).([]netmapNode) + } + + return []netmapNode{} +} + +func getSnapshot(ctx storage.Context, key string) []storageNode { + data := storage.Get(ctx, key) + if data != nil { + return binary.Deserialize(data.([]byte)).([]storageNode) + } + + return []storageNode{} +} + +func getBallots(ctx storage.Context) []ballot { + data := storage.Get(ctx, voteKey) + if data != nil { + return binary.Deserialize(data.([]byte)).([]ballot) + } + + return []ballot{} +} + +func setSerialized(ctx storage.Context, key interface{}, value interface{}) { + data := binary.Serialize(value) + storage.Put(ctx, key, data) +} + +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 invokeID(args []interface{}, prefix []byte) []byte { + for i := range args { + arg := args[i].([]byte) + prefix = append(prefix, arg...) + } + + return crypto.SHA256(prefix) +} + +// neo-go#1176 +func bytesEqual(a []byte, b []byte) bool { + return util.Equals(string(a), string(b)) +} diff --git a/reputation/config.yml b/reputation/config.yml new file mode 100644 index 0000000..7c0479d --- /dev/null +++ b/reputation/config.yml @@ -0,0 +1,2 @@ +hasstorage: true +ispayable: false diff --git a/reputation/reputation_contract.go b/reputation/reputation_contract.go new file mode 100644 index 0000000..2a62cf1 --- /dev/null +++ b/reputation/reputation_contract.go @@ -0,0 +1,92 @@ +package reputationcontract + +import ( + "github.com/nspcc-dev/neo-go/pkg/interop/binary" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" +) + +const version = 1 + +const ( + peerIDSize = 46 // NeoFS PeerIDSize + trustValSize = 8 // float64 + + trustStructSize = 0 + + peerIDSize + // manager ID + peerIDSize + // trusted ID + trustValSize // trust value +) + +var ( + trustJournalPrefix = []byte("trustJournal") + + ctx storage.Context +) + +func init() { + if runtime.GetTrigger() != runtime.Application { + panic("contract has not been called in application node") + } + + ctx = storage.GetContext() +} + +func Put(manager, epoch, typ []byte, newTrustList [][]byte) bool { + if !runtime.CheckWitness(manager) { + panic("put: incorrect manager key") + } + + for i := 0; i < len(newTrustList); i++ { + trustData := newTrustList[i] + + if len(trustData) != trustStructSize { + panic("list: invalid trust value") + } + } + + // todo: consider using notification for inner ring node + + // todo: limit size of the trust journal: + // history will be stored in chain (args or notifies) + // contract storage will be used as a cache if needed + key := append(trustJournalPrefix, append(epoch, typ...)...) + + trustList := getList(ctx, key) + + // fixme: with neo3.0 it is kinda unnecessary + if len(trustList) == 0 { + // if journal slice is not initialized, then `append` will throw + trustList = newTrustList + } else { + for i := 0; i < len(newTrustList); i++ { + trustList = append(trustList, newTrustList[i]) + } + } + + setSerialized(ctx, key, trustList) + + runtime.Log("trust list was successfully updated") + + return true +} + +func List(epoch, typ []byte) [][]byte { + key := append(trustJournalPrefix, append(epoch, typ...)...) + + return getList(ctx, key) +} + +func getList(ctx storage.Context, key interface{}) [][]byte { + data := storage.Get(ctx, key) + if data != nil { + return binary.Deserialize(data.([]byte)).([][]byte) + } + + return [][]byte{} +} + +func setSerialized(ctx storage.Context, key interface{}, value interface{}) { + data := binary.Serialize(value) + storage.Put(ctx, key, data) +}