[#44] policy: Initial implementation
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
This commit is contained in:
parent
901d5a4083
commit
9ed3845aa9
6 changed files with 309 additions and 1 deletions
2
Makefile
2
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)
|
||||
|
|
2
policy/config.yml
Normal file
2
policy/config.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
name: "APE"
|
||||
safemethods: ["listChains"]
|
12
policy/doc.go
Normal file
12
policy/doc.go
Normal 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 |
|
||||
|
||||
*/
|
||||
|
||||
package policy
|
79
policy/policy_contract.go
Normal file
79
policy/policy_contract.go
Normal 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
124
rpcclient/policy/client.go
Normal 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
91
tests/policy_test.go
Normal 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)
|
||||
}
|
Loading…
Reference in a new issue