From 9ed3845aa98930e7c4a83f1221dbb7d7f5c9ff7f Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 19 Oct 2023 19:33:02 +0300 Subject: [PATCH] [#44] policy: Initial implementation Signed-off-by: Evgenii Stratonikov --- Makefile | 2 +- policy/config.yml | 2 + policy/doc.go | 12 ++++ policy/policy_contract.go | 79 +++++++++++++++++++++++ rpcclient/policy/client.go | 124 +++++++++++++++++++++++++++++++++++++ tests/policy_test.go | 91 +++++++++++++++++++++++++++ 6 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 policy/config.yml create mode 100644 policy/doc.go create mode 100644 policy/policy_contract.go create mode 100644 rpcclient/policy/client.go create mode 100644 tests/policy_test.go diff --git a/Makefile b/Makefile index 48bed61..c94e67b 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ all: sidechain mainnet sidechain: alphabet morph nns alphabet_sc = alphabet -morph_sc = audit balance container frostfsid netmap proxy reputation +morph_sc = audit balance container frostfsid netmap proxy reputation policy mainnet_sc = frostfs processing nns_sc = nns all_sc = $(alphabet_sc) $(morph_sc) $(mainnet_sc) $(nns_sc) diff --git a/policy/config.yml b/policy/config.yml new file mode 100644 index 0000000..4b43ec9 --- /dev/null +++ b/policy/config.yml @@ -0,0 +1,2 @@ +name: "APE" +safemethods: ["listChains"] diff --git a/policy/doc.go b/policy/doc.go new file mode 100644 index 0000000..dee664a --- /dev/null +++ b/policy/doc.go @@ -0,0 +1,12 @@ +/* + +# Contract storage scheme + + | Key | Value | Description | + |------------------------------------------|--------|-----------------------------------| + | 'c' + uint16(len(container)) + container | []byte | Namespace chain | + | 'n' + uint16(len(namespace)) + namespace | []byte | Container chain | + +*/ + +package policy diff --git a/policy/policy_contract.go b/policy/policy_contract.go new file mode 100644 index 0000000..b7e7b21 --- /dev/null +++ b/policy/policy_contract.go @@ -0,0 +1,79 @@ +package policy + +import ( + "git.frostfs.info/TrueCloudLab/frostfs-contract/common" + "github.com/nspcc-dev/neo-go/pkg/interop/iterator" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" +) + +// Kind represents the object the chain is attached to. +// Currently only namespace and container are supported. +type Kind byte + +const ( + Namespace = 'n' + Container = 'c' + IAM = 'i' +) + +// _deploy function sets up initial list of inner ring public keys. +func _deploy(data interface{}, isUpdate bool) { +} + +func storageKey(prefix Kind, entityName, name string) []byte { + ln := len(entityName) + key := append([]byte{byte(prefix)}, byte(ln&0xFF), byte(ln>>8)) + key = append(key, entityName...) + return append(key, name...) +} + +func AddChain(entity Kind, entityName, name string, chain []byte) { + common.CheckAlphabetWitness() // TODO: Allow to work with chain directly for everyone? + + ctx := storage.GetContext() + key := storageKey(entity, entityName, name) + storage.Put(ctx, key, chain) +} + +func RemoveChain(entity Kind, entityName string, name string) { + common.CheckAlphabetWitness() + + ctx := storage.GetContext() + key := storageKey(entity, entityName, name) + storage.Delete(ctx, key) +} + +func RemoveChainsByPrefix(entity Kind, entityName string, name string) { + common.CheckAlphabetWitness() + + ctx := storage.GetContext() + key := storageKey(entity, entityName, name) + it := storage.Find(ctx, key, storage.KeysOnly) + for iterator.Next(it) { + storage.Delete(ctx, iterator.Value(it).([]byte)) + } +} + +// ListChains lists all chains for the namespace by prefix. +// container may be empty. +func ListChains(namespace, container, name string) [][]byte { + ctx := storage.GetReadOnlyContext() + + var result [][]byte + + prefixNs := storageKey(Namespace, namespace, name) + it := storage.Find(ctx, prefixNs, storage.ValuesOnly) + for iterator.Next(it) { + result = append(result, iterator.Value(it).([]byte)) + } + + if container != "" { + prefixCnr := storageKey(Container, container, name) + it = storage.Find(ctx, prefixCnr, storage.ValuesOnly) + for iterator.Next(it) { + result = append(result, iterator.Value(it).([]byte)) + } + } + + return result +} diff --git a/rpcclient/policy/client.go b/rpcclient/policy/client.go new file mode 100644 index 0000000..1c48c99 --- /dev/null +++ b/rpcclient/policy/client.go @@ -0,0 +1,124 @@ +// Package ape contains RPC wrappers for APE contract. +// +// Code generated by neo-go contract generate-rpcwrapper --manifest --out [--hash ] [--config ]; DO NOT EDIT. +package ape + +import ( + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "math/big" +) + +// Invoker is used by ContractReader to call various safe methods. +type Invoker interface { + Call(contract util.Uint160, operation string, params ...any) (*result.Invoke, error) +} + +// Actor is used by Contract to call state-changing methods. +type Actor interface { + Invoker + + MakeCall(contract util.Uint160, method string, params ...any) (*transaction.Transaction, error) + MakeRun(script []byte) (*transaction.Transaction, error) + MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...any) (*transaction.Transaction, error) + MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) + SendCall(contract util.Uint160, method string, params ...any) (util.Uint256, uint32, error) + SendRun(script []byte) (util.Uint256, uint32, error) +} + +// ContractReader implements safe contract methods. +type ContractReader struct { + invoker Invoker + hash util.Uint160 +} + +// Contract implements all contract methods. +type Contract struct { + ContractReader + actor Actor + hash util.Uint160 +} + +// NewReader creates an instance of ContractReader using provided contract hash and the given Invoker. +func NewReader(invoker Invoker, hash util.Uint160) *ContractReader { + return &ContractReader{invoker, hash} +} + +// New creates an instance of Contract using provided contract hash and the given Actor. +func New(actor Actor, hash util.Uint160) *Contract { + return &Contract{ContractReader{actor, hash}, actor, hash} +} + +// ListChains invokes `listChains` method of contract. +func (c *ContractReader) ListChains(namespace string, container string, name string) ([]stackitem.Item, error) { + return unwrap.Array(c.invoker.Call(c.hash, "listChains", namespace, container, name)) +} + +// AddChain creates a transaction invoking `addChain` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) AddChain(entity *big.Int, entityName string, name string, chain []byte) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "addChain", entity, entityName, name, chain) +} + +// AddChainTransaction creates a transaction invoking `addChain` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) AddChainTransaction(entity *big.Int, entityName string, name string, chain []byte) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "addChain", entity, entityName, name, chain) +} + +// AddChainUnsigned creates a transaction invoking `addChain` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) AddChainUnsigned(entity *big.Int, entityName string, name string, chain []byte) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "addChain", nil, entity, entityName, name, chain) +} + +// RemoveChain creates a transaction invoking `removeChain` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) RemoveChain(entity *big.Int, entityName string, name string) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "removeChain", entity, entityName, name) +} + +// RemoveChainTransaction creates a transaction invoking `removeChain` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) RemoveChainTransaction(entity *big.Int, entityName string, name string) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "removeChain", entity, entityName, name) +} + +// RemoveChainUnsigned creates a transaction invoking `removeChain` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) RemoveChainUnsigned(entity *big.Int, entityName string, name string) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "removeChain", nil, entity, entityName, name) +} + +// RemoveChainsByPrefix creates a transaction invoking `removeChainsByPrefix` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) RemoveChainsByPrefix(entity *big.Int, entityName string, name string) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "removeChainsByPrefix", entity, entityName, name) +} + +// RemoveChainsByPrefixTransaction creates a transaction invoking `removeChainsByPrefix` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) RemoveChainsByPrefixTransaction(entity *big.Int, entityName string, name string) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "removeChainsByPrefix", entity, entityName, name) +} + +// RemoveChainsByPrefixUnsigned creates a transaction invoking `removeChainsByPrefix` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) RemoveChainsByPrefixUnsigned(entity *big.Int, entityName string, name string) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "removeChainsByPrefix", nil, entity, entityName, name) +} diff --git a/tests/policy_test.go b/tests/policy_test.go new file mode 100644 index 0000000..10a74db --- /dev/null +++ b/tests/policy_test.go @@ -0,0 +1,91 @@ +package tests + +import ( + "path" + "testing" + + "git.frostfs.info/TrueCloudLab/frostfs-contract/policy" + "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/stretchr/testify/require" +) + +const policyPath = "../policy" + +func deployPolicyContract(t *testing.T, e *neotest.Executor) util.Uint160 { + cfgPath := path.Join(policyPath, "config.yml") + c := neotest.CompileFile(t, e.CommitteeHash, policyPath, cfgPath) + e.DeployContract(t, c, nil) + return c.Hash +} + +func newPolicyInvoker(t *testing.T) *neotest.ContractInvoker { + e := newExecutor(t) + h := deployPolicyContract(t, e) + return e.CommitteeInvoker(h) +} + +func TestPolicy(t *testing.T) { + e := newPolicyInvoker(t) + + // Policies are opaque to the contract and are just raw bytes to store. + p1 := []byte("chain1") + p2 := []byte("chain2") + p3 := []byte("chain3") + p33 := []byte("chain33") + + e.Invoke(t, stackitem.Null{}, "addChain", policy.Namespace, "mynamespace", "ingress:123", p1) + checkChains(t, e, "mynamespace", "", "ingress", [][]byte{p1}) + checkChains(t, e, "mynamespace", "", "all", nil) + + e.Invoke(t, stackitem.Null{}, "addChain", policy.Container, "cnr1", "ingress:myrule2", p2) + checkChains(t, e, "mynamespace", "", "ingress", [][]byte{p1}) // Only namespace chains. + checkChains(t, e, "mynamespace", "cnr1", "ingress", [][]byte{p1, p2}) + checkChains(t, e, "mynamespace", "cnr1", "all", nil) // No chains attached to 'all'. + checkChains(t, e, "mynamespace", "cnr2", "ingress", [][]byte{p1}) // Only namespace, no chains for the container. + + e.Invoke(t, stackitem.Null{}, "addChain", policy.Container, "cnr1", "ingress:myrule3", p3) + checkChains(t, e, "mynamespace", "cnr1", "ingress", [][]byte{p1, p2, p3}) + + e.Invoke(t, stackitem.Null{}, "addChain", policy.Container, "cnr1", "ingress:myrule3", p33) + checkChains(t, e, "mynamespace", "cnr1", "ingress", [][]byte{p1, p2, p33}) // Override chain. + + t.Run("removal", func(t *testing.T) { + t.Run("wrong name", func(t *testing.T) { + e.Invoke(t, stackitem.Null{}, "removeChain", policy.Namespace, "mynamespace", "ingress") + checkChains(t, e, "mynamespace", "", "ingress", [][]byte{p1}) + }) + + e.Invoke(t, stackitem.Null{}, "removeChain", policy.Namespace, "mynamespace", "ingress:123") + checkChains(t, e, "mynamespace", "", "ingress", nil) + checkChains(t, e, "mynamespace", "cnr1", "ingress", [][]byte{p2, p33}) // Container chains still exist. + + // Remove by prefix. + e.Invoke(t, stackitem.Null{}, "removeChainsByPrefix", policy.Container, "cnr1", "ingress") + checkChains(t, e, "mynamespace", "cnr1", "ingress", nil) + }) +} + +func checkChains(t *testing.T, e *neotest.ContractInvoker, namespace, container, name string, expected [][]byte) { + s, err := e.TestInvoke(t, "listChains", namespace, container, name) + require.NoError(t, err) + require.Equal(t, 1, s.Len()) + + if len(expected) == 0 { + _, ok := s.Pop().Item().(stackitem.Null) + require.True(t, ok) + return + } + + var actual [][]byte + arr := s.Pop().Array() + for i := range arr { + bs, err := arr[i].TryBytes() + + require.NoError(t, err) + actual = append(actual, bs) + } + + require.ElementsMatch(t, expected, actual) +}