2020-03-30 13:59:54 +00:00
|
|
|
package smart_contract
|
|
|
|
|
|
|
|
import (
|
|
|
|
"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"
|
|
|
|
)
|
|
|
|
|
2020-05-27 12:23:44 +00:00
|
|
|
type (
|
|
|
|
ballot struct {
|
|
|
|
id []byte
|
|
|
|
n int
|
|
|
|
}
|
2020-03-30 13:59:54 +00:00
|
|
|
|
2020-05-27 12:23:44 +00:00
|
|
|
node struct {
|
|
|
|
pub []byte
|
|
|
|
}
|
2020-03-30 13:59:54 +00:00
|
|
|
|
2020-05-27 12:23:44 +00:00
|
|
|
check struct {
|
|
|
|
id []byte
|
|
|
|
height []byte
|
|
|
|
}
|
|
|
|
)
|
2020-03-30 13:59:54 +00:00
|
|
|
|
2020-05-27 12:23:44 +00:00
|
|
|
const (
|
|
|
|
// GAS NEP-5 HASH
|
|
|
|
tokenHash = "\x77\xea\x59\x6b\x7a\xdf\x7e\x4d\xd1\x40\x76\x97\x31\xb7\xd2\xf0\xe0\x6b\xcd\x9b"
|
|
|
|
innerRingCandidateFee = 100 * 1000 * 1000 // 10^8
|
|
|
|
version = 1
|
|
|
|
voteKey = "ballots"
|
|
|
|
)
|
2020-03-30 13:59:54 +00:00
|
|
|
|
|
|
|
func Main(op string, args []interface{}) interface{} {
|
|
|
|
// The trigger determines whether this smart-contract is being
|
|
|
|
// run in 'verification' or 'application' mode.
|
|
|
|
if runtime.GetTrigger() != runtime.Application() {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
2020-05-22 08:06:12 +00:00
|
|
|
Utility operations - they will be changed in production:
|
|
|
|
- Deploy(params: address, pubKey, ... ) - setup initial inner ring state
|
|
|
|
|
|
|
|
User operations:
|
|
|
|
- InnerRingList() - get list of inner ring nodes addresses and public keys
|
|
|
|
- InnerRingCandidateRemove(params: pubKey) - remove node with given public key from the inner ring candidate queue
|
2020-05-26 14:48:56 +00:00
|
|
|
- InnerRingCandidateAdd(params: pubKey) - add node to the inner ring candidate queue
|
2020-05-22 08:06:12 +00:00
|
|
|
- Deposit(params: pubKey, amount) - deposit GAS to the NeoFS account
|
|
|
|
- Withdraw(params: withdrawCheque) - withdraw GAS from the NeoFS account
|
|
|
|
- InnerRingUpdate(params: irCheque) - change list of inner ring nodes
|
|
|
|
- IsInnerRing(params: pubKey) - returns true if pubKey presented in inner ring list
|
|
|
|
- Version() - get version of the NeoFS smart-contract
|
|
|
|
|
|
|
|
Params:
|
|
|
|
- address - string of the valid multiaddress (github.com/multiformats/multiaddr)
|
|
|
|
- pubKey - 33 byte public key
|
|
|
|
- withdrawCheque - serialized structure, that confirms GAS transfer;
|
|
|
|
contains inner ring signatures
|
|
|
|
- irCheque - serialized structure, that confirms new inner ring node list;
|
|
|
|
contains inner ring signatures
|
2020-03-30 13:59:54 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
ctx := storage.GetContext()
|
|
|
|
switch op {
|
|
|
|
case "Deploy":
|
|
|
|
irList := getSerialized(ctx, "InnerRingList").([]node)
|
|
|
|
if len(irList) >= 3 {
|
|
|
|
panic("contract already deployed")
|
|
|
|
}
|
|
|
|
|
|
|
|
irList = []node{}
|
2020-05-26 14:48:56 +00:00
|
|
|
for i := 0; i < len(args); i++ {
|
|
|
|
pub := args[i].([]byte)
|
|
|
|
irList = append(irList, node{pub: pub})
|
2020-03-30 13:59:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
data := runtime.Serialize(irList)
|
|
|
|
storage.Put(ctx, "InnerRingList", data)
|
|
|
|
|
|
|
|
data = runtime.Serialize([]interface{}{})
|
|
|
|
storage.Put(ctx, "UsedVerifCheckList", data)
|
|
|
|
storage.Put(ctx, "InnerRingCandidates", data)
|
|
|
|
|
2020-05-27 12:23:44 +00:00
|
|
|
data = runtime.Serialize([]ballot{})
|
|
|
|
storage.Put(ctx, voteKey, data)
|
|
|
|
|
2020-03-30 13:59:54 +00:00
|
|
|
return true
|
|
|
|
case "InnerRingList":
|
|
|
|
irList := getSerialized(ctx, "InnerRingList").([]node)
|
|
|
|
|
|
|
|
return irList
|
|
|
|
case "InnerRingCandidateRemove":
|
|
|
|
data := args[0].([]byte) // public key
|
|
|
|
if !runtime.CheckWitness(data) {
|
|
|
|
panic("you should be the owner of the public key")
|
|
|
|
}
|
|
|
|
|
|
|
|
delSerializedIR(ctx, "InnerRingCandidates", data)
|
|
|
|
|
|
|
|
return true
|
|
|
|
case "InnerRingCandidateAdd":
|
2020-05-26 14:48:56 +00:00
|
|
|
key := args[0].([]byte) // public key
|
2020-03-30 13:59:54 +00:00
|
|
|
|
2020-05-26 14:48:56 +00:00
|
|
|
if !runtime.CheckWitness(key) {
|
2020-03-30 13:59:54 +00:00
|
|
|
panic("you should be the owner of the public key")
|
|
|
|
}
|
|
|
|
|
|
|
|
candidates := getSerialized(ctx, "InnerRingCandidates").([]node)
|
2020-05-26 14:48:56 +00:00
|
|
|
if containsPub(candidates, key) {
|
2020-03-30 13:59:54 +00:00
|
|
|
panic("is already in list")
|
|
|
|
}
|
|
|
|
|
2020-05-26 14:48:56 +00:00
|
|
|
from := pubToScriptHash(key)
|
2020-03-30 13:59:54 +00:00
|
|
|
to := engine.GetExecutingScriptHash()
|
|
|
|
params := []interface{}{from, to, innerRingCandidateFee}
|
|
|
|
|
|
|
|
transferred := engine.AppCall([]byte(tokenHash), "transfer", params).(bool)
|
|
|
|
if !transferred {
|
|
|
|
panic("failed to transfer funds, aborting")
|
|
|
|
}
|
|
|
|
|
2020-05-26 14:48:56 +00:00
|
|
|
candidate := node{pub: key}
|
2020-03-30 13:59:54 +00:00
|
|
|
if !putSerialized(ctx, "InnerRingCandidates", candidate) {
|
|
|
|
panic("failed to put candidate into the queue")
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
case "Deposit":
|
|
|
|
pk := args[0].([]byte)
|
|
|
|
if !runtime.CheckWitness(pk) {
|
|
|
|
panic("you should be the owner of the public key")
|
|
|
|
}
|
|
|
|
|
|
|
|
amount := args[1].(int)
|
|
|
|
if amount > 0 {
|
|
|
|
amount = amount * 100000000
|
|
|
|
}
|
|
|
|
|
|
|
|
from := pubToScriptHash(pk)
|
|
|
|
to := engine.GetExecutingScriptHash()
|
|
|
|
params := []interface{}{from, to, amount}
|
|
|
|
transferred := engine.AppCall([]byte(tokenHash), "transfer", params).(bool)
|
|
|
|
if !transferred {
|
|
|
|
panic("failed to transfer funds, aborting")
|
|
|
|
}
|
|
|
|
|
|
|
|
runtime.Log("funds have been transferred")
|
|
|
|
|
|
|
|
return true
|
|
|
|
case "Withdraw":
|
|
|
|
from := engine.GetExecutingScriptHash()
|
|
|
|
data := args[0].([]byte)
|
|
|
|
message := data[0:66]
|
|
|
|
uuid := data[0:25]
|
|
|
|
owner := data[25:50]
|
|
|
|
value := data[50:58]
|
|
|
|
height := data[58:66]
|
|
|
|
offset := 68
|
|
|
|
usedList := getSerialized(ctx, "UsedVerifCheckList").([]check)
|
|
|
|
|
|
|
|
c := check{
|
|
|
|
id: uuid,
|
|
|
|
height: height,
|
|
|
|
}
|
|
|
|
if containsCheck(usedList, c) {
|
|
|
|
panic("verification check has already been used")
|
|
|
|
}
|
|
|
|
|
|
|
|
irList := getSerialized(ctx, "InnerRingList").([]node)
|
|
|
|
if !verifySignatures(irList, data, message, offset) {
|
|
|
|
panic("can't verify signatures")
|
|
|
|
}
|
|
|
|
|
|
|
|
h := owner[1:21]
|
|
|
|
params := []interface{}{from, h, value}
|
|
|
|
transferred := engine.AppCall([]byte(tokenHash), "transfer", params).(bool)
|
|
|
|
if !transferred {
|
|
|
|
panic("failed to transfer funds, aborting")
|
|
|
|
}
|
|
|
|
|
|
|
|
putSerialized(ctx, "UsedVerifCheckList", c)
|
|
|
|
|
|
|
|
return true
|
|
|
|
case "InnerRingUpdate":
|
|
|
|
data := args[0].([]byte)
|
|
|
|
|
|
|
|
id := data[:8]
|
|
|
|
var ln interface{} = data[8:10]
|
|
|
|
listItemCount := ln.(int)
|
|
|
|
listSize := listItemCount * 33
|
|
|
|
|
|
|
|
offset := 8 + 2 + listSize
|
|
|
|
|
|
|
|
irList := getSerialized(ctx, "InnerRingList").([]node)
|
|
|
|
usedList := getSerialized(ctx, "UsedVerifCheckList").([]check)
|
2020-05-27 12:23:44 +00:00
|
|
|
threshold := len(irList)/3*2 + 1
|
|
|
|
|
|
|
|
if !isInnerRingRequest(irList) {
|
|
|
|
panic("innerRingUpdate: invoked by non inner ring node")
|
2020-03-30 13:59:54 +00:00
|
|
|
}
|
2020-05-27 12:23:44 +00:00
|
|
|
|
|
|
|
c := check{id: id}
|
2020-03-30 13:59:54 +00:00
|
|
|
if containsCheck(usedList, c) {
|
2020-05-27 12:23:44 +00:00
|
|
|
panic("innerRingUpdate: cheque has non unique id")
|
2020-03-30 13:59:54 +00:00
|
|
|
}
|
|
|
|
|
2020-05-27 12:23:44 +00:00
|
|
|
chequeHash := crypto.Hash256(data)
|
|
|
|
|
|
|
|
n := vote(ctx, chequeHash)
|
|
|
|
if n >= threshold {
|
|
|
|
removeVotes(ctx, chequeHash)
|
|
|
|
|
|
|
|
candidates := getSerialized(ctx, "InnerRingCandidates").([]node)
|
|
|
|
offset = 10
|
|
|
|
newIR := []node{}
|
|
|
|
|
|
|
|
loop:
|
|
|
|
for i := 0; i < listItemCount; i, offset = i+1, offset+33 {
|
|
|
|
pub := data[offset : offset+33]
|
|
|
|
|
|
|
|
for j := 0; j < len(irList); j++ {
|
|
|
|
n := irList[j]
|
|
|
|
if util.Equals(n.pub, pub) {
|
|
|
|
newIR = append(newIR, n)
|
|
|
|
continue loop
|
|
|
|
}
|
2020-03-30 13:59:54 +00:00
|
|
|
}
|
|
|
|
|
2020-05-27 12:23:44 +00:00
|
|
|
for j := 0; j < len(candidates); j++ {
|
|
|
|
n := candidates[j]
|
|
|
|
if util.Equals(n.pub, pub) {
|
|
|
|
newIR = append(newIR, n)
|
|
|
|
continue loop
|
|
|
|
}
|
2020-03-30 13:59:54 +00:00
|
|
|
}
|
|
|
|
}
|
2020-05-20 09:38:11 +00:00
|
|
|
|
2020-05-27 12:23:44 +00:00
|
|
|
if len(newIR) != listItemCount {
|
|
|
|
panic("new inner ring wasn't processed correctly")
|
|
|
|
}
|
2020-03-30 13:59:54 +00:00
|
|
|
|
2020-05-27 12:23:44 +00:00
|
|
|
for i := 0; i < len(newIR); i++ {
|
|
|
|
n := newIR[i]
|
|
|
|
delSerializedIR(ctx, "InnerRingCandidates", n.pub)
|
|
|
|
}
|
2020-03-30 13:59:54 +00:00
|
|
|
|
2020-05-27 12:23:44 +00:00
|
|
|
newIRData := runtime.Serialize(newIR)
|
|
|
|
storage.Put(ctx, "InnerRingList", newIRData)
|
|
|
|
putSerialized(ctx, "UsedVerifCheckList", c)
|
|
|
|
|
|
|
|
runtime.Notify("InnerRingUpdate", c.id, newIRData)
|
|
|
|
}
|
2020-03-30 13:59:54 +00:00
|
|
|
|
|
|
|
return true
|
2020-05-22 08:06:12 +00:00
|
|
|
case "IsInnerRing":
|
|
|
|
if len(args) != 1 {
|
|
|
|
panic("isInnerRing: wrong arguments")
|
|
|
|
}
|
|
|
|
|
|
|
|
key := args[0].([]byte)
|
|
|
|
if len(key) != 33 {
|
|
|
|
panic("isInnerRing: incorrect public key")
|
|
|
|
}
|
|
|
|
|
|
|
|
irList := getSerialized(ctx, "InnerRingList").([]node)
|
|
|
|
for i := range irList {
|
|
|
|
node := irList[i]
|
|
|
|
|
|
|
|
if util.Equals(node.pub, key) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
2020-03-30 13:59:54 +00:00
|
|
|
case "Version":
|
|
|
|
return version
|
|
|
|
}
|
|
|
|
|
|
|
|
panic("unknown operation")
|
|
|
|
}
|
|
|
|
|
|
|
|
func getSerialized(ctx storage.Context, key string) interface{} {
|
|
|
|
data := storage.Get(ctx, key).([]byte)
|
|
|
|
if len(data) != 0 {
|
|
|
|
return runtime.Deserialize(data)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func delSerialized(ctx storage.Context, key string, value []byte) bool {
|
|
|
|
data := storage.Get(ctx, key).([]byte)
|
|
|
|
deleted := false
|
|
|
|
|
|
|
|
var newList [][]byte
|
|
|
|
if len(data) != 0 {
|
|
|
|
lst := runtime.Deserialize(data).([][]byte)
|
|
|
|
for i := 0; i < len(lst); i++ {
|
|
|
|
if util.Equals(value, lst[i]) {
|
|
|
|
deleted = true
|
|
|
|
} else {
|
|
|
|
newList = append(newList, lst[i])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if deleted {
|
|
|
|
if len(newList) != 0 {
|
|
|
|
data := runtime.Serialize(newList)
|
|
|
|
storage.Put(ctx, key, data)
|
|
|
|
} else {
|
|
|
|
storage.Delete(ctx, key)
|
|
|
|
}
|
|
|
|
runtime.Log("target element has been removed")
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
runtime.Log("target element has not been removed")
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func putSerialized(ctx storage.Context, key string, value interface{}) bool {
|
|
|
|
data := storage.Get(ctx, key).([]byte)
|
|
|
|
|
|
|
|
var lst []interface{}
|
|
|
|
if len(data) != 0 {
|
|
|
|
lst = runtime.Deserialize(data).([]interface{})
|
|
|
|
}
|
|
|
|
|
|
|
|
lst = append(lst, value)
|
|
|
|
data = runtime.Serialize(lst)
|
|
|
|
storage.Put(ctx, key, data)
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
func pubToScriptHash(pkey []byte) []byte {
|
|
|
|
pre := []byte{0x21}
|
|
|
|
buf := append(pre, pkey...)
|
|
|
|
buf = append(buf, 0xac)
|
|
|
|
h := crypto.Hash160(buf)
|
|
|
|
return h
|
|
|
|
}
|
|
|
|
|
|
|
|
func containsCheck(lst []check, c check) bool {
|
|
|
|
for i := 0; i < len(lst); i++ {
|
|
|
|
if util.Equals(c, lst[i]) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
func containsPub(lst []node, elem []byte) bool {
|
|
|
|
for i := 0; i < len(lst); i++ {
|
|
|
|
e := lst[i]
|
|
|
|
if util.Equals(elem, e.pub) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func delSerializedIR(ctx storage.Context, key string, value []byte) bool {
|
|
|
|
data := storage.Get(ctx, key).([]byte)
|
|
|
|
deleted := false
|
|
|
|
|
|
|
|
newList := []node{}
|
|
|
|
if len(data) != 0 {
|
|
|
|
lst := runtime.Deserialize(data).([]node)
|
|
|
|
for i := 0; i < len(lst); i++ {
|
|
|
|
n := lst[i]
|
|
|
|
if util.Equals(value, n.pub) {
|
|
|
|
deleted = true
|
|
|
|
} else {
|
|
|
|
newList = append(newList, n)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if deleted {
|
|
|
|
data := runtime.Serialize(newList)
|
|
|
|
storage.Put(ctx, key, data)
|
|
|
|
runtime.Log("target element has been removed")
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
runtime.Log("target element has not been removed")
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
func verifySignatures(irList []node, data []byte, message []byte, offset int) bool {
|
|
|
|
n := len(irList)
|
|
|
|
f := (n - 1) / 3
|
|
|
|
s := n - f
|
|
|
|
if s < 3 {
|
|
|
|
runtime.Log("not enough inner ring nodes for consensus")
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
used := [][]byte{}
|
|
|
|
count := 0
|
|
|
|
for count < s && offset < len(data) {
|
|
|
|
pubkey := data[offset : offset+33]
|
|
|
|
signature := data[offset+33 : offset+97]
|
|
|
|
if containsPub(irList, pubkey) {
|
|
|
|
if crypto.VerifySignature(message, signature, pubkey) {
|
|
|
|
count++
|
|
|
|
for i := 0; i < len(used); i++ {
|
|
|
|
if util.Equals(used[i], pubkey) {
|
|
|
|
panic("duplicate public keys")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
used = append(used, pubkey)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
offset += 97
|
|
|
|
}
|
|
|
|
|
|
|
|
if count >= s {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
runtime.Log("not enough verified signatures")
|
|
|
|
return false
|
|
|
|
}
|
2020-05-27 12:23:44 +00:00
|
|
|
|
|
|
|
// isInnerRingRequest returns true if contract was invoked by inner ring node.
|
|
|
|
func isInnerRingRequest(irList []node) bool {
|
|
|
|
for i := 0; i < len(irList); i++ {
|
|
|
|
irNode := irList[i]
|
|
|
|
|
|
|
|
if runtime.CheckWitness(irNode.pub) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// todo: votes must be from unique inner ring nods
|
|
|
|
func vote(ctx storage.Context, id []byte) int {
|
|
|
|
var (
|
|
|
|
newCandidates = []ballot{}
|
|
|
|
candidates = getSerialized(ctx, voteKey).([]ballot)
|
|
|
|
found = -1
|
|
|
|
)
|
|
|
|
|
|
|
|
for i := 0; i < len(candidates); i++ {
|
|
|
|
cnd := candidates[i]
|
|
|
|
if util.Equals(cnd.id, id) {
|
|
|
|
cnd = ballot{id: id, n: cnd.n + 1}
|
|
|
|
found = cnd.n
|
|
|
|
}
|
|
|
|
newCandidates = append(newCandidates, cnd)
|
|
|
|
}
|
|
|
|
|
|
|
|
if found < 0 {
|
|
|
|
newCandidates = append(newCandidates, ballot{id: id, n: 1})
|
|
|
|
found = 1
|
|
|
|
}
|
|
|
|
|
|
|
|
data := runtime.Serialize(newCandidates)
|
|
|
|
storage.Put(ctx, voteKey, data)
|
|
|
|
|
|
|
|
return found
|
|
|
|
}
|
|
|
|
|
|
|
|
func removeVotes(ctx storage.Context, id []byte) {
|
|
|
|
var (
|
|
|
|
newCandidates = []ballot{}
|
|
|
|
candidates = getSerialized(ctx, voteKey).([]ballot)
|
|
|
|
)
|
|
|
|
|
|
|
|
for i := 0; i < len(candidates); i++ {
|
|
|
|
cnd := candidates[i]
|
|
|
|
if !util.Equals(cnd.id, id) {
|
|
|
|
newCandidates = append(newCandidates, cnd)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
data := runtime.Serialize(newCandidates)
|
|
|
|
storage.Put(ctx, voteKey, data)
|
|
|
|
}
|