Remove tests and update readme file

This commit is contained in:
alexvanin 2020-07-20 16:14:40 +03:00
parent 013a70f3bf
commit 8460ab01e4
2 changed files with 10 additions and 563 deletions

View file

@ -1,43 +1,31 @@
# NeoFS smart-contract # NeoFS smart-contract
This smart-contract controls list of NeoFS Inner Ring nodes and provides This smart-contract controls list of NeoFS Inner Ring nodes, user assets in
methods to deposit and withdraw assets. These assets are used as a payment and NeoFS balance contract and stores NeoFS runtime configuration.
a reward for data storage.
## Getting Started ## Getting Started
This repository contains: This repository contains:
- NeoFS smart-contract written in Go - NeoFS smart-contract in Go
- Unit tests for the smart-contract
### Prerequisites ### Prerequisites
To compile smart-contract you need: To compile smart-contract you need:
- [neo-go](https://github.com/nspcc-dev/neo-go) >= 0.74.0 - [neo-go](https://github.com/nspcc-dev/neo-go) >= 0.90.0
To run tests you need:
- [go](https://golang.org/dl/) >= 1.12
## Compiling ## Compiling
To compile smart contract run `make build` command. Compiled contract To compile smart contract run `make build` command. Compiled contract
`neofs_contract.avm` will be placed in the same directory. `neofs_contract.nef` and manifest `config.json` will be placed in the same
directory.
``` ```
$ make build $ make build
neo-go contract compile -i neofs_contract.go neo-go contract compile -i neofs_contract.go -c neofs_config.yml -m config.json
02a600c56b6a007bc46a517bc468164e656f2e52756e74696d652e476574547269676765726165880d9e640700006c756668164e656f2e53746f726167652e476574436f6e74657874616a527bc46a00c376064465706c6f798764c7016a52c30d496e6e657252696e674c6973747c657c0d6a537bc46a5 $ ls neofs_contract.nef config.json
3c3c000a0642f0019636f6e747261637420616c7265616479206465706c6f796564680f4e656f2e52756e74696d652e4c6f67f06a51c3c06a547bc46a54c35297009e6448003270726f76696465207061697273206f6620696e6e65722072696e67206164647265737320616e64207075626c6963206b65 config.json neofs_contract.nef
...
c46a00c36a59c37c6592fd6476006a52c36a5ac36a59c3ad6469006a58c38b6a587bc4006a5b7bc46a5bc36a57c3c09f6444006a57c36a5bc3c36a59c387642b00156475706c6963617465207075626c6963206b657973680f4e656f2e52756e74696d652e4c6f67f06a5bc38b6a5b7bc462b7ff6a57c36
a59c3787cc86a577bc46a53c30161936a537bc46234ff6a58c36a56c3a2640700516c75661e6e6f7420656e6f756768207665726966696564207369676e617475726573680f4e656f2e52756e74696d652e4c6f6761006c7566
$ ls neofs_contract.avm
neofs_contract.avm
``` ```
You can specify path to the `neo-go` binary with `NEOGO` environment variable: You can specify path to the `neo-go` binary with `NEOGO` environment variable:
@ -46,51 +34,6 @@ You can specify path to the `neo-go` binary with `NEOGO` environment variable:
$ NEOGO=/home/user/neo-go/bin/neo-go make build $ NEOGO=/home/user/neo-go/bin/neo-go make build
``` ```
## Running the tests
`neofs_contract_test.go` file contains tests for most of the provided methods.
It compiles smart-contract and uses instance of the NeoVM to run
code.
To test smart contract run `make tests` command.
```
$ make tests
go mod vendor
go test -mod=vendor -v -race github.com/nspcc-dev/neofs-contract
=== RUN TestContract
TestContract: neofs_contract_test.go:360: provide pairs of inner ring address and public key
TestContract: neofs_contract_test.go:360: contract already deployed
=== RUN TestContract/InnerRingAddress
TestContract: neofs_contract_test.go:360: target element has been removed
=== RUN TestContract/Deposit
TestContract: neofs_contract_test.go:360: funds have been transfered
=== RUN TestContract/Withdraw
=== RUN TestContract/Withdraw/Double_Withdraw
TestContract: neofs_contract_test.go:360: verification check has already been used
=== RUN TestContract/InnerRingCandidateAdd
=== RUN TestContract/InnerRingCandidateAdd/Double_InnerRingCandidateAdd
TestContract: neofs_contract_test.go:360: is already in list
=== RUN TestContract/InnerRingCandidateRemove
=== RUN TestContract/InnerRingCandidateRemove/Remove_unknown_candidate
TestContract: neofs_contract_test.go:360: target element has not been removed
TestContract: neofs_contract_test.go:360: target element has not been removed
=== RUN TestContract/InnerRingUpdate
TestContract/InnerRingUpdate: neofs_contract_test.go:174: implement getIRExcludeCheque without neofs-node dependency
--- PASS: TestContract (0.43s)
--- PASS: TestContract/InnerRingAddress (0.00s)
--- PASS: TestContract/Deposit (0.00s)
--- PASS: TestContract/Withdraw (0.01s)
--- PASS: TestContract/Withdraw/Double_Withdraw (0.00s)
--- PASS: TestContract/InnerRingCandidateAdd (0.00s)
--- PASS: TestContract/InnerRingCandidateAdd/Double_InnerRingCandidateAdd (0.00s)
--- PASS: TestContract/InnerRingCandidateRemove (0.00s)
--- PASS: TestContract/InnerRingCandidateRemove/Remove_unknown_candidate (0.00s)
--- SKIP: TestContract/InnerRingUpdate (0.00s)
PASS
ok github.com/nspcc-dev/neofs-contract 0.453s
```
## License ## License
This project is licensed under the GPLv3 License - see the This project is licensed under the GPLv3 License - see the

View file

@ -1,496 +0,0 @@
package smart_contract
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/hex"
"fmt"
"math/big"
"os"
"testing"
"github.com/nspcc-dev/neo-go/pkg/compiler"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
crypto "github.com/nspcc-dev/neofs-crypto"
"github.com/nspcc-dev/neofs-crypto/test"
"github.com/stretchr/testify/require"
)
const contractTemplate = "./neofs_contract.go"
var (
contractHash = util.Uint160{0x1, 0x2, 0x3, 0x4}
// token hash is not random to run tests of .avm or .go files
contractStr = string(contractHash[:])
txHash = mustHex("3ca2575bd90129e3730c46ba3f163fcfd5fff11eaedb2b6aa3d76bd03ab8a890")
)
type contract struct {
script []byte
privs []*ecdsa.PrivateKey
cgasHash string
}
func TestContract(t *testing.T) {
const nodeCount = 6
plug := newStoragePlugin(t)
contract := initGoContract(t, contractTemplate, nodeCount)
plug.cgas[contractStr] = util.Fixed8FromInt64(1000)
plug.invokeKey = crypto.MarshalPublicKey(&contract.privs[0].PublicKey)
var args []interface{}
for i := range contract.privs {
args = append(args, crypto.MarshalPublicKey(&contract.privs[i].PublicKey))
}
v := initVM(contract, plug)
loadArg(t, v, "Deploy", args)
require.NoError(t, v.Run())
// double deploy
v = initVM(contract, plug)
loadArg(t, v, "Deploy", args)
require.Error(t, v.Run())
t.Run("Deposit", func(t *testing.T) {
const (
amount = 1000
balance = 4000
)
before := plug.cgas[contractStr]
gas := util.Fixed8FromInt64(amount)
key, err := keys.NewPublicKeyFromString("031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a")
require.NoError(t, err)
plug.setCGASBalance(key.Bytes(), balance)
v := initVM(contract, plug)
loadArg(t, v, "Deposit", []interface{}{key.Bytes(), int(gas.IntegralValue())})
require.NoError(t, v.Run())
require.Equal(t, before+gas, plug.cgas[contractStr])
require.Equal(t, util.Fixed8FromInt64(balance-amount), plug.cgas[string(key.GetScriptHash().BytesBE())])
checkNotification(t, plug.notify, []byte("Deposit"), key.Bytes(), big.NewInt(int64(gas)), []byte{}, txHash)
})
t.Run("Withdraw", func(t *testing.T) {
const amount = 21
gas := util.Fixed8FromInt64(amount)
key, err := keys.NewPublicKeyFromString("031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a")
require.NoError(t, err)
v := initVM(contract, plug)
loadArg(t, v, "Withdraw", []interface{}{key.Bytes(), amount})
require.NoError(t, v.Run())
checkNotification(t, plug.notify, []byte("Withdraw"), key.Bytes(), big.NewInt(int64(gas)), txHash)
})
t.Run("Cheque", func(t *testing.T) {
const amount = 21
id := []byte("id")
gas := util.Fixed8FromInt64(amount)
user := randScriptHash()
lockAcc := randScriptHash()
contractGas := plug.cgas[contractStr]
// call it threshold amount of times
for i := 0; i < 2*nodeCount/3+1; i++ {
plug.invokeKey = crypto.MarshalPublicKey(&contract.privs[i].PublicKey)
v := initVM(contract, plug)
loadArg(t, v, "Cheque", []interface{}{id, user, int(gas), lockAcc})
require.NoError(t, v.Run())
}
require.Equal(t, contractGas-gas, plug.cgas[contractStr])
require.Equal(t, gas, plug.cgas[string(user)])
checkNotification(t, plug.notify, []byte("Cheque"), id, user, big.NewInt(int64(gas)), lockAcc)
t.Run("Double cheque", func(t *testing.T) {
v := initVM(contract, plug)
loadArg(t, v, "Cheque", []interface{}{id, user, int(gas), lockAcc})
require.Error(t, v.Run())
})
})
t.Run("InnerRingCandidateAdd", func(t *testing.T) {
v := initVM(contract, plug)
before := plug.cgas[contractStr]
key := crypto.MarshalPublicKey(&test.DecodeKey(1).PublicKey)
plug.setCGASBalance(key, 4000)
loadArg(t, v, "InnerRingCandidateAdd", []interface{}{key})
require.NoError(t, v.Run())
fee := util.Fixed8FromInt64(1)
require.Equal(t, before+fee, plug.cgas[contractStr])
require.Equal(t, util.Fixed8FromInt64(4000)-fee,
plug.cgas[string(mustPKey(key).GetScriptHash().BytesBE())])
require.True(t, bytes.Contains(plug.mem["InnerRingCandidates"], key))
t.Run("Double InnerRingCandidateAdd", func(t *testing.T) {
v := initVM(contract, plug)
loadArg(t, v, "InnerRingCandidateAdd", []interface{}{key})
require.Error(t, v.Run())
})
})
t.Run("InnerRingCandidateRemove", func(t *testing.T) {
key := crypto.MarshalPublicKey(&test.DecodeKey(2).PublicKey)
plug.setCGASBalance(key, 4000)
v := initVM(contract, plug)
loadArg(t, v, "InnerRingCandidateAdd", []interface{}{key})
require.NoError(t, v.Run())
require.True(t, bytes.Contains(plug.mem["InnerRingCandidates"], key))
t.Run("Remove unknown candidate", func(t *testing.T) {
v := initVM(contract, plug)
// unknown candidate
badKey := crypto.MarshalPublicKey(&test.DecodeKey(3).PublicKey)
loadArg(t, v, "InnerRingCandidateRemove", []interface{}{badKey})
require.NoError(t, v.Run())
require.True(t, bytes.Contains(plug.mem["InnerRingCandidates"], key))
})
v = initVM(contract, plug)
key = mustHex("031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a")
loadArg(t, v, "InnerRingCandidateRemove", []interface{}{key})
require.NoError(t, v.Run())
require.False(t, bytes.Contains(plug.mem["InnerRingCandidates"], key))
})
}
func initGoContract(t *testing.T, path string, n int) *contract {
f, err := os.Open(path)
require.NoError(t, err)
defer f.Close()
buf, err := compiler.Compile(f)
require.NoError(t, err)
return &contract{script: buf, privs: getKeys(t, n)}
}
func getKeys(t *testing.T, n int) []*ecdsa.PrivateKey {
privs := make([]*ecdsa.PrivateKey, n)
for i := range privs {
var err error
privs[i], err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
}
return privs
}
func randScriptHash() []byte {
var scriptHash = make([]byte, 20)
rand.Read(scriptHash)
return scriptHash
}
func mustHex(s string) []byte {
result, err := hex.DecodeString(s)
if err != nil {
panic(fmt.Errorf("invalid hex: %v", err))
}
return result
}
func initVM(c *contract, plug *storagePlugin) *vm.VM {
v := vm.New()
v.Load(c.script)
v.SetScriptGetter(plug.getScript)
v.RegisterInteropGetter(plug.getInterop)
return v
}
func loadArg(t *testing.T, v *vm.VM, operation string, params []interface{}) {
arr := make([]vm.StackItem, len(params))
for i := range arr {
arr[i] = toStackItem(params[i])
require.NotNil(t, arr[i], "invalid stack item")
}
v.Estack().PushVal(vm.NewArrayItem(arr))
v.Estack().PushVal(operation)
}
func toStackItem(v interface{}) vm.StackItem {
switch val := v.(type) {
case int:
return vm.NewBigIntegerItem(int64(val))
case string:
return vm.NewByteArrayItem([]byte(val))
case []byte:
return vm.NewByteArrayItem(val)
default:
return nil
}
}
const cgasSyscall = "MockCGAS"
type kv struct {
Operation string
Key []byte
Value []byte
}
type storagePlugin struct {
mem map[string][]byte
cgas map[string]util.Fixed8
interops map[uint32]vm.InteropFunc
storageOps []kv
notify []interface{}
invokeKey []byte
}
func newStoragePlugin(t *testing.T) *storagePlugin {
s := &storagePlugin{
mem: make(map[string][]byte),
cgas: make(map[string]util.Fixed8),
interops: make(map[uint32]vm.InteropFunc),
}
s.interops[getID("Neo.Storage.Delete")] = s.Delete
s.interops[getID("Neo.Storage.Get")] = s.Get
s.interops[getID("Neo.Storage.GetContext")] = s.GetContext
s.interops[getID("Neo.Storage.Put")] = s.Put
s.interops[getID("Neo.Runtime.GetExecutingScriptHash")] = s.GetExecutingScriptHash
s.interops[getID("Neo.Runtime.GetTrigger")] = s.GetTrigger
s.interops[getID("Neo.Runtime.CheckWitness")] = s.CheckWitness
s.interops[getID("System.ExecutionEngine.GetExecutingScriptHash")] = s.GetExecutingScriptHash
s.interops[getID(cgasSyscall)] = s.CGASInvoke
s.interops[getID("Neo.Runtime.Log")] = func(v *vm.VM) error {
msg := string(v.Estack().Pop().Bytes())
t.Log(msg)
return nil
}
s.interops[getID("Neo.Runtime.Notify")] = func(v *vm.VM) error {
val := v.Estack().Pop().Value()
s.notify = append(s.notify, toInterface(val))
return nil
}
s.interops[getID("System.ExecutionEngine.GetScriptContainer")] = s.GetScriptContainer
s.interops[getID("Neo.Transaction.GetHash")] = s.GetHash
s.interops[getID("Neo.Blockchain.GetHeight")] = s.GetHeight
return s
}
func toInterface(val interface{}) interface{} {
switch v := val.(type) {
case []vm.StackItem:
arr := make([]interface{}, len(v))
for i, item := range v {
arr[i] = toInterface(item)
}
return arr
case vm.StackItem:
return toInterface(v.Value())
default:
return v
}
}
func getID(name string) uint32 {
return vm.InteropNameToID([]byte(name))
}
func (s *storagePlugin) getInterop(id uint32) *vm.InteropFuncPrice {
f := s.interops[id]
if f != nil {
return &vm.InteropFuncPrice{Func: f, Price: 1}
}
switch id {
case getID("Neo.Runtime.Serialize"):
case getID("Neo.Runtime.Deserialize"):
default:
panic("unexpected interop")
}
return nil
}
func mustPKey(pub []byte) *keys.PublicKey {
var pk keys.PublicKey
if err := pk.DecodeBytes(pub); err != nil {
panic(err)
}
return &pk
}
func (s *storagePlugin) setCGASBalance(pub []byte, amount int64) {
pk := mustPKey(pub)
from := string(pk.GetScriptHash().BytesBE())
s.cgas[from] = util.Fixed8FromInt64(amount)
}
func (s *storagePlugin) CGASInvoke(v *vm.VM) error {
op := string(v.Estack().Pop().Bytes())
args := v.Estack().Pop().Array()
var result bool
switch op {
case "transfer":
from := args[0].Value().([]byte)
to := args[1].Value().([]byte)
if len(from) != 20 || len(to) != 20 {
panic("invalid arguments")
}
var amount util.Fixed8
val := args[2].Value()
switch v := val.(type) {
case *big.Int:
amount = util.Fixed8(v.Int64())
case []byte:
amount = util.Fixed8(emit.BytesToInt(v).Int64())
default:
panic("invalid amount")
}
if s.cgas[string(from)] >= amount {
s.cgas[string(from)] -= amount
s.cgas[string(to)] += amount
result = true
}
default:
panic("invalid operation")
}
v.Estack().PushVal(result)
return nil
}
func (s *storagePlugin) getScript(u util.Uint160) ([]byte, bool) {
var realHash util.Uint160
copy(realHash[:], tokenHash[:])
if u.Equals(realHash) {
buf := io.NewBufBinWriter()
emit.Syscall(buf.BinWriter, cgasSyscall)
return buf.Bytes(), false
}
panic("wrong script hash")
}
func (s *storagePlugin) GetTrigger(v *vm.VM) error {
// todo: remove byte casting when neo-go issue will be resolved
// https: //github.com/nspcc-dev/neo-go/issues/776
v.Estack().PushVal(byte(trigger.Application))
return nil
}
func (s *storagePlugin) CheckWitness(v *vm.VM) error {
key := v.Estack().Pop().Value().([]byte)
if bytes.Equal(key, s.invokeKey) {
v.Estack().PushVal(true)
} else {
v.Estack().PushVal(false)
}
return nil
}
func (s *storagePlugin) GetExecutingScriptHash(v *vm.VM) error {
var h util.Uint160
copy(h[:], contractHash[:])
v.Estack().PushVal(h.BytesBE())
return nil
}
func (s *storagePlugin) GetScriptContainer(v *vm.VM) error {
v.Estack().PushVal(true)
return nil
}
func (s *storagePlugin) GetHash(v *vm.VM) error {
v.Estack().PushVal(txHash)
return nil
}
func (s *storagePlugin) GetHeight(v *vm.VM) error {
v.Estack().PushVal(42)
return nil
}
func (s *storagePlugin) logStorage(op string, key, value []byte) {
s.storageOps = append(s.storageOps, kv{
Operation: op,
Key: key,
Value: value,
})
}
func (s *storagePlugin) Delete(v *vm.VM) error {
v.Estack().Pop()
key := v.Estack().Pop().Bytes()
s.logStorage("Delete", key, s.mem[string(key)])
delete(s.mem, string(key))
return nil
}
func (s *storagePlugin) Put(v *vm.VM) error {
v.Estack().Pop()
key := v.Estack().Pop().Bytes()
value := v.Estack().Pop().Bytes()
s.logStorage("Put", key, value)
s.mem[string(key)] = value
return nil
}
func (s *storagePlugin) Get(v *vm.VM) error {
v.Estack().Pop()
item := v.Estack().Pop().Bytes()
if val, ok := s.mem[string(item)]; ok {
v.Estack().PushVal(val)
s.logStorage("Get", item, val)
return nil
}
v.Estack().PushVal([]byte{})
s.logStorage("Get", item, nil)
return nil
}
func (s *storagePlugin) GetContext(v *vm.VM) error {
// Pushing anything on the stack here will work. This is just to satisfy
// the compiler, thinking it has pushed the context ^^.
v.Estack().PushVal(10)
return nil
}
func checkNotification(t *testing.T, store []interface{}, args ...interface{}) {
ln := len(store)
require.True(t, ln > 0)
notification := store[ln-1].([]interface{})
require.Equal(t, len(args), len(notification))
for i := range args {
require.Equal(t, args[i], notification[i])
}
}