policy: Initial implementation #44

Merged
fyrchik merged 1 commit from fyrchik/frostfs-contract:policy-contract into master 2023-11-02 07:07:58 +00:00
6 changed files with 309 additions and 1 deletions

View file

@ -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)

2
policy/config.yml Normal file
View file

@ -0,0 +1,2 @@
name: "APE"
safemethods: ["listChains"]

12
policy/doc.go Normal file
View file

@ -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 |

The doc table lacks i prefix that is used for IAMs

The doc table lacks `i` prefix that is used for `IAM`s

Will fix, I have added IAM for discussion -- we might want to store these policies somewhere, they could be accessed with the same interface, but have different format.

Will fix, I have added IAM for discussion -- we might want to store these policies somewhere, they could be accessed with the same interface, but have different format.
*/
package policy

79
policy/policy_contract.go Normal file
View file

@ -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
}

124
rpcclient/policy/client.go Normal file
View file

@ -0,0 +1,124 @@
// Package ape contains RPC wrappers for APE contract.
//
// Code generated by neo-go contract generate-rpcwrapper --manifest <file.json> --out <file.go> [--hash <hash>] [--config <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)
}

91
tests/policy_test.go Normal file
View file

@ -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)
}