policy: Initial implementation #44
6 changed files with 309 additions and 1 deletions
2
Makefile
2
Makefile
|
@ -22,7 +22,7 @@ all: sidechain mainnet
|
||||||
sidechain: alphabet morph nns
|
sidechain: alphabet morph nns
|
||||||
|
|
||||||
alphabet_sc = alphabet
|
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
|
mainnet_sc = frostfs processing
|
||||||
nns_sc = nns
|
nns_sc = nns
|
||||||
all_sc = $(alphabet_sc) $(morph_sc) $(mainnet_sc) $(nns_sc)
|
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