From e586588e4431a18381403b329b2247e6b76a5ed1 Mon Sep 17 00:00:00 2001 From: Alex Vanin Date: Tue, 20 Apr 2021 17:27:15 +0300 Subject: [PATCH 01/19] [#74] neofs: Add `AlphabetAddress` method This method returns multi signature address of alphabet nodes in NeoFS contract. Signed-off-by: Alex Vanin --- neofs/config.yml | 2 +- neofs/neofs_contract.go | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/neofs/config.yml b/neofs/config.yml index 9fc400ab..b22aa29e 100644 --- a/neofs/config.yml +++ b/neofs/config.yml @@ -1,5 +1,5 @@ name: "NeoFS" -safemethods: ["alphabetList", "innerRingCandidates", "config", "listConfig", "version"] +safemethods: ["alphabetList", "alphabetAddress", "innerRingCandidates", "config", "listConfig", "version"] events: - name: Deposit parameters: diff --git a/neofs/neofs_contract.go b/neofs/neofs_contract.go index c6170025..b0369935 100644 --- a/neofs/neofs_contract.go +++ b/neofs/neofs_contract.go @@ -15,6 +15,7 @@ package smart_contract Inner ring list related methods: - AlphabetList + - AlphabetAddress - InnerRingCandidates - InnerRingCandidateAdd - InnerRingCandidateRemove @@ -26,6 +27,7 @@ package smart_contract - SetConfig Other utility methods: + - Migrate - Version - Cheque */ @@ -127,6 +129,12 @@ func AlphabetList() []common.IRNode { return getNodes(ctx, alphabetKey) } +// AlphabetAddress returns 2\3n+1 multi signature address of alphabet nodes. +func AlphabetAddress() interop.Hash160 { + ctx := storage.GetReadOnlyContext() + return multiaddress(getNodes(ctx, alphabetKey)) +} + // InnerRingCandidates returns array of inner ring candidate node keys. func InnerRingCandidates() []common.IRNode { ctx := storage.GetReadOnlyContext() @@ -534,3 +542,17 @@ func rmNodeByKey(lst, add []common.IRNode, k []byte) ([]common.IRNode, []common. return newLst, add, flag } + +// multiaddress returns multi signature address from list of IRNode structures +// with m = 2/3n+1. +func multiaddress(n []common.IRNode) []byte { + threshold := len(n)*2/3 + 1 + + keys := []interop.PublicKey{} + for _, node := range n { + key := node.PublicKey + keys = append(keys, key) + } + + return contract.CreateMultisigAccount(threshold, keys) +} From c41abf12641c15d5cfc67e271bfd67f523805d40 Mon Sep 17 00:00:00 2001 From: Alex Vanin Date: Tue, 20 Apr 2021 17:30:43 +0300 Subject: [PATCH 02/19] [#74] neofs: Check alphabet multi signature and don't use ballots Ballots are inefficient to collect invocations of contract methods. Instead contract can check multi signature collected outside of the contract, e.g. with notary service. Signed-off-by: Alex Vanin --- neofs/neofs_contract.go | 99 +++++++++++------------------------------ 1 file changed, 26 insertions(+), 73 deletions(-) diff --git a/neofs/neofs_contract.go b/neofs/neofs_contract.go index b0369935..d1d01ea2 100644 --- a/neofs/neofs_contract.go +++ b/neofs/neofs_contract.go @@ -36,7 +36,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/interop" "github.com/nspcc-dev/neo-go/pkg/interop/contract" "github.com/nspcc-dev/neo-go/pkg/interop/iterator" - "github.com/nspcc-dev/neo-go/pkg/interop/native/crypto" "github.com/nspcc-dev/neo-go/pkg/interop/native/gas" "github.com/nspcc-dev/neo-go/pkg/interop/native/management" "github.com/nspcc-dev/neo-go/pkg/interop/native/std" @@ -98,7 +97,6 @@ func Init(owner interop.PublicKey, args []interop.PublicKey) bool { // initialize all storage slices common.SetSerialized(ctx, alphabetKey, irList) - common.InitVote(ctx) common.SetSerialized(ctx, candidatesKey, []common.IRNode{}) storage.Put(ctx, common.OwnerKey, owner) @@ -145,22 +143,9 @@ func InnerRingCandidates() []common.IRNode { func InnerRingCandidateRemove(key interop.PublicKey) bool { ctx := storage.GetContext() - if !runtime.CheckWitness(key) { - alphabet := getNodes(ctx, alphabetKey) - threshold := len(alphabet)*2/3 + 1 - - nodeKey := common.InnerRingInvoker(alphabet) - if len(nodeKey) == 0 { - panic("irCandidateRemove: invoked by non alphabet node") - } - - id := append(key, []byte("delete")...) - hashID := crypto.Sha256(id) - - n := common.Vote(ctx, hashID, nodeKey) - if n < threshold { - return true - } + multiaddr := AlphabetAddress() + if !runtime.CheckWitness(key) && !runtime.CheckWitness(multiaddr) { + panic("irCandidateRemove: this method must be invoked by candidate or alphabet") } nodes := []common.IRNode{} // it is explicit declaration of empty slice, not nil @@ -185,7 +170,7 @@ func InnerRingCandidateAdd(key interop.PublicKey) bool { ctx := storage.GetContext() if !runtime.CheckWitness(key) { - panic("irCandidateAdd: you should be the owner of the public key") + panic("irCandidateAdd: this method must be invoked by candidate") } c := common.IRNode{PublicKey: key} @@ -287,31 +272,21 @@ func Withdraw(user []byte, amount int) bool { // Cheque sends gas assets back to the user if they were successfully // locked in NeoFS balance contract. func Cheque(id []byte, user interop.Hash160, amount int, lockAcc []byte) bool { - ctx := storage.GetContext() - alphabet := getNodes(ctx, alphabetKey) - threshold := len(alphabet)*2/3 + 1 - - hashID := crypto.Sha256(id) - - key := common.InnerRingInvoker(alphabet) - if len(key) == 0 { - panic("cheque: invoked by non alphabet node") + multiaddr := AlphabetAddress() + if !runtime.CheckWitness(multiaddr) { + panic("cheque: this method must be invoked by alphabet") } - n := common.Vote(ctx, hashID, key) - if n >= threshold { - common.RemoveVotes(ctx, hashID) - from := runtime.GetExecutingScriptHash() - - transferred := gas.Transfer(from, user, amount, nil) - if !transferred { - panic("cheque: failed to transfer funds, aborting") - } + from := runtime.GetExecutingScriptHash() - runtime.Log("cheque: funds have been transferred") - runtime.Notify("Cheque", id, user, amount, lockAcc) + transferred := gas.Transfer(from, user, amount, nil) + if !transferred { + panic("cheque: failed to transfer funds, aborting") } + runtime.Log("cheque: funds have been transferred") + runtime.Notify("Cheque", id, user, amount, lockAcc) + return true } @@ -360,12 +335,9 @@ func AlphabetUpdate(chequeID []byte, args []interop.PublicKey) bool { panic("alphabetUpdate: bad arguments") } - alphabet := getNodes(ctx, alphabetKey) - threshold := len(alphabet)*2/3 + 1 - - key := common.InnerRingInvoker(alphabet) - if len(key) == 0 { - panic("innerRingUpdate: invoked by non alphabet node") + multiaddr := AlphabetAddress() + if !runtime.CheckWitness(multiaddr) { + panic("alphabetUpdate: this method must be invoked by alphabet") } newAlphabet := []common.IRNode{} @@ -381,17 +353,10 @@ func AlphabetUpdate(chequeID []byte, args []interop.PublicKey) bool { }) } - hashID := crypto.Sha256(chequeID) + common.SetSerialized(ctx, alphabetKey, newAlphabet) - n := common.Vote(ctx, hashID, key) - if n >= threshold { - common.RemoveVotes(ctx, hashID) - - common.SetSerialized(ctx, alphabetKey, newAlphabet) - - runtime.Notify("AlphabetUpdate", chequeID, newAlphabet) - runtime.Log("alphabetUpdate: alphabet list has been updated") - } + runtime.Notify("AlphabetUpdate", chequeID, newAlphabet) + runtime.Log("alphabetUpdate: alphabet list has been updated") return true } @@ -406,27 +371,15 @@ func Config(key []byte) interface{} { func SetConfig(id, key, val []byte) bool { ctx := storage.GetContext() - // check if it is alphabet invocation - alphabet := getNodes(ctx, alphabetKey) - threshold := len(alphabet)*2/3 + 1 - - nodeKey := common.InnerRingInvoker(alphabet) - if len(nodeKey) == 0 { - panic("setConfig: invoked by non alphabet node") + multiaddr := AlphabetAddress() + if !runtime.CheckWitness(multiaddr) { + panic("setConfig: this method must be invoked by alphabet") } - // vote for new configuration value - hashID := crypto.Sha256(id) - - n := common.Vote(ctx, hashID, nodeKey) - if n >= threshold { - common.RemoveVotes(ctx, hashID) + setConfig(ctx, key, val) - setConfig(ctx, key, val) - - runtime.Notify("SetConfig", id, key, val) - runtime.Log("setConfig: configuration has been updated") - } + runtime.Notify("SetConfig", id, key, val) + runtime.Log("setConfig: configuration has been updated") return true } From 8a6484edcbf6b7c1c680ed131fe53ba2a7e54e0b Mon Sep 17 00:00:00 2001 From: Alex Vanin Date: Tue, 20 Apr 2021 17:34:48 +0300 Subject: [PATCH 03/19] [#74] neofs: Remove unused code All ballots and voting methods are gone. Multi signature checks are used in all contracts. Default global config values are also removed. Configuration must be provided by initialization script. Signed-off-by: Alex Vanin --- common/ir.go | 13 ----- common/vote.go | 109 ---------------------------------------- neofs/neofs_contract.go | 29 +---------- 3 files changed, 2 insertions(+), 149 deletions(-) diff --git a/common/ir.go b/common/ir.go index 73002ab4..cdb9a140 100644 --- a/common/ir.go +++ b/common/ir.go @@ -6,25 +6,12 @@ import ( "github.com/nspcc-dev/neo-go/pkg/interop/native/ledger" "github.com/nspcc-dev/neo-go/pkg/interop/native/neo" "github.com/nspcc-dev/neo-go/pkg/interop/native/roles" - "github.com/nspcc-dev/neo-go/pkg/interop/runtime" ) type IRNode struct { PublicKey interop.PublicKey } -// InnerRingInvoker returns public key of inner ring node that invoked contract. -func InnerRingInvoker(ir []IRNode) interop.PublicKey { - for i := 0; i < len(ir); i++ { - node := ir[i] - if runtime.CheckWitness(node.PublicKey) { - return node.PublicKey - } - } - - return nil -} - // InnerRingNodes return list of inner ring nodes from state validator role // in side chain. func InnerRingNodes() []IRNode { diff --git a/common/vote.go b/common/vote.go index 309da2a2..4dad7947 100644 --- a/common/vote.go +++ b/common/vote.go @@ -1,120 +1,11 @@ package common import ( - "github.com/nspcc-dev/neo-go/pkg/interop" - "github.com/nspcc-dev/neo-go/pkg/interop/native/crypto" - "github.com/nspcc-dev/neo-go/pkg/interop/native/ledger" - "github.com/nspcc-dev/neo-go/pkg/interop/native/std" - "github.com/nspcc-dev/neo-go/pkg/interop/storage" "github.com/nspcc-dev/neo-go/pkg/interop/util" ) -type Ballot struct { - // ID of the voting decision. - ID []byte - - // Public keys of already voted inner ring nodes. - Voters []interop.PublicKey - - // Height of block with the last vote. - Height int -} - -const voteKey = "ballots" - -const blockDiff = 20 // change base on performance evaluation - -func InitVote(ctx storage.Context) { - SetSerialized(ctx, voteKey, []Ballot{}) -} - -// Vote adds ballot for the decision with specific 'id' and returns amount -// on unique voters for that decision. -func Vote(ctx storage.Context, id, from []byte) int { - var ( - newCandidates []Ballot - candidates = getBallots(ctx) - found = -1 - blockHeight = ledger.CurrentIndex() - ) - - for i := 0; i < len(candidates); i++ { - cnd := candidates[i] - - if blockHeight-cnd.Height > blockDiff { - continue - } - - if BytesEqual(cnd.ID, id) { - voters := cnd.Voters - - for j := range voters { - if BytesEqual(voters[j], from) { - return len(voters) - } - } - - voters = append(voters, from) - cnd = Ballot{ID: id, Voters: voters, Height: blockHeight} - found = len(voters) - } - - newCandidates = append(newCandidates, cnd) - } - - if found < 0 { - voters := []interop.PublicKey{from} - newCandidates = append(newCandidates, Ballot{ - ID: id, - Voters: voters, - Height: blockHeight}) - found = 1 - } - - SetSerialized(ctx, voteKey, newCandidates) - - return found -} - -// RemoveVotes clears ballots of the decision that has been accepted by -// inner ring nodes. -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) -} - -// getBallots returns deserialized slice of vote ballots. -func getBallots(ctx storage.Context) []Ballot { - data := storage.Get(ctx, voteKey) - if data != nil { - return std.Deserialize(data.([]byte)).([]Ballot) - } - - return []Ballot{} -} - // BytesEqual compares two slice of bytes by wrapping them into strings, // which is necessary with new util.Equal interop behaviour, see neo-go#1176. func BytesEqual(a []byte, b []byte) bool { return util.Equals(string(a), string(b)) } - -func InvokeID(args []interface{}, prefix []byte) []byte { - for i := range args { - arg := args[i].([]byte) - prefix = append(prefix, arg...) - } - - return crypto.Sha256(prefix) -} diff --git a/neofs/neofs_contract.go b/neofs/neofs_contract.go index d1d01ea2..9f9bb08a 100644 --- a/neofs/neofs_contract.go +++ b/neofs/neofs_contract.go @@ -52,14 +52,12 @@ type ( ) const ( - defaultCandidateFee = 100 * 1_0000_0000 // 100 Fixed8 Gas candidateFeeConfigKey = "InnerRingCandidateFee" version = 3 - alphabetKey = "alphabet" - candidatesKey = "candidates" - cashedChequesKey = "cheques" + alphabetKey = "alphabet" + candidatesKey = "candidates" publicKeySize = 33 @@ -416,8 +414,6 @@ func InitConfig(args [][]byte) bool { panic("initConfig: bad arguments") } - setConfig(ctx, candidateFeeConfigKey, defaultCandidateFee) - for i := 0; i < ln/2; i++ { key := args[i*2] val := args[i*2+1] @@ -475,27 +471,6 @@ func addNode(lst []common.IRNode, n common.IRNode) ([]common.IRNode, bool) { return lst, true } -// rmNodeByKey returns slice of nodes without node with key 'k', -// slices of nodes 'add' with node with key 'k' and bool flag, -// that set to false if node with a key 'k' does not exists in the slice 'lst'. -func rmNodeByKey(lst, add []common.IRNode, k []byte) ([]common.IRNode, []common.IRNode, bool) { - var ( - flag bool - newLst = []common.IRNode{} // it is explicit declaration of empty slice, not nil - ) - - for i := 0; i < len(lst); i++ { - if common.BytesEqual(k, lst[i].PublicKey) { - add = append(add, lst[i]) - flag = true - } else { - newLst = append(newLst, lst[i]) - } - } - - return newLst, add, flag -} - // multiaddress returns multi signature address from list of IRNode structures // with m = 2/3n+1. func multiaddress(n []common.IRNode) []byte { From b79a541b324b003949251f604db8a0fe6cb22d65 Mon Sep 17 00:00:00 2001 From: Alex Vanin Date: Tue, 20 Apr 2021 17:40:18 +0300 Subject: [PATCH 04/19] [#74] processing: Add processing contract Processing contract deployed in main chain and processes multi signature invocations of NeoFS contract by verifying multi signature and paying for it. Signed-off-by: Alex Vanin --- Makefile | 2 +- processing/config.yml | 2 + processing/processing_contract.go | 69 +++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 processing/config.yml create mode 100644 processing/processing_contract.go diff --git a/Makefile b/Makefile index e4fe89ef..4e882076 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ sidechain: alphabet morph alphabet_sc = alphabet morph_sc = audit balance container neofsid netmap proxy reputation -mainnet_sc = neofs +mainnet_sc = neofs processing define sc_template $(2)$(1)/$(1)_contract.nef: $(2)$(1)/$(1)_contract.go diff --git a/processing/config.yml b/processing/config.yml new file mode 100644 index 00000000..87bb0acb --- /dev/null +++ b/processing/config.yml @@ -0,0 +1,2 @@ +name: "NeoFS Multi Signature Processing" +safemethods: ["verify", "version"] diff --git a/processing/processing_contract.go b/processing/processing_contract.go new file mode 100644 index 00000000..fe52eb4b --- /dev/null +++ b/processing/processing_contract.go @@ -0,0 +1,69 @@ +package processingcontract + +import ( + "github.com/nspcc-dev/neo-go/pkg/interop" + "github.com/nspcc-dev/neo-go/pkg/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/interop/native/gas" + "github.com/nspcc-dev/neo-go/pkg/interop/native/management" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" + "github.com/nspcc-dev/neofs-contract/common" +) + +const ( + version = 1 + + neofsContractKey = "neofsScriptHash" + + multiaddrMethod = "alphabetAddress" +) + +func OnNEP17Payment(from interop.Hash160, amount int, data interface{}) { + caller := runtime.GetCallingScriptHash() + if !common.BytesEqual(caller, []byte(gas.Hash)) { + panic("onNEP17Payment: processing contract accepts GAS only") + } +} + +func Init(owner, addrNeoFS interop.Hash160) { + ctx := storage.GetContext() + + if !common.HasUpdateAccess(ctx) { + panic("only owner can reinitialize contract") + } + + if len(addrNeoFS) != 20 { + panic("init: incorrect length of contract script hash") + } + + storage.Put(ctx, common.OwnerKey, owner) + storage.Put(ctx, neofsContractKey, addrNeoFS) + + runtime.Log("processing contract initialized") +} + +func Migrate(script []byte, manifest []byte) bool { + ctx := storage.GetReadOnlyContext() + + if !common.HasUpdateAccess(ctx) { + runtime.Log("only owner can update contract") + return false + } + + management.Update(script, manifest) + runtime.Log("processing contract updated") + + return true +} + +func Verify() bool { + ctx := storage.GetContext() + neofsContractAddr := storage.Get(ctx, neofsContractKey).(interop.Hash160) + multiaddr := contract.Call(neofsContractAddr, multiaddrMethod, contract.ReadOnly).(interop.Hash160) + + return runtime.CheckWitness(multiaddr) +} + +func Version() int { + return version +} From b8e015fb55e5215fc35bbe89c59f1e84b1f1a0d6 Mon Sep 17 00:00:00 2001 From: Alex Vanin Date: Tue, 20 Apr 2021 17:44:36 +0300 Subject: [PATCH 05/19] [#74] neofs: Pay withdraw fee to processing contract Processing contracts pays for cheque that transfer assets back to the user, so user should transfer some fee to this contract. Withdraw fee defined in NeoFS global configuration. Signed-off-by: Alex Vanin --- neofs/neofs_contract.go | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/neofs/neofs_contract.go b/neofs/neofs_contract.go index 9f9bb08a..0bb95459 100644 --- a/neofs/neofs_contract.go +++ b/neofs/neofs_contract.go @@ -53,12 +53,15 @@ type ( const ( candidateFeeConfigKey = "InnerRingCandidateFee" + withdrawFeeConfigKey = "WithdrawFee" version = 3 alphabetKey = "alphabet" candidatesKey = "candidates" + processingContractKey = "processingScriptHash" + publicKeySize = 33 maxBalanceAmount = 9000 // Max integer of Fixed12 in JSON bound (2**53-1) @@ -72,7 +75,7 @@ var ( ) // Init set up initial alphabet node keys. -func Init(owner interop.PublicKey, args []interop.PublicKey) bool { +func Init(owner, addrProc interop.Hash160, args []interop.PublicKey) bool { ctx := storage.GetContext() if !common.HasUpdateAccess(ctx) { @@ -85,6 +88,10 @@ func Init(owner interop.PublicKey, args []interop.PublicKey) bool { panic("neofs: at least one alphabet key must be provided") } + if len(addrProc) != 20 { + panic("neofs: incorrect length of contract script hash") + } + for i := 0; i < len(args); i++ { pub := args[i] if len(pub) != publicKeySize { @@ -98,6 +105,7 @@ func Init(owner interop.PublicKey, args []interop.PublicKey) bool { common.SetSerialized(ctx, candidatesKey, []common.IRNode{}) storage.Put(ctx, common.OwnerKey, owner) + storage.Put(ctx, processingContractKey, addrProc) runtime.Log("neofs: contract initialized") @@ -246,7 +254,7 @@ func Deposit(from interop.Hash160, amount int, rcv interop.Hash160) bool { } // Withdraw initialize gas asset withdraw from NeoFS balance. -func Withdraw(user []byte, amount int) bool { +func Withdraw(user interop.Hash160, amount int) bool { if !runtime.CheckWitness(user) { panic("withdraw: you should be the owner of the wallet") } @@ -259,9 +267,20 @@ func Withdraw(user []byte, amount int) bool { panic("withdraw: out of max amount limit") } - amount = amount * 100000000 + // transfer fee to proxy contract to pay cheque invocation + ctx := storage.GetContext() + fee := getConfig(ctx, withdrawFeeConfigKey).(int) + processingAddr := storage.Get(ctx, processingContractKey).(interop.Hash160) + transferred := gas.Transfer(user, processingAddr, fee, []byte{}) + if !transferred { + panic("withdraw: failed to transfer withdraw fee, aborting") + } + + // notify alphabet nodes + amount = amount * 100000000 tx := runtime.GetScriptContainer() + runtime.Notify("Withdraw", user, amount, tx.Hash) return true From 898093f6760feda924040700797075f44366d62e Mon Sep 17 00:00:00 2001 From: Alex Vanin Date: Tue, 20 Apr 2021 17:45:22 +0300 Subject: [PATCH 06/19] [#74] proxy: Fix typo Signed-off-by: Alex Vanin --- proxy/proxy_contract.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/proxy_contract.go b/proxy/proxy_contract.go index ef6036e8..60ca52c0 100644 --- a/proxy/proxy_contract.go +++ b/proxy/proxy_contract.go @@ -19,7 +19,7 @@ const ( func OnNEP17Payment(from interop.Hash160, amount int, data interface{}) { caller := runtime.GetCallingScriptHash() if !common.BytesEqual(caller, []byte(gas.Hash)) { - panic("onNEP17Payment: alphabet contract accepts GAS only") + panic("onNEP17Payment: proxy contract accepts GAS only") } } From a53b817c92b71247074b1656fe973c535512ea77 Mon Sep 17 00:00:00 2001 From: Alex Vanin Date: Wed, 21 Apr 2021 09:44:44 +0300 Subject: [PATCH 07/19] [#74] Update readme file Signed-off-by: Alex Vanin --- README.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index a42aa2e6..724db3d4 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,14 @@ NeoFS-Contract contains all NeoFS related contracts written for [neo-go](https://github.com/nspcc-dev/neo-go) compiler. These contracts -are deployed both in mainnet and sidechain. +are deployed both in main chain and side chain. -Mainnet contract: +Main chain contracts: - neofs +- processing -Sidechain contracts: +Side chain contracts: - alphabet - audit @@ -34,7 +35,7 @@ Sidechain contracts: To compile smart contracts you need: -- [neo-go](https://github.com/nspcc-dev/neo-go) >= 0.94.0 +- [neo-go](https://github.com/nspcc-dev/neo-go) >= 0.94.1 ## Compilation @@ -44,15 +45,16 @@ corresponding directories. ``` $ make all -neo-go contract compile -i alphabet/alphabet_contract.go -c alphabet/config.yml -m alphabet/config.json -neo-go contract compile -i audit/audit_contract.go -c audit/config.yml -m audit/config.json -neo-go contract compile -i balance/balance_contract.go -c balance/config.yml -m balance/config.json -neo-go contract compile -i container/container_contract.go -c container/config.yml -m container/config.json -neo-go contract compile -i neofsid/neofsid_contract.go -c neofsid/config.yml -m neofsid/config.json -neo-go contract compile -i netmap/netmap_contract.go -c netmap/config.yml -m netmap/config.json -neo-go contract compile -i proxy/proxy_contract.go -c proxy/config.yml -m proxy/config.json +neo-go contract compile -i alphabet/alphabet_contract.go -c alphabet/config.yml -m alphabet/config.json +neo-go contract compile -i audit/audit_contract.go -c audit/config.yml -m audit/config.json +neo-go contract compile -i balance/balance_contract.go -c balance/config.yml -m balance/config.json +neo-go contract compile -i container/container_contract.go -c container/config.yml -m container/config.json +neo-go contract compile -i neofsid/neofsid_contract.go -c neofsid/config.yml -m neofsid/config.json +neo-go contract compile -i netmap/netmap_contract.go -c netmap/config.yml -m netmap/config.json +neo-go contract compile -i proxy/proxy_contract.go -c proxy/config.yml -m proxy/config.json neo-go contract compile -i reputation/reputation_contract.go -c reputation/config.yml -m reputation/config.json neo-go contract compile -i neofs/neofs_contract.go -c neofs/config.yml -m neofs/config.json +neo-go contract compile -i processing/processing_contract.go -c processing/config.yml -m processing/config.json ``` You can specify path to the `neo-go` binary with `NEOGO` environment variable: From 4ea4f8b35a2aba65699d7cd753cee207f56ccfa7 Mon Sep 17 00:00:00 2001 From: Alex Vanin Date: Thu, 29 Apr 2021 16:02:33 +0300 Subject: [PATCH 08/19] [#74] Return ballot collection functions Signed-off-by: Alex Vanin --- common/ir.go | 23 ++++++++++ common/vote.go | 111 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) diff --git a/common/ir.go b/common/ir.go index cdb9a140..86f50b58 100644 --- a/common/ir.go +++ b/common/ir.go @@ -6,12 +6,28 @@ import ( "github.com/nspcc-dev/neo-go/pkg/interop/native/ledger" "github.com/nspcc-dev/neo-go/pkg/interop/native/neo" "github.com/nspcc-dev/neo-go/pkg/interop/native/roles" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" ) type IRNode struct { PublicKey interop.PublicKey } +const irListMethod = "innerRingList" + +// InnerRingInvoker returns public key of inner ring node that invoked contract. +// Work around for environments without notary support. +func InnerRingInvoker(ir []IRNode) interop.PublicKey { + for i := 0; i < len(ir); i++ { + node := ir[i] + if runtime.CheckWitness(node.PublicKey) { + return node.PublicKey + } + } + + return nil +} + // InnerRingNodes return list of inner ring nodes from state validator role // in side chain. func InnerRingNodes() []IRNode { @@ -20,6 +36,13 @@ func InnerRingNodes() []IRNode { return keysToNodes(list) } +// InnerRingNodesFromNetmap gets list of inner ring through +// calling "innerRingList" method of smart contract. +// Work around for environments without notary support. +func InnerRingNodesFromNetmap(sc interop.Hash160) []IRNode { + return contract.Call(sc, irListMethod, contract.ReadOnly).([]IRNode) +} + // AlphabetNodes return list of alphabet nodes from committee in side chain. func AlphabetNodes() []IRNode { list := neo.GetCommittee() diff --git a/common/vote.go b/common/vote.go index 4dad7947..a29ea4d3 100644 --- a/common/vote.go +++ b/common/vote.go @@ -1,11 +1,122 @@ package common import ( + "github.com/nspcc-dev/neo-go/pkg/interop" + "github.com/nspcc-dev/neo-go/pkg/interop/native/crypto" + "github.com/nspcc-dev/neo-go/pkg/interop/native/ledger" + "github.com/nspcc-dev/neo-go/pkg/interop/native/std" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" "github.com/nspcc-dev/neo-go/pkg/interop/util" ) +type Ballot struct { + // ID of the voting decision. + ID []byte + + // Public keys of already voted inner ring nodes. + Voters []interop.PublicKey + + // Height of block with the last vote. + Height int +} + +const voteKey = "ballots" + +const blockDiff = 20 // change base on performance evaluation + +func InitVote(ctx storage.Context) { + SetSerialized(ctx, voteKey, []Ballot{}) +} + +// Vote adds ballot for the decision with specific 'id' and returns amount +// on unique voters for that decision. +func Vote(ctx storage.Context, id, from []byte) int { + var ( + newCandidates []Ballot + candidates = getBallots(ctx) + found = -1 + blockHeight = ledger.CurrentIndex() + ) + + for i := 0; i < len(candidates); i++ { + cnd := candidates[i] + + if blockHeight-cnd.Height > blockDiff { + continue + } + + if BytesEqual(cnd.ID, id) { + voters := cnd.Voters + + for j := range voters { + if BytesEqual(voters[j], from) { + return len(voters) + } + } + + voters = append(voters, from) + cnd = Ballot{ID: id, Voters: voters, Height: blockHeight} + found = len(voters) + } + + newCandidates = append(newCandidates, cnd) + } + + if found < 0 { + voters := []interop.PublicKey{from} + newCandidates = append(newCandidates, Ballot{ + ID: id, + Voters: voters, + Height: blockHeight}) + found = 1 + } + + SetSerialized(ctx, voteKey, newCandidates) + + return found +} + +// RemoveVotes clears ballots of the decision that has been accepted by +// inner ring nodes. +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) +} + +// getBallots returns deserialized slice of vote ballots. +func getBallots(ctx storage.Context) []Ballot { + data := storage.Get(ctx, voteKey) + if data != nil { + return std.Deserialize(data.([]byte)).([]Ballot) + } + + return []Ballot{} +} + // BytesEqual compares two slice of bytes by wrapping them into strings, // which is necessary with new util.Equal interop behaviour, see neo-go#1176. func BytesEqual(a []byte, b []byte) bool { return util.Equals(string(a), string(b)) } + +// InvokeID returns hashed value of prefix and args concatenation. Used to +// identify different ballots. +func InvokeID(args []interface{}, prefix []byte) []byte { + for i := range args { + arg := args[i].([]byte) + prefix = append(prefix, arg...) + } + + return crypto.Sha256(prefix) +} From e752a4267e4aed566e125476ab73b9dac19ef597 Mon Sep 17 00:00:00 2001 From: Alex Vanin Date: Tue, 27 Apr 2021 13:48:40 +0300 Subject: [PATCH 09/19] [#74] Add notary disabled option to contracts Signed-off-by: Alex Vanin --- alphabet/alphabet_contract.go | 10 +++++++++- audit/audit_contract.go | 7 ++++++- balance/balance_contract.go | 9 ++++++++- container/container_contract.go | 12 ++++++++++-- neofs/neofs_contract.go | 13 ++++++++++--- neofsid/neofsid_contract.go | 9 ++++++++- netmap/netmap_contract.go | 16 ++++++++++++---- reputation/reputation_contract.go | 10 +++++++++- 8 files changed, 72 insertions(+), 14 deletions(-) diff --git a/alphabet/alphabet_contract.go b/alphabet/alphabet_contract.go index d6b56713..47244093 100644 --- a/alphabet/alphabet_contract.go +++ b/alphabet/alphabet_contract.go @@ -19,6 +19,8 @@ const ( totalKey = "threshold" nameKey = "name" + notaryDisabledKey = "notary" + version = 1 ) @@ -30,7 +32,7 @@ func OnNEP17Payment(from interop.Hash160, amount int, data interface{}) { } } -func Init(owner interop.Hash160, addrNetmap, addrProxy interop.Hash160, name string, index, total int) { +func Init(notaryDisabled bool, owner interop.Hash160, addrNetmap, addrProxy interop.Hash160, name string, index, total int) { ctx := storage.GetContext() if !common.HasUpdateAccess(ctx) { @@ -48,6 +50,12 @@ func Init(owner interop.Hash160, addrNetmap, addrProxy interop.Hash160, name str storage.Put(ctx, indexKey, index) storage.Put(ctx, totalKey, total) + // initialize the way to collect signatures + storage.Put(ctx, notaryDisabledKey, notaryDisabled) + if notaryDisabled { + common.InitVote(ctx) + } + runtime.Log(name + " contract initialized") } diff --git a/audit/audit_contract.go b/audit/audit_contract.go index 400c6925..47b7794e 100644 --- a/audit/audit_contract.go +++ b/audit/audit_contract.go @@ -38,9 +38,11 @@ const ( version = 1 netmapContractKey = "netmapScriptHash" + + notaryDisabledKey = "notary" ) -func Init(owner interop.Hash160, addrNetmap interop.Hash160) { +func Init(notaryDisabled bool, owner interop.Hash160, addrNetmap interop.Hash160) { ctx := storage.GetContext() if !common.HasUpdateAccess(ctx) { @@ -54,6 +56,9 @@ func Init(owner interop.Hash160, addrNetmap interop.Hash160) { storage.Put(ctx, common.OwnerKey, owner) storage.Put(ctx, netmapContractKey, addrNetmap) + // initialize the way to collect signatures + storage.Put(ctx, notaryDisabledKey, notaryDisabled) + runtime.Log("audit contract initialized") } diff --git a/balance/balance_contract.go b/balance/balance_contract.go index e91f9bfc..0a59f6d9 100644 --- a/balance/balance_contract.go +++ b/balance/balance_contract.go @@ -40,6 +40,7 @@ const ( netmapContractKey = "netmapScriptHash" containerContractKey = "containerScriptHash" + notaryDisabledKey = "notary" ) var ( @@ -62,7 +63,7 @@ func init() { token = CreateToken() } -func Init(owner, addrNetmap, addrContainer interop.Hash160) { +func Init(notaryDisabled bool, owner, addrNetmap, addrContainer interop.Hash160) { ctx := storage.GetContext() if !common.HasUpdateAccess(ctx) { @@ -77,6 +78,12 @@ func Init(owner, addrNetmap, addrContainer interop.Hash160) { storage.Put(ctx, netmapContractKey, addrNetmap) storage.Put(ctx, containerContractKey, addrContainer) + // initialize the way to collect signatures + storage.Put(ctx, notaryDisabledKey, notaryDisabled) + if notaryDisabled { + common.InitVote(ctx) + } + runtime.Log("balance contract initialized") } diff --git a/container/container_contract.go b/container/container_contract.go index 4271d956..e15a02ed 100644 --- a/container/container_contract.go +++ b/container/container_contract.go @@ -41,7 +41,9 @@ const ( neofsIDContractKey = "identityScriptHash" balanceContractKey = "balanceScriptHash" netmapContractKey = "netmapScriptHash" - containerFeeKey = "ContainerFee" + notaryDisabledKey = "notary" + + containerFeeKey = "ContainerFee" containerIDSize = 32 // SHA256 size @@ -53,7 +55,7 @@ var ( eACLPrefix = []byte("eACL") ) -func Init(owner, addrNetmap, addrBalance, addrID interop.Hash160) { +func Init(notaryDisabled bool, owner, addrNetmap, addrBalance, addrID interop.Hash160) { ctx := storage.GetContext() if !common.HasUpdateAccess(ctx) { @@ -69,6 +71,12 @@ func Init(owner, addrNetmap, addrBalance, addrID interop.Hash160) { storage.Put(ctx, balanceContractKey, addrBalance) storage.Put(ctx, neofsIDContractKey, addrID) + // initialize the way to collect signatures + storage.Put(ctx, notaryDisabledKey, notaryDisabled) + if notaryDisabled { + common.InitVote(ctx) + } + runtime.Log("container contract initialized") } diff --git a/neofs/neofs_contract.go b/neofs/neofs_contract.go index 0bb95459..e993419b 100644 --- a/neofs/neofs_contract.go +++ b/neofs/neofs_contract.go @@ -57,8 +57,9 @@ const ( version = 3 - alphabetKey = "alphabet" - candidatesKey = "candidates" + alphabetKey = "alphabet" + candidatesKey = "candidates" + notaryDisabledKey = "notary" processingContractKey = "processingScriptHash" @@ -75,7 +76,7 @@ var ( ) // Init set up initial alphabet node keys. -func Init(owner, addrProc interop.Hash160, args []interop.PublicKey) bool { +func Init(notaryDisabled bool, owner, addrProc interop.Hash160, args []interop.PublicKey) bool { ctx := storage.GetContext() if !common.HasUpdateAccess(ctx) { @@ -107,6 +108,12 @@ func Init(owner, addrProc interop.Hash160, args []interop.PublicKey) bool { storage.Put(ctx, common.OwnerKey, owner) storage.Put(ctx, processingContractKey, addrProc) + // initialize the way to collect signatures + storage.Put(ctx, notaryDisabledKey, notaryDisabled) + if notaryDisabled { + common.InitVote(ctx) + } + runtime.Log("neofs: contract initialized") return true diff --git a/neofsid/neofsid_contract.go b/neofsid/neofsid_contract.go index da9f43d4..929c85d5 100644 --- a/neofsid/neofsid_contract.go +++ b/neofsid/neofsid_contract.go @@ -20,9 +20,10 @@ const ( netmapContractKey = "netmapScriptHash" containerContractKey = "containerScriptHash" + notaryDisabledKey = "notary" ) -func Init(owner, addrNetmap, addrContainer interop.Hash160) { +func Init(notaryDisabled bool, owner, addrNetmap, addrContainer interop.Hash160) { ctx := storage.GetContext() if !common.HasUpdateAccess(ctx) { @@ -37,6 +38,12 @@ func Init(owner, addrNetmap, addrContainer interop.Hash160) { storage.Put(ctx, netmapContractKey, addrNetmap) storage.Put(ctx, containerContractKey, addrContainer) + // initialize the way to collect signatures + storage.Put(ctx, notaryDisabledKey, notaryDisabled) + if notaryDisabled { + common.InitVote(ctx) + } + runtime.Log("neofsid contract initialized") } diff --git a/netmap/netmap_contract.go b/netmap/netmap_contract.go index 085d8b91..f7c73a51 100644 --- a/netmap/netmap_contract.go +++ b/netmap/netmap_contract.go @@ -32,8 +32,9 @@ type ( const ( version = 1 - netmapKey = "netmap" - configuredKey = "initconfig" + netmapKey = "netmap" + configuredKey = "initconfig" + notaryDisabledKey = "notary" snapshot0Key = "snapshotCurrent" snapshot1Key = "snapshotPrevious" @@ -41,7 +42,8 @@ const ( containerContractKey = "containerScriptHash" balanceContractKey = "balanceScriptHash" - cleanupEpochMethod = "newEpoch" + + cleanupEpochMethod = "newEpoch" ) const ( @@ -56,7 +58,7 @@ var ( // Init function sets up initial list of inner ring public keys and should // be invoked once at neofs infrastructure setup. -func Init(owner, addrBalance, addrContainer interop.Hash160) { +func Init(notaryDisabled bool, owner, addrBalance, addrContainer interop.Hash160) { ctx := storage.GetContext() if !common.HasUpdateAccess(ctx) { @@ -80,6 +82,12 @@ func Init(owner, addrBalance, addrContainer interop.Hash160) { storage.Put(ctx, balanceContractKey, addrBalance) storage.Put(ctx, containerContractKey, addrContainer) + // initialize the way to collect signatures + storage.Put(ctx, notaryDisabledKey, notaryDisabled) + if notaryDisabled { + common.InitVote(ctx) + } + runtime.Log("netmap contract initialized") } diff --git a/reputation/reputation_contract.go b/reputation/reputation_contract.go index e4640fab..2069a0da 100644 --- a/reputation/reputation_contract.go +++ b/reputation/reputation_contract.go @@ -11,10 +11,12 @@ import ( ) const ( + notaryDisabledKey = "notary" + version = 1 ) -func Init(owner interop.Hash160) { +func Init(notaryDisabled bool, owner interop.Hash160) { ctx := storage.GetContext() if !common.HasUpdateAccess(ctx) { @@ -23,6 +25,12 @@ func Init(owner interop.Hash160) { storage.Put(ctx, common.OwnerKey, owner) + // initialize the way to collect signatures + storage.Put(ctx, notaryDisabledKey, notaryDisabled) + if notaryDisabled { + common.InitVote(ctx) + } + runtime.Log("reputation contract initialized") } From 9c73522903f7d07bc8dbafe71d1f18df8643415e Mon Sep 17 00:00:00 2001 From: Alex Vanin Date: Tue, 27 Apr 2021 13:56:02 +0300 Subject: [PATCH 10/19] [#74] netmap: Store inner ring node list when notary disabled Signed-off-by: Alex Vanin --- netmap/config.yml | 2 +- netmap/netmap_contract.go | 46 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/netmap/config.yml b/netmap/config.yml index 61104f9f..bf487681 100644 --- a/netmap/config.yml +++ b/netmap/config.yml @@ -1,5 +1,5 @@ name: "NeoFS Netmap" -safemethods: ["epoch", "netmap", "snapshot", "snapshotByEpoch", "config", "listConfig", "version"] +safemethods: ["innerRingList", "epoch", "netmap", "snapshot", "snapshotByEpoch", "config", "listConfig", "version"] events: - name: AddPeer parameters: diff --git a/netmap/netmap_contract.go b/netmap/netmap_contract.go index f7c73a51..0ab9d348 100644 --- a/netmap/netmap_contract.go +++ b/netmap/netmap_contract.go @@ -35,6 +35,7 @@ const ( netmapKey = "netmap" configuredKey = "initconfig" notaryDisabledKey = "notary" + innerRingKey = "innerring" snapshot0Key = "snapshotCurrent" snapshot1Key = "snapshotPrevious" @@ -58,7 +59,7 @@ var ( // Init function sets up initial list of inner ring public keys and should // be invoked once at neofs infrastructure setup. -func Init(notaryDisabled bool, owner, addrBalance, addrContainer interop.Hash160) { +func Init(notaryDisabled bool, owner, addrBalance, addrContainer interop.Hash160, keys []interop.PublicKey) { ctx := storage.GetContext() if !common.HasUpdateAccess(ctx) { @@ -85,6 +86,14 @@ func Init(notaryDisabled bool, owner, addrBalance, addrContainer interop.Hash160 // initialize the way to collect signatures storage.Put(ctx, notaryDisabledKey, notaryDisabled) if notaryDisabled { + var irList []common.IRNode + + for i := 0; i < len(keys); i++ { + key := keys[i] + irList = append(irList, common.IRNode{PublicKey: key}) + } + + common.SetSerialized(ctx, innerRingKey, irList) common.InitVote(ctx) } @@ -105,6 +114,32 @@ func Migrate(script []byte, manifest []byte) bool { return true } +func InnerRingList() []common.IRNode { + ctx := storage.GetReadOnlyContext() + return getIRNodes(ctx) +} + +func UpdateInnerRing(keys []interop.PublicKey) bool { + ctx := storage.GetContext() + + multiaddr := common.AlphabetAddress() + if !runtime.CheckWitness(multiaddr) { + panic("updateInnerRing: this method must be invoked by alpahbet nodes") + } + + var irList []common.IRNode + + for i := 0; i < len(keys); i++ { + key := keys[i] + irList = append(irList, common.IRNode{PublicKey: key}) + } + + runtime.Log("updateInnerRing: inner ring list updated") + common.SetSerialized(ctx, innerRingKey, irList) + + return true +} + func AddPeer(nodeInfo []byte) bool { ctx := storage.GetContext() @@ -399,3 +434,12 @@ func cleanup(ctx storage.Context, epoch int) { containerContractAddr := storage.Get(ctx, containerContractKey).(interop.Hash160) contract.Call(containerContractAddr, cleanupEpochMethod, contract.All, epoch) } + +func getIRNodes(ctx storage.Context) []common.IRNode { + data := storage.Get(ctx, innerRingKey) + if data != nil { + return std.Deserialize(data.([]byte)).([]common.IRNode) + } + + return []common.IRNode{} +} From 4a7b04cc51a2d1ce5584f5a77eb12e6e0901ca91 Mon Sep 17 00:00:00 2001 From: Alex Vanin Date: Thu, 29 Apr 2021 16:06:34 +0300 Subject: [PATCH 11/19] [#74] alphabet: Support notary disabled work flow Signed-off-by: Alex Vanin --- alphabet/alphabet_contract.go | 61 ++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 5 deletions(-) diff --git a/alphabet/alphabet_contract.go b/alphabet/alphabet_contract.go index 47244093..d796fdd0 100644 --- a/alphabet/alphabet_contract.go +++ b/alphabet/alphabet_contract.go @@ -3,6 +3,7 @@ package alphabetcontract import ( "github.com/nspcc-dev/neo-go/pkg/interop" "github.com/nspcc-dev/neo-go/pkg/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/interop/native/crypto" "github.com/nspcc-dev/neo-go/pkg/interop/native/gas" "github.com/nspcc-dev/neo-go/pkg/interop/native/management" "github.com/nspcc-dev/neo-go/pkg/interop/native/neo" @@ -112,6 +113,7 @@ func checkPermission(ir []common.IRNode) bool { func Emit() bool { ctx := storage.GetReadOnlyContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) alphabet := common.AlphabetNodes() if !checkPermission(alphabet) { @@ -134,7 +136,15 @@ func Emit() bool { gas.Transfer(contractHash, proxyAddr, proxyGas, nil) runtime.Log("utility token has been emitted to proxy contract") - innerRing := common.InnerRingNodes() + var innerRing []common.IRNode + + if notaryDisabled { + netmapContract := storage.Get(ctx, netmapKey).(interop.Hash160) + innerRing = common.InnerRingNodesFromNetmap(netmapContract) + } else { + innerRing = common.InnerRingNodes() + } + gasPerNode := gasBalance / 2 * 7 / 8 / len(innerRing) if gasPerNode != 0 { @@ -150,13 +160,27 @@ func Emit() bool { } func Vote(epoch int, candidates []interop.PublicKey) { - ctx := storage.GetReadOnlyContext() + ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) index := index(ctx) name := name(ctx) - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { - panic("invalid invoker") + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + ) + + if notaryDisabled { + alphabet = common.AlphabetNodes() + nodeKey = common.InnerRingInvoker(alphabet) + if len(nodeKey) == 0 { + panic("invalid invoker") + } + } else { + multiaddr := common.AlphabetAddress() + if !runtime.CheckWitness(multiaddr) { + panic("invalid invoker") + } } curEpoch := currentEpoch(ctx) @@ -167,6 +191,18 @@ func Vote(epoch int, candidates []interop.PublicKey) { candidate := candidates[index%len(candidates)] address := runtime.GetExecutingScriptHash() + if notaryDisabled { + threshold := len(alphabet)*2/3 + 1 + id := voteID(epoch, candidates) + + n := common.Vote(ctx, id, nodeKey) + if n < threshold { + return + } + + common.RemoveVotes(ctx, id) + } + ok := neo.Vote(address, candidate) if ok { runtime.Log(name + ": successfully voted for validator") @@ -177,6 +213,21 @@ func Vote(epoch int, candidates []interop.PublicKey) { return } +func voteID(epoch interface{}, args []interop.PublicKey) []byte { + var ( + result []byte + epochBytes = epoch.([]byte) + ) + + result = append(result, epochBytes...) + + for i := range args { + result = append(result, args[i]...) + } + + return crypto.Sha256(result) +} + func Name() string { ctx := storage.GetReadOnlyContext() return name(ctx) From ef4f610a49261187bfb0e5098fb33b11b626f0e7 Mon Sep 17 00:00:00 2001 From: Alex Vanin Date: Thu, 29 Apr 2021 16:07:08 +0300 Subject: [PATCH 12/19] [#74] audit: Support notary disabled work flow Signed-off-by: Alex Vanin --- audit/audit_contract.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/audit/audit_contract.go b/audit/audit_contract.go index 47b7794e..276a6a97 100644 --- a/audit/audit_contract.go +++ b/audit/audit_contract.go @@ -78,7 +78,16 @@ func Migrate(script []byte, manifest []byte) bool { func Put(rawAuditResult []byte) bool { ctx := storage.GetContext() - innerRing := common.InnerRingNodes() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) + + var innerRing []common.IRNode + + if notaryDisabled { + netmapContract := storage.Get(ctx, netmapContractKey).(interop.Hash160) + innerRing = common.InnerRingNodesFromNetmap(netmapContract) + } else { + innerRing = common.InnerRingNodes() + } hdr := newAuditHeader(rawAuditResult) presented := false @@ -153,6 +162,7 @@ func list(it iterator.Iterator) [][]byte { ignore := [][]byte{ []byte(netmapContractKey), []byte(common.OwnerKey), + []byte(notaryDisabledKey), } loop: From fbf427129012eadf6a3d27964cdc143c0defa4c7 Mon Sep 17 00:00:00 2001 From: Alex Vanin Date: Thu, 29 Apr 2021 16:12:41 +0300 Subject: [PATCH 13/19] [#74] balance: Support notary disabled work flow Signed-off-by: Alex Vanin --- balance/balance_contract.go | 158 ++++++++++++++++++++++++++++++++---- common/vote.go | 25 ++++++ 2 files changed, 166 insertions(+), 17 deletions(-) diff --git a/balance/balance_contract.go b/balance/balance_contract.go index 0a59f6d9..9d7e7e08 100644 --- a/balance/balance_contract.go +++ b/balance/balance_contract.go @@ -126,10 +126,43 @@ func Transfer(from, to interop.Hash160, amount int, data interface{}) bool { func TransferX(from, to interop.Hash160, amount int, details []byte) bool { ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { - panic("transferX: this method must be invoked from inner ring") + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + inderectCall bool + ) + + if notaryDisabled { + alphabet = common.AlphabetNodes() + nodeKey = common.InnerRingInvoker(alphabet) + if len(nodeKey) == 0 { + panic("transferX: this method must be invoked from inner ring") + } + + inderectCall = common.FromKnownContract( + ctx, + runtime.GetCallingScriptHash(), + containerContractKey, + ) + } else { + multiaddr := common.AlphabetAddress() + if !runtime.CheckWitness(multiaddr) { + panic("transferX: this method must be invoked from inner ring") + } + } + + if notaryDisabled && !inderectCall { + threshold := len(alphabet)*2/3 + 1 + id := common.InvokeID([]interface{}{from, to, amount}, []byte("transfer")) + + n := common.Vote(ctx, id, nodeKey) + if n < threshold { + return true + } + + common.RemoveVotes(ctx, id) } result := token.transfer(ctx, from, to, amount, true, details) @@ -145,20 +178,47 @@ func TransferX(from, to interop.Hash160, amount int, details []byte) bool { func Lock(txDetails []byte, from, to interop.Hash160, amount, until int) bool { ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) + + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + ) - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { - panic("lock: this method must be invoked from inner ring") + if notaryDisabled { + alphabet = common.AlphabetNodes() + nodeKey = common.InnerRingInvoker(alphabet) + if len(nodeKey) == 0 { + panic("lock: this method must be invoked from inner ring") + } + } else { + multiaddr := common.AlphabetAddress() + if !runtime.CheckWitness(multiaddr) { + panic("lock: this method must be invoked from inner ring") + } } + details := common.LockTransferDetails(txDetails) + lockAccount := Account{ Balance: 0, Until: until, Parent: from, } - common.SetSerialized(ctx, to, lockAccount) - details := common.LockTransferDetails(txDetails) + if notaryDisabled { + threshold := len(alphabet)*2/3 + 1 + id := common.InvokeID([]interface{}{txDetails}, []byte("lock")) + + n := common.Vote(ctx, id, nodeKey) + if n < threshold { + return true + } + + common.RemoveVotes(ctx, id) + } + + common.SetSerialized(ctx, to, lockAccount) result := token.transfer(ctx, from, to, amount, true, details) if !result { @@ -174,10 +234,22 @@ func Lock(txDetails []byte, from, to interop.Hash160, amount, until int) bool { func NewEpoch(epochNum int) bool { ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { - panic("newEpoch: this method must be invoked from inner ring") + if notaryDisabled { + indirectCall := common.FromKnownContract( + ctx, + runtime.GetCallingScriptHash(), + netmapContractKey, + ) + if !indirectCall { + panic("newEpoch: this method must be invoked from inner ring") + } + } else { + multiaddr := common.AlphabetAddress() + if !runtime.CheckWitness(multiaddr) { + panic("newEpoch: this method must be invoked from inner ring") + } } it := storage.Find(ctx, []byte{}, storage.KeysOnly) @@ -204,14 +276,40 @@ func NewEpoch(epochNum int) bool { func Mint(to interop.Hash160, amount int, txDetails []byte) bool { ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { - panic("mint: this method must be invoked from inner ring") + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + ) + + if notaryDisabled { + alphabet = common.AlphabetNodes() + nodeKey = common.InnerRingInvoker(alphabet) + if len(nodeKey) == 0 { + panic("mint: this method must be invoked from inner ring") + } + } else { + multiaddr := common.AlphabetAddress() + if !runtime.CheckWitness(multiaddr) { + panic("mint: this method must be invoked from inner ring") + } } details := common.MintTransferDetails(txDetails) + if notaryDisabled { + threshold := len(alphabet)*2/3 + 1 + id := common.InvokeID([]interface{}{txDetails}, []byte("mint")) + + n := common.Vote(ctx, id, nodeKey) + if n < threshold { + return true + } + + common.RemoveVotes(ctx, id) + } + ok := token.transfer(ctx, nil, to, amount, true, details) if !ok { panic("mint: can't transfer assets") @@ -228,14 +326,40 @@ func Mint(to interop.Hash160, amount int, txDetails []byte) bool { func Burn(from interop.Hash160, amount int, txDetails []byte) bool { ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) + + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + ) - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { - panic("burn: this method must be invoked from inner ring") + if notaryDisabled { + alphabet = common.AlphabetNodes() + nodeKey = common.InnerRingInvoker(alphabet) + if len(nodeKey) == 0 { + panic("burn: this method must be invoked from inner ring") + } + } else { + multiaddr := common.AlphabetAddress() + if !runtime.CheckWitness(multiaddr) { + panic("burn: this method must be invoked from inner ring") + } } details := common.BurnTransferDetails(txDetails) + if notaryDisabled { + threshold := len(alphabet)*2/3 + 1 + id := common.InvokeID([]interface{}{txDetails}, []byte("burn")) + + n := common.Vote(ctx, id, nodeKey) + if n < threshold { + return true + } + + common.RemoveVotes(ctx, id) + } + ok := token.transfer(ctx, from, nil, amount, true, details) if !ok { panic("burn: can't transfer assets") diff --git a/common/vote.go b/common/vote.go index a29ea4d3..bd45cf6e 100644 --- a/common/vote.go +++ b/common/vote.go @@ -120,3 +120,28 @@ func InvokeID(args []interface{}, prefix []byte) []byte { return crypto.Sha256(prefix) } + +/* + 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(ctx storage.Context, caller interop.Hash160, key string) bool { + addr := storage.Get(ctx, key).(interop.Hash160) + return BytesEqual(caller, addr) +} From 0f46a865cddfcaf321302fb5af050e354b3f00d5 Mon Sep 17 00:00:00 2001 From: Alex Vanin Date: Thu, 29 Apr 2021 16:14:25 +0300 Subject: [PATCH 14/19] [#74] container: Support notary disabled work flow Signed-off-by: Alex Vanin --- container/container_contract.go | 148 +++++++++++++++++++++++++++++--- 1 file changed, 134 insertions(+), 14 deletions(-) diff --git a/container/container_contract.go b/container/container_contract.go index e15a02ed..3c72278c 100644 --- a/container/container_contract.go +++ b/container/container_contract.go @@ -96,6 +96,7 @@ func Migrate(script []byte, manifest []byte) bool { func Put(container []byte, signature interop.Signature, publicKey interop.PublicKey) bool { ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) offset := int(container[1]) offset = 2 + offset + 4 // version prefix + version size + owner prefix @@ -103,8 +104,21 @@ func Put(container []byte, signature interop.Signature, publicKey interop.Public containerID := crypto.Sha256(container) neofsIDContractAddr := storage.Get(ctx, neofsIDContractKey).(interop.Hash160) - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { + var ( // for invocation collection without notary + alphabet = common.AlphabetNodes() + nodeKey []byte + alphabetCall bool + ) + + if notaryDisabled { + nodeKey = common.InnerRingInvoker(alphabet) + alphabetCall = len(nodeKey) != 0 + } else { + multiaddr := common.AlphabetAddress() + alphabetCall = runtime.CheckWitness(multiaddr) + } + + if !alphabetCall { if !isSignedByOwnerKey(container, signature, ownerID, publicKey) { // check keys from NeoFSID keys := contract.Call(neofsIDContractAddr, "key", contract.ReadOnly, ownerID).([]interop.PublicKey) @@ -126,7 +140,18 @@ func Put(container []byte, signature interop.Signature, publicKey interop.Public // todo: check if new container with unique container id - alphabet := common.AlphabetNodes() + if notaryDisabled { + threshold := len(alphabet)*2/3 + 1 + id := common.InvokeID([]interface{}{container, signature, publicKey}, []byte("put")) + + n := common.Vote(ctx, id, nodeKey) + if n < threshold { + return true + } + + common.RemoveVotes(ctx, id) + } + for i := 0; i < len(alphabet); i++ { node := alphabet[i] to := contract.CreateStandardAccount(node.PublicKey) @@ -153,14 +178,29 @@ func Put(container []byte, signature interop.Signature, publicKey interop.Public func Delete(containerID, signature []byte) bool { ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) ownerID := getOwnerByID(ctx, containerID) if len(ownerID) == 0 { panic("delete: container does not exist") } - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + alphabetCall bool + ) + + if notaryDisabled { + alphabet = common.AlphabetNodes() + nodeKey = common.InnerRingInvoker(alphabet) + alphabetCall = len(nodeKey) != 0 + } else { + multiaddr := common.AlphabetAddress() + alphabetCall = runtime.CheckWitness(multiaddr) + } + + if !alphabetCall { // check provided key neofsIDContractAddr := storage.Get(ctx, neofsIDContractKey).(interop.Hash160) keys := contract.Call(neofsIDContractAddr, "key", contract.ReadOnly, ownerID).([]interop.PublicKey) @@ -173,6 +213,18 @@ func Delete(containerID, signature []byte) bool { return true } + if notaryDisabled { + threshold := len(alphabet)*2/3 + 1 + id := common.InvokeID([]interface{}{containerID, signature}, []byte("delete")) + + n := common.Vote(ctx, id, nodeKey) + if n < threshold { + return true + } + + common.RemoveVotes(ctx, id) + } + removeContainer(ctx, containerID, ownerID) runtime.Log("delete: remove container") @@ -341,10 +393,22 @@ func ListContainerSizes(epoch int) [][]byte { func NewEpoch(epochNum int) { ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { - panic("newEpoch: this method must be invoked from inner ring") + if notaryDisabled { + indirectCall := common.FromKnownContract( + ctx, + runtime.GetCallingScriptHash(), + netmapContractKey, + ) + if !indirectCall { + panic("newEpoch: this method must be invoked from inner ring") + } + } else { + multiaddr := common.AlphabetAddress() + if !runtime.CheckWitness(multiaddr) { + panic("newEpoch: this method must be invoked from inner ring") + } } candidates := keysToDelete(ctx, epochNum) @@ -354,9 +418,37 @@ func NewEpoch(epochNum int) { } func StartContainerEstimation(epoch int) bool { - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { - panic("startEstimation: only inner ring nodes can invoke this") + ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) + + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + ) + + if notaryDisabled { + alphabet = common.AlphabetNodes() + nodeKey = common.InnerRingInvoker(alphabet) + if len(nodeKey) == 0 { + panic("startEstimation: only inner ring nodes can invoke this") + } + } else { + multiaddr := common.AlphabetAddress() + if !runtime.CheckWitness(multiaddr) { + panic("startEstimation: only inner ring nodes can invoke this") + } + } + + if notaryDisabled { + threshold := len(alphabet)*2/3 + 1 + id := common.InvokeID([]interface{}{epoch}, []byte("startEstimation")) + + n := common.Vote(ctx, id, nodeKey) + if n < threshold { + return true + } + + common.RemoveVotes(ctx, id) } runtime.Notify("StartEstimation", epoch) @@ -366,9 +458,37 @@ func StartContainerEstimation(epoch int) bool { } func StopContainerEstimation(epoch int) bool { - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { - panic("stopEstimation: only inner ring nodes can invoke this") + ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) + + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + ) + + if notaryDisabled { + alphabet = common.AlphabetNodes() + nodeKey = common.InnerRingInvoker(alphabet) + if len(nodeKey) == 0 { + panic("stopEstimation: only inner ring nodes can invoke this") + } + } else { + multiaddr := common.AlphabetAddress() + if !runtime.CheckWitness(multiaddr) { + panic("stopEstimation: only inner ring nodes can invoke this") + } + } + + if notaryDisabled { + threshold := len(alphabet)*2/3 + 1 + id := common.InvokeID([]interface{}{epoch}, []byte("stopEstimation")) + + n := common.Vote(ctx, id, nodeKey) + if n < threshold { + return true + } + + common.RemoveVotes(ctx, id) } runtime.Notify("StopEstimation", epoch) From c09df527c5f42acea708ec43b818843608cac372 Mon Sep 17 00:00:00 2001 From: Alex Vanin Date: Thu, 29 Apr 2021 16:15:29 +0300 Subject: [PATCH 15/19] [#74] neofs: Support notary disabled work flow Signed-off-by: Alex Vanin --- neofs/neofs_contract.go | 164 +++++++++++++++++++++++++++++++++++----- 1 file changed, 144 insertions(+), 20 deletions(-) diff --git a/neofs/neofs_contract.go b/neofs/neofs_contract.go index e993419b..59a68ad0 100644 --- a/neofs/neofs_contract.go +++ b/neofs/neofs_contract.go @@ -36,6 +36,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/interop" "github.com/nspcc-dev/neo-go/pkg/interop/contract" "github.com/nspcc-dev/neo-go/pkg/interop/iterator" + "github.com/nspcc-dev/neo-go/pkg/interop/native/crypto" "github.com/nspcc-dev/neo-go/pkg/interop/native/gas" "github.com/nspcc-dev/neo-go/pkg/interop/native/management" "github.com/nspcc-dev/neo-go/pkg/interop/native/std" @@ -155,10 +156,28 @@ func InnerRingCandidates() []common.IRNode { // InnerRingCandidateRemove removes key from the list of inner ring candidates. func InnerRingCandidateRemove(key interop.PublicKey) bool { ctx := storage.GetContext() - - multiaddr := AlphabetAddress() - if !runtime.CheckWitness(key) && !runtime.CheckWitness(multiaddr) { - panic("irCandidateRemove: this method must be invoked by candidate or alphabet") + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) + + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + ) + + keyOwner := runtime.CheckWitness(key) + + if !keyOwner { + if notaryDisabled { + alphabet = getNodes(ctx, alphabetKey) + nodeKey = common.InnerRingInvoker(alphabet) + if len(nodeKey) == 0 { + panic("irCandidateRemove: this method must be invoked by candidate or alphabet") + } + } else { + multiaddr := AlphabetAddress() + if !runtime.CheckWitness(multiaddr) { + panic("irCandidateRemove: this method must be invoked by candidate or alphabet") + } + } } nodes := []common.IRNode{} // it is explicit declaration of empty slice, not nil @@ -173,6 +192,19 @@ func InnerRingCandidateRemove(key interop.PublicKey) bool { } } + if notaryDisabled && !keyOwner { + threshold := len(alphabet)*2/3 + 1 + id := append(key, []byte("delete")...) + hashID := crypto.Sha256(id) + + n := common.Vote(ctx, hashID, nodeKey) + if n < threshold { + return true + } + + common.RemoveVotes(ctx, hashID) + } + common.SetSerialized(ctx, candidatesKey, nodes) return true @@ -274,14 +306,29 @@ func Withdraw(user interop.Hash160, amount int) bool { panic("withdraw: out of max amount limit") } - // transfer fee to proxy contract to pay cheque invocation ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) + + // transfer fee to proxy contract to pay cheque invocation fee := getConfig(ctx, withdrawFeeConfigKey).(int) - processingAddr := storage.Get(ctx, processingContractKey).(interop.Hash160) - transferred := gas.Transfer(user, processingAddr, fee, []byte{}) - if !transferred { - panic("withdraw: failed to transfer withdraw fee, aborting") + if notaryDisabled { + alphabet := getNodes(ctx, alphabetKey) + for _, node := range alphabet { + processingAddr := contract.CreateStandardAccount(node.PublicKey) + + transferred := gas.Transfer(user, processingAddr, fee, []byte{}) + if !transferred { + panic("withdraw: failed to transfer withdraw fee, aborting") + } + } + } else { + processingAddr := storage.Get(ctx, processingContractKey).(interop.Hash160) + + transferred := gas.Transfer(user, processingAddr, fee, []byte{}) + if !transferred { + panic("withdraw: failed to transfer withdraw fee, aborting") + } } // notify alphabet nodes @@ -296,13 +343,40 @@ func Withdraw(user interop.Hash160, amount int) bool { // Cheque sends gas assets back to the user if they were successfully // locked in NeoFS balance contract. func Cheque(id []byte, user interop.Hash160, amount int, lockAcc []byte) bool { - multiaddr := AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { - panic("cheque: this method must be invoked by alphabet") + ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) + + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + ) + + if notaryDisabled { + alphabet = getNodes(ctx, alphabetKey) + nodeKey = common.InnerRingInvoker(alphabet) + if len(nodeKey) == 0 { + panic("cheque: this method must be invoked by alphabet") + } + } else { + multiaddr := AlphabetAddress() + if !runtime.CheckWitness(multiaddr) { + panic("cheque: this method must be invoked by alphabet") + } } from := runtime.GetExecutingScriptHash() + if notaryDisabled { + threshold := len(alphabet)*2/3 + 1 + + n := common.Vote(ctx, id, nodeKey) + if n < threshold { + return true + } + + common.RemoveVotes(ctx, id) + } + transferred := gas.Transfer(from, user, amount, nil) if !transferred { panic("cheque: failed to transfer funds, aborting") @@ -352,16 +426,30 @@ func Unbind(user []byte, keys []interop.PublicKey) bool { // AlphabetUpdate updates list of alphabet nodes with provided list of // public keys. -func AlphabetUpdate(chequeID []byte, args []interop.PublicKey) bool { +func AlphabetUpdate(id []byte, args []interop.PublicKey) bool { ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) if len(args) == 0 { panic("alphabetUpdate: bad arguments") } - multiaddr := AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { - panic("alphabetUpdate: this method must be invoked by alphabet") + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + ) + + if notaryDisabled { + alphabet = getNodes(ctx, alphabetKey) + nodeKey = common.InnerRingInvoker(alphabet) + if len(nodeKey) == 0 { + panic("alphabetUpdate: this method must be invoked by alphabet") + } + } else { + multiaddr := AlphabetAddress() + if !runtime.CheckWitness(multiaddr) { + panic("alphabetUpdate: this method must be invoked by alphabet") + } } newAlphabet := []common.IRNode{} @@ -377,9 +465,20 @@ func AlphabetUpdate(chequeID []byte, args []interop.PublicKey) bool { }) } + if notaryDisabled { + threshold := len(alphabet)*2/3 + 1 + + n := common.Vote(ctx, id, nodeKey) + if n < threshold { + return true + } + + common.RemoveVotes(ctx, id) + } + common.SetSerialized(ctx, alphabetKey, newAlphabet) - runtime.Notify("AlphabetUpdate", chequeID, newAlphabet) + runtime.Notify("AlphabetUpdate", id, newAlphabet) runtime.Log("alphabetUpdate: alphabet list has been updated") return true @@ -394,10 +493,35 @@ func Config(key []byte) interface{} { // SetConfig key-value pair as a NeoFS runtime configuration value. func SetConfig(id, key, val []byte) bool { ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) + + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + ) + + if notaryDisabled { + alphabet = getNodes(ctx, alphabetKey) + nodeKey = common.InnerRingInvoker(alphabet) + if len(key) == 0 { + panic("setConfig: this method must be invoked by alphabet") + } + } else { + multiaddr := AlphabetAddress() + if !runtime.CheckWitness(multiaddr) { + panic("setConfig: this method must be invoked by alphabet") + } + } + + if notaryDisabled { + threshold := len(alphabet)*2/3 + 1 + + n := common.Vote(ctx, id, nodeKey) + if n < threshold { + return true + } - multiaddr := AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { - panic("setConfig: this method must be invoked by alphabet") + common.RemoveVotes(ctx, id) } setConfig(ctx, key, val) From c5cd7fa7f8624a57f0fcbb8dd69429a5f52771be Mon Sep 17 00:00:00 2001 From: Alex Vanin Date: Thu, 29 Apr 2021 16:17:41 +0300 Subject: [PATCH 16/19] [#74] neofsid: Support notary disabled work flow Signed-off-by: Alex Vanin --- neofsid/neofsid_contract.go | 82 ++++++++++++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 6 deletions(-) diff --git a/neofsid/neofsid_contract.go b/neofsid/neofsid_contract.go index 929c85d5..1eb66c2a 100644 --- a/neofsid/neofsid_contract.go +++ b/neofsid/neofsid_contract.go @@ -2,6 +2,7 @@ package neofsidcontract import ( "github.com/nspcc-dev/neo-go/pkg/interop" + "github.com/nspcc-dev/neo-go/pkg/interop/native/crypto" "github.com/nspcc-dev/neo-go/pkg/interop/native/management" "github.com/nspcc-dev/neo-go/pkg/interop/native/std" "github.com/nspcc-dev/neo-go/pkg/interop/runtime" @@ -67,10 +68,31 @@ func AddKey(owner []byte, keys []interop.PublicKey) bool { } ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { - panic("addKey: invocation from non inner ring node") + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + inderectCall bool + ) + + if notaryDisabled { + alphabet = common.AlphabetNodes() + nodeKey = common.InnerRingInvoker(alphabet) + if len(nodeKey) == 0 { + panic("addKey: invocation from non inner ring node") + } + + inderectCall = common.FromKnownContract( + ctx, + runtime.GetCallingScriptHash(), + containerContractKey, + ) + } else { + multiaddr := common.AlphabetAddress() + if !runtime.CheckWitness(multiaddr) { + panic("addKey: invocation from non inner ring node") + } } info := getUserInfo(ctx, owner) @@ -92,6 +114,18 @@ addLoop: info.Keys = append(info.Keys, pubKey) } + if notaryDisabled && !inderectCall { + threshold := len(alphabet)*2/3 + 1 + id := invokeIDKeys(owner, keys, []byte("add")) + + n := common.Vote(ctx, id, nodeKey) + if n < threshold { + return true + } + + common.RemoveVotes(ctx, id) + } + common.SetSerialized(ctx, owner, info) runtime.Log("addKey: key bound to the owner") @@ -104,10 +138,24 @@ func RemoveKey(owner []byte, keys []interop.PublicKey) bool { } ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { - panic("removeKey: invocation from non inner ring node") + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + ) + + if notaryDisabled { + alphabet = common.AlphabetNodes() + nodeKey = common.InnerRingInvoker(alphabet) + if len(nodeKey) == 0 { + panic("removeKey: invocation from non inner ring node") + } + } else { + multiaddr := common.AlphabetAddress() + if !runtime.CheckWitness(multiaddr) { + panic("removeKey: invocation from non inner ring node") + } } info := getUserInfo(ctx, owner) @@ -132,6 +180,19 @@ rmLoop: } info.Keys = leftKeys + + if notaryDisabled { + threshold := len(alphabet)*2/3 + 1 + id := invokeIDKeys(owner, keys, []byte("remove")) + + n := common.Vote(ctx, id, nodeKey) + if n < threshold { + return true + } + + common.RemoveVotes(ctx, id) + } + common.SetSerialized(ctx, owner, info) return true @@ -161,3 +222,12 @@ func getUserInfo(ctx storage.Context, key interface{}) UserInfo { return UserInfo{Keys: [][]byte{}} } + +func invokeIDKeys(owner []byte, keys []interop.PublicKey, prefix []byte) []byte { + prefix = append(prefix, owner...) + for i := range keys { + prefix = append(prefix, keys[i]...) + } + + return crypto.Sha256(prefix) +} From 74edff67670165ce538a3da2c56826baf179df94 Mon Sep 17 00:00:00 2001 From: Alex Vanin Date: Thu, 29 Apr 2021 16:20:00 +0300 Subject: [PATCH 17/19] [#74] netmap: Support notary disabled work flow Signed-off-by: Alex Vanin --- netmap/netmap_contract.go | 175 +++++++++++++++++++++++++++++++++++--- 1 file changed, 161 insertions(+), 14 deletions(-) diff --git a/netmap/netmap_contract.go b/netmap/netmap_contract.go index 0ab9d348..c7492621 100644 --- a/netmap/netmap_contract.go +++ b/netmap/netmap_contract.go @@ -4,6 +4,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/interop" "github.com/nspcc-dev/neo-go/pkg/interop/contract" "github.com/nspcc-dev/neo-go/pkg/interop/iterator" + "github.com/nspcc-dev/neo-go/pkg/interop/native/crypto" "github.com/nspcc-dev/neo-go/pkg/interop/native/management" "github.com/nspcc-dev/neo-go/pkg/interop/native/std" "github.com/nspcc-dev/neo-go/pkg/interop/runtime" @@ -121,10 +122,24 @@ func InnerRingList() []common.IRNode { func UpdateInnerRing(keys []interop.PublicKey) bool { ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { - panic("updateInnerRing: this method must be invoked by alpahbet nodes") + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + ) + + if notaryDisabled { + alphabet = common.AlphabetNodes() + nodeKey = common.InnerRingInvoker(alphabet) + if len(nodeKey) == 0 { + panic("updateInnerRing: this method must be invoked by alphabet nodes") + } + } else { + multiaddr := common.AlphabetAddress() + if !runtime.CheckWitness(multiaddr) { + panic("updateInnerRing: this method must be invoked by alphabet nodes") + } } var irList []common.IRNode @@ -134,6 +149,18 @@ func UpdateInnerRing(keys []interop.PublicKey) bool { irList = append(irList, common.IRNode{PublicKey: key}) } + if notaryDisabled { + threshold := len(alphabet)*2/3 + 1 + id := keysID(keys, []byte("updateIR")) + + n := common.Vote(ctx, id, nodeKey) + if n < threshold { + return true + } + + common.RemoveVotes(ctx, id) + } + runtime.Log("updateInnerRing: inner ring list updated") common.SetSerialized(ctx, innerRingKey, irList) @@ -142,9 +169,24 @@ func UpdateInnerRing(keys []interop.PublicKey) bool { func AddPeer(nodeInfo []byte) bool { ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) + + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + alphabetCall bool + ) + + if notaryDisabled { + alphabet = common.AlphabetNodes() + nodeKey = common.InnerRingInvoker(alphabet) + alphabetCall = len(nodeKey) != 0 + } else { + multiaddr := common.AlphabetAddress() + alphabetCall = runtime.CheckWitness(multiaddr) + } - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { + if !alphabetCall { publicKey := nodeInfo[2:35] // offset:2, len:33 if !runtime.CheckWitness(publicKey) { panic("addPeer: witness check failed") @@ -159,6 +201,19 @@ func AddPeer(nodeInfo []byte) bool { nm := addToNetmap(ctx, candidate) + if notaryDisabled { + threshold := len(alphabet)*2/3 + 1 + rawCandidate := std.Serialize(candidate) + id := crypto.Sha256(rawCandidate) + + n := common.Vote(ctx, id, nodeKey) + if n < threshold { + return true + } + + common.RemoveVotes(ctx, id) + } + if nm == nil { runtime.Log("addPeer: storage node already in the netmap") } else { @@ -175,9 +230,24 @@ func UpdateState(state int, publicKey interop.PublicKey) bool { } ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) + + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + alphabetCall bool + ) - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { + if notaryDisabled { + alphabet = common.AlphabetNodes() + nodeKey = common.InnerRingInvoker(alphabet) + alphabetCall = len(nodeKey) != 0 + } else { + multiaddr := common.AlphabetAddress() + alphabetCall = runtime.CheckWitness(multiaddr) + } + + if !alphabetCall { if !runtime.CheckWitness(publicKey) { panic("updateState: witness check failed") } @@ -187,6 +257,18 @@ func UpdateState(state int, publicKey interop.PublicKey) bool { return true } + if notaryDisabled { + threshold := len(alphabet)*2/3 + 1 + id := common.InvokeID([]interface{}{state, publicKey}, []byte("update")) + + n := common.Vote(ctx, id, nodeKey) + if n < threshold { + return true + } + + common.RemoveVotes(ctx, id) + } + switch nodeState(state) { case offlineState: newNetmap := removeFromNetmap(ctx, publicKey) @@ -201,10 +283,36 @@ func UpdateState(state int, publicKey interop.PublicKey) bool { func NewEpoch(epochNum int) bool { ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) + + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + ) + + if notaryDisabled { + alphabet = common.AlphabetNodes() + nodeKey = common.InnerRingInvoker(alphabet) + if len(nodeKey) == 0 { + panic("newEpoch: this method must be invoked by inner ring nodes") + } + } else { + multiaddr := common.AlphabetAddress() + if !runtime.CheckWitness(multiaddr) { + panic("newEpoch: this method must be invoked by inner ring nodes") + } + } - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { - panic("newEpoch: this method must be invoked by inner ring nodes") + if notaryDisabled { + threshold := len(alphabet)*2/3 + 1 + id := common.InvokeID([]interface{}{epochNum}, []byte("epoch")) + + n := common.Vote(ctx, id, nodeKey) + if n < threshold { + return true + } + + common.RemoveVotes(ctx, id) } currentEpoch := storage.Get(ctx, snapshotEpoch).(int) @@ -273,12 +381,37 @@ func Config(key []byte) interface{} { } func SetConfig(id, key, val []byte) bool { - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { - panic("setConfig: invoked by non inner ring node") + ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) + + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + ) + + if notaryDisabled { + alphabet = common.AlphabetNodes() + nodeKey = common.InnerRingInvoker(alphabet) + if len(nodeKey) == 0 { + panic("setConfig: invoked by non inner ring node") + } + } else { + multiaddr := common.AlphabetAddress() + if !runtime.CheckWitness(multiaddr) { + panic("setConfig: invoked by non inner ring node") + } } - ctx := storage.GetContext() + if notaryDisabled { + threshold := len(alphabet)*2/3 + 1 + + n := common.Vote(ctx, id, nodeKey) + if n < threshold { + return true + } + + common.RemoveVotes(ctx, id) + } setConfig(ctx, key, val) @@ -443,3 +576,17 @@ func getIRNodes(ctx storage.Context) []common.IRNode { return []common.IRNode{} } + +func keysID(args []interop.PublicKey, prefix []byte) []byte { + var ( + result []byte + ) + + result = append(result, prefix...) + + for i := range args { + result = append(result, args[i]...) + } + + return crypto.Sha256(result) +} From e6adb03dc39f0b8afea28f5de63dd3cf76c4615f Mon Sep 17 00:00:00 2001 From: Alex Vanin Date: Thu, 29 Apr 2021 16:20:22 +0300 Subject: [PATCH 18/19] [#74] reputation: Support notary disabled work flow Signed-off-by: Alex Vanin --- reputation/reputation_contract.go | 32 +++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/reputation/reputation_contract.go b/reputation/reputation_contract.go index 2069a0da..2a9b3778 100644 --- a/reputation/reputation_contract.go +++ b/reputation/reputation_contract.go @@ -50,9 +50,24 @@ func Migrate(script []byte, manifest []byte) bool { func Put(epoch int, peerID []byte, value []byte) { ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + alphabetCall bool + ) + + if notaryDisabled { + alphabet = common.AlphabetNodes() + nodeKey = common.InnerRingInvoker(alphabet) + alphabetCall = len(nodeKey) != 0 + } else { + multiaddr := common.AlphabetAddress() + alphabetCall = runtime.CheckWitness(multiaddr) + } + + if !alphabetCall { runtime.Notify("reputationPut", epoch, peerID, value) return } @@ -63,6 +78,18 @@ func Put(epoch int, peerID []byte, value []byte) { reputationValues = append(reputationValues, value) rawValues := std.Serialize(reputationValues) + + if notaryDisabled { + threshold := len(alphabet)*2/3 + 1 + + n := common.Vote(ctx, id, nodeKey) + if n < threshold { + return + } + + common.RemoveVotes(ctx, id) + } + storage.Put(ctx, id, rawValues) } @@ -93,6 +120,7 @@ func ListByEpoch(epoch int) [][]byte { ignore := [][]byte{ []byte(common.OwnerKey), + []byte(notaryDisabledKey), } loop: From e0a2d2ed596a255c8baf569a5c3b3c7e5b5a4857 Mon Sep 17 00:00:00 2001 From: Alex Vanin Date: Tue, 4 May 2021 17:02:05 +0300 Subject: [PATCH 19/19] [#74] Log notary disabled flag at init Signed-off-by: Alex Vanin --- alphabet/alphabet_contract.go | 1 + audit/audit_contract.go | 3 +++ balance/balance_contract.go | 1 + container/container_contract.go | 1 + neofs/neofs_contract.go | 1 + neofsid/neofsid_contract.go | 1 + netmap/netmap_contract.go | 1 + reputation/reputation_contract.go | 1 + 8 files changed, 10 insertions(+) diff --git a/alphabet/alphabet_contract.go b/alphabet/alphabet_contract.go index d796fdd0..d6c2113b 100644 --- a/alphabet/alphabet_contract.go +++ b/alphabet/alphabet_contract.go @@ -55,6 +55,7 @@ func Init(notaryDisabled bool, owner interop.Hash160, addrNetmap, addrProxy inte storage.Put(ctx, notaryDisabledKey, notaryDisabled) if notaryDisabled { common.InitVote(ctx) + runtime.Log(name + " notary disabled") } runtime.Log(name + " contract initialized") diff --git a/audit/audit_contract.go b/audit/audit_contract.go index 276a6a97..3ba24e19 100644 --- a/audit/audit_contract.go +++ b/audit/audit_contract.go @@ -58,6 +58,9 @@ func Init(notaryDisabled bool, owner interop.Hash160, addrNetmap interop.Hash160 // initialize the way to collect signatures storage.Put(ctx, notaryDisabledKey, notaryDisabled) + if notaryDisabled { + runtime.Log("audit contract notary disabled") + } runtime.Log("audit contract initialized") } diff --git a/balance/balance_contract.go b/balance/balance_contract.go index 9d7e7e08..8e223ce4 100644 --- a/balance/balance_contract.go +++ b/balance/balance_contract.go @@ -82,6 +82,7 @@ func Init(notaryDisabled bool, owner, addrNetmap, addrContainer interop.Hash160) storage.Put(ctx, notaryDisabledKey, notaryDisabled) if notaryDisabled { common.InitVote(ctx) + runtime.Log("balance contract notary disabled") } runtime.Log("balance contract initialized") diff --git a/container/container_contract.go b/container/container_contract.go index 3c72278c..ecae9f64 100644 --- a/container/container_contract.go +++ b/container/container_contract.go @@ -75,6 +75,7 @@ func Init(notaryDisabled bool, owner, addrNetmap, addrBalance, addrID interop.Ha storage.Put(ctx, notaryDisabledKey, notaryDisabled) if notaryDisabled { common.InitVote(ctx) + runtime.Log("container contract notary disabled") } runtime.Log("container contract initialized") diff --git a/neofs/neofs_contract.go b/neofs/neofs_contract.go index 59a68ad0..f114c024 100644 --- a/neofs/neofs_contract.go +++ b/neofs/neofs_contract.go @@ -113,6 +113,7 @@ func Init(notaryDisabled bool, owner, addrProc interop.Hash160, args []interop.P storage.Put(ctx, notaryDisabledKey, notaryDisabled) if notaryDisabled { common.InitVote(ctx) + runtime.Log("neofs contract notary disabled") } runtime.Log("neofs: contract initialized") diff --git a/neofsid/neofsid_contract.go b/neofsid/neofsid_contract.go index 1eb66c2a..8d0553f8 100644 --- a/neofsid/neofsid_contract.go +++ b/neofsid/neofsid_contract.go @@ -43,6 +43,7 @@ func Init(notaryDisabled bool, owner, addrNetmap, addrContainer interop.Hash160) storage.Put(ctx, notaryDisabledKey, notaryDisabled) if notaryDisabled { common.InitVote(ctx) + runtime.Log("neofsid contract notary disabled") } runtime.Log("neofsid contract initialized") diff --git a/netmap/netmap_contract.go b/netmap/netmap_contract.go index c7492621..e87842e2 100644 --- a/netmap/netmap_contract.go +++ b/netmap/netmap_contract.go @@ -96,6 +96,7 @@ func Init(notaryDisabled bool, owner, addrBalance, addrContainer interop.Hash160 common.SetSerialized(ctx, innerRingKey, irList) common.InitVote(ctx) + runtime.Log("netmap contract notary disabled") } runtime.Log("netmap contract initialized") diff --git a/reputation/reputation_contract.go b/reputation/reputation_contract.go index 2a9b3778..59740e69 100644 --- a/reputation/reputation_contract.go +++ b/reputation/reputation_contract.go @@ -29,6 +29,7 @@ func Init(notaryDisabled bool, owner interop.Hash160) { storage.Put(ctx, notaryDisabledKey, notaryDisabled) if notaryDisabled { common.InitVote(ctx) + runtime.Log("reputation contract notary disabled") } runtime.Log("reputation contract initialized")