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/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: diff --git a/alphabet/alphabet_contract.go b/alphabet/alphabet_contract.go index d6b56713..d6c2113b 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" @@ -19,6 +20,8 @@ const ( totalKey = "threshold" nameKey = "name" + notaryDisabledKey = "notary" + version = 1 ) @@ -30,7 +33,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 +51,13 @@ 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 + " notary disabled") + } + runtime.Log(name + " contract initialized") } @@ -104,6 +114,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) { @@ -126,7 +137,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 { @@ -142,13 +161,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) @@ -159,6 +192,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") @@ -169,6 +214,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) diff --git a/audit/audit_contract.go b/audit/audit_contract.go index 400c6925..3ba24e19 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,12 @@ 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) + if notaryDisabled { + runtime.Log("audit contract notary disabled") + } + runtime.Log("audit contract initialized") } @@ -73,7 +81,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 @@ -148,6 +165,7 @@ func list(it iterator.Iterator) [][]byte { ignore := [][]byte{ []byte(netmapContractKey), []byte(common.OwnerKey), + []byte(notaryDisabledKey), } loop: diff --git a/balance/balance_contract.go b/balance/balance_contract.go index e91f9bfc..8e223ce4 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,13 @@ 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 notary disabled") + } + runtime.Log("balance contract initialized") } @@ -119,10 +127,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) + + 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 + } - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { - panic("transferX: this method must be invoked from inner ring") + common.RemoveVotes(ctx, id) } result := token.transfer(ctx, from, to, amount, true, details) @@ -138,20 +179,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) - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { - panic("lock: 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("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 { @@ -167,10 +235,22 @@ func Lock(txDetails []byte, from, to interop.Hash160, amount, until int) bool { func NewEpoch(epochNum int) bool { ctx := storage.GetContext() - - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { - panic("newEpoch: this method must be invoked from inner ring") + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) + + 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) @@ -197,14 +277,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) + + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + ) - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { - panic("mint: this method must be invoked from inner ring") + 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") @@ -221,14 +327,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) - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { - panic("burn: 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("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/ir.go b/common/ir.go index 73002ab4..86f50b58 100644 --- a/common/ir.go +++ b/common/ir.go @@ -13,7 +13,10 @@ 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] @@ -33,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 309da2a2..bd45cf6e 100644 --- a/common/vote.go +++ b/common/vote.go @@ -110,6 +110,8 @@ 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) @@ -118,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) +} diff --git a/container/container_contract.go b/container/container_contract.go index 4271d956..ecae9f64 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,13 @@ 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 notary disabled") + } + runtime.Log("container contract initialized") } @@ -88,6 +97,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 @@ -95,8 +105,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) @@ -118,7 +141,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) @@ -145,14 +179,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) @@ -165,6 +214,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") @@ -333,10 +394,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) @@ -346,9 +419,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) @@ -358,9 +459,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) 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..f114c024 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 */ @@ -51,14 +53,16 @@ type ( ) const ( - defaultCandidateFee = 100 * 1_0000_0000 // 100 Fixed8 Gas candidateFeeConfigKey = "InnerRingCandidateFee" + withdrawFeeConfigKey = "WithdrawFee" version = 3 - alphabetKey = "alphabet" - candidatesKey = "candidates" - cashedChequesKey = "cheques" + alphabetKey = "alphabet" + candidatesKey = "candidates" + notaryDisabledKey = "notary" + + processingContractKey = "processingScriptHash" publicKeySize = 33 @@ -73,7 +77,7 @@ var ( ) // Init set up initial alphabet node keys. -func Init(owner interop.PublicKey, args []interop.PublicKey) bool { +func Init(notaryDisabled bool, owner, addrProc interop.Hash160, args []interop.PublicKey) bool { ctx := storage.GetContext() if !common.HasUpdateAccess(ctx) { @@ -86,6 +90,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 { @@ -96,10 +104,17 @@ 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) + 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 notary disabled") + } runtime.Log("neofs: contract initialized") @@ -127,6 +142,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() @@ -136,22 +157,27 @@ func InnerRingCandidates() []common.IRNode { // InnerRingCandidateRemove removes key from the list of inner ring candidates. func InnerRingCandidateRemove(key interop.PublicKey) bool { ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) - 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") - } + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + ) - id := append(key, []byte("delete")...) - hashID := crypto.Sha256(id) + keyOwner := runtime.CheckWitness(key) - n := common.Vote(ctx, hashID, nodeKey) - if n < threshold { - return true + 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") + } } } @@ -167,6 +193,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 @@ -177,7 +216,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} @@ -255,7 +294,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") } @@ -268,9 +307,35 @@ func Withdraw(user []byte, amount int) bool { panic("withdraw: out of max amount limit") } - amount = amount * 100000000 + ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) + + // transfer fee to proxy contract to pay cheque invocation + fee := getConfig(ctx, withdrawFeeConfigKey).(int) + + 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 + amount = amount * 100000000 tx := runtime.GetScriptContainer() + runtime.Notify("Withdraw", user, amount, tx.Hash) return true @@ -280,30 +345,47 @@ func Withdraw(user []byte, amount int) bool { // 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 + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) - hashID := crypto.Sha256(id) + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + ) - key := common.InnerRingInvoker(alphabet) - if len(key) == 0 { - panic("cheque: invoked by non alphabet node") + 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") + } } - n := common.Vote(ctx, hashID, key) - if n >= threshold { - common.RemoveVotes(ctx, hashID) - from := runtime.GetExecutingScriptHash() + from := runtime.GetExecutingScriptHash() - transferred := gas.Transfer(from, user, amount, nil) - if !transferred { - panic("cheque: failed to transfer funds, aborting") + if notaryDisabled { + threshold := len(alphabet)*2/3 + 1 + + n := common.Vote(ctx, id, nodeKey) + if n < threshold { + return true } - runtime.Log("cheque: funds have been transferred") - runtime.Notify("Cheque", id, user, amount, lockAcc) + common.RemoveVotes(ctx, id) } + 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 } @@ -345,19 +427,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") } - alphabet := getNodes(ctx, alphabetKey) - threshold := len(alphabet)*2/3 + 1 + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + ) - key := common.InnerRingInvoker(alphabet) - if len(key) == 0 { - panic("innerRingUpdate: invoked by non alphabet node") + 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{} @@ -373,18 +466,22 @@ func AlphabetUpdate(chequeID []byte, args []interop.PublicKey) bool { }) } - hashID := crypto.Sha256(chequeID) - - n := common.Vote(ctx, hashID, key) - if n >= threshold { - common.RemoveVotes(ctx, hashID) + if notaryDisabled { + threshold := len(alphabet)*2/3 + 1 - common.SetSerialized(ctx, alphabetKey, newAlphabet) + n := common.Vote(ctx, id, nodeKey) + if n < threshold { + return true + } - runtime.Notify("AlphabetUpdate", chequeID, newAlphabet) - runtime.Log("alphabetUpdate: alphabet list has been updated") + common.RemoveVotes(ctx, id) } + common.SetSerialized(ctx, alphabetKey, newAlphabet) + + runtime.Notify("AlphabetUpdate", id, newAlphabet) + runtime.Log("alphabetUpdate: alphabet list has been updated") + return true } @@ -397,29 +494,42 @@ 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) - // check if it is alphabet invocation - alphabet := getNodes(ctx, alphabetKey) - threshold := len(alphabet)*2/3 + 1 + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + ) - nodeKey := common.InnerRingInvoker(alphabet) - if len(nodeKey) == 0 { - panic("setConfig: invoked by non alphabet node") + 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") + } } - // vote for new configuration value - hashID := crypto.Sha256(id) - - n := common.Vote(ctx, hashID, nodeKey) - if n >= threshold { - common.RemoveVotes(ctx, hashID) + if notaryDisabled { + threshold := len(alphabet)*2/3 + 1 - setConfig(ctx, key, val) + n := common.Vote(ctx, id, nodeKey) + if n < threshold { + return true + } - runtime.Notify("SetConfig", id, key, val) - runtime.Log("setConfig: configuration has been updated") + common.RemoveVotes(ctx, id) } + setConfig(ctx, key, val) + + runtime.Notify("SetConfig", id, key, val) + runtime.Log("setConfig: configuration has been updated") + return true } @@ -455,8 +565,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] @@ -514,23 +622,16 @@ 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 - ) +// 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 - 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]) - } + keys := []interop.PublicKey{} + for _, node := range n { + key := node.PublicKey + keys = append(keys, key) } - return newLst, add, flag + return contract.CreateMultisigAccount(threshold, keys) } diff --git a/neofsid/neofsid_contract.go b/neofsid/neofsid_contract.go index da9f43d4..8d0553f8 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" @@ -20,9 +21,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 +39,13 @@ 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 notary disabled") + } + runtime.Log("neofsid contract initialized") } @@ -60,10 +69,31 @@ func AddKey(owner []byte, keys []interop.PublicKey) bool { } ctx := storage.GetContext() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) + + 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") + } - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { - 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) @@ -85,6 +115,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") @@ -97,10 +139,24 @@ func RemoveKey(owner []byte, keys []interop.PublicKey) bool { } ctx := storage.GetContext() - - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { - panic("removeKey: invocation from non inner ring node") + 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("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) @@ -125,6 +181,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 @@ -154,3 +223,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) +} 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 085d8b91..e87842e2 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" @@ -32,8 +33,10 @@ type ( const ( version = 1 - netmapKey = "netmap" - configuredKey = "initconfig" + netmapKey = "netmap" + configuredKey = "initconfig" + notaryDisabledKey = "notary" + innerRingKey = "innerring" snapshot0Key = "snapshotCurrent" snapshot1Key = "snapshotPrevious" @@ -41,7 +44,8 @@ const ( containerContractKey = "containerScriptHash" balanceContractKey = "balanceScriptHash" - cleanupEpochMethod = "newEpoch" + + cleanupEpochMethod = "newEpoch" ) const ( @@ -56,7 +60,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, keys []interop.PublicKey) { ctx := storage.GetContext() if !common.HasUpdateAccess(ctx) { @@ -80,6 +84,21 @@ 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 { + var irList []common.IRNode + + for i := 0; i < len(keys); i++ { + key := keys[i] + irList = append(irList, common.IRNode{PublicKey: key}) + } + + common.SetSerialized(ctx, innerRingKey, irList) + common.InitVote(ctx) + runtime.Log("netmap contract notary disabled") + } + runtime.Log("netmap contract initialized") } @@ -97,11 +116,78 @@ 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() + notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) + + var ( // for invocation collection without notary + alphabet []common.IRNode + nodeKey []byte + ) + + if notaryDisabled { + alphabet = common.AlphabetNodes() + nodeKey = common.InnerRingInvoker(alphabet) + if len(nodeKey) == 0 { + panic("updateInnerRing: this method must be invoked by alphabet nodes") + } + } else { + multiaddr := common.AlphabetAddress() + if !runtime.CheckWitness(multiaddr) { + panic("updateInnerRing: this method must be invoked by alphabet nodes") + } + } + + var irList []common.IRNode + + for i := 0; i < len(keys); i++ { + key := keys[i] + irList = append(irList, common.IRNode{PublicKey: key}) + } + + if notaryDisabled { + threshold := len(alphabet)*2/3 + 1 + id := keysID(keys, []byte("updateIR")) + + n := common.Vote(ctx, id, nodeKey) + if n < threshold { + return true + } + + common.RemoveVotes(ctx, id) + } + + runtime.Log("updateInnerRing: inner ring list updated") + common.SetSerialized(ctx, innerRingKey, irList) + + return true +} + func AddPeer(nodeInfo []byte) bool { 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 { publicKey := nodeInfo[2:35] // offset:2, len:33 if !runtime.CheckWitness(publicKey) { panic("addPeer: witness check failed") @@ -116,6 +202,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 { @@ -132,9 +231,24 @@ func UpdateState(state int, publicKey interop.PublicKey) bool { } 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 { if !runtime.CheckWitness(publicKey) { panic("updateState: witness check failed") } @@ -144,6 +258,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) @@ -158,10 +284,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 + ) - multiaddr := common.AlphabetAddress() - if !runtime.CheckWitness(multiaddr) { - panic("newEpoch: this method must be invoked by inner ring nodes") + if notaryDisabled { + alphabet = common.AlphabetNodes() + nodeKey = common.InnerRingInvoker(alphabet) + if len(nodeKey) == 0 { + panic("newEpoch: this method must be invoked by inner ring nodes") + } + } else { + multiaddr := common.AlphabetAddress() + if !runtime.CheckWitness(multiaddr) { + panic("newEpoch: this method must be invoked by inner ring nodes") + } + } + + if notaryDisabled { + threshold := len(alphabet)*2/3 + 1 + id := common.InvokeID([]interface{}{epochNum}, []byte("epoch")) + + n := common.Vote(ctx, id, nodeKey) + if n < threshold { + return true + } + + common.RemoveVotes(ctx, id) } currentEpoch := storage.Get(ctx, snapshotEpoch).(int) @@ -230,12 +382,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) @@ -391,3 +568,26 @@ 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{} +} + +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) +} 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 +} 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") } } diff --git a/reputation/reputation_contract.go b/reputation/reputation_contract.go index e4640fab..59740e69 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,13 @@ 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 notary disabled") + } + runtime.Log("reputation contract initialized") } @@ -42,9 +51,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) + + 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 { runtime.Notify("reputationPut", epoch, peerID, value) return } @@ -55,6 +79,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) } @@ -85,6 +121,7 @@ func ListByEpoch(epoch int) [][]byte { ignore := [][]byte{ []byte(common.OwnerKey), + []byte(notaryDisabledKey), } loop: