diff --git a/README.md b/README.md index 9e579b4..c3f7ac5 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,31 @@ # NeoFS smart-contract -This smart-contract controls list of NeoFS Inner Ring nodes and provides -methods to deposit and withdraw assets. These assets are used as a payment and -a reward for data storage. - +This smart-contract controls list of NeoFS Inner Ring nodes, user assets in +NeoFS balance contract and stores NeoFS runtime configuration. ## Getting Started This repository contains: -- NeoFS smart-contract written in Go -- Unit tests for the smart-contract +- NeoFS smart-contract in Go ### Prerequisites To compile smart-contract you need: -- [neo-go](https://github.com/nspcc-dev/neo-go) >= 0.74.0 - - -To run tests you need: - -- [go](https://golang.org/dl/) >= 1.12 +- [neo-go](https://github.com/nspcc-dev/neo-go) >= 0.90.0 ## Compiling 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 -neo-go contract compile -i neofs_contract.go -02a600c56b6a007bc46a517bc468164e656f2e52756e74696d652e476574547269676765726165880d9e640700006c756668164e656f2e53746f726167652e476574436f6e74657874616a527bc46a00c376064465706c6f798764c7016a52c30d496e6e657252696e674c6973747c657c0d6a537bc46a5 -3c3c000a0642f0019636f6e747261637420616c7265616479206465706c6f796564680f4e656f2e52756e74696d652e4c6f67f06a51c3c06a547bc46a54c35297009e6448003270726f76696465207061697273206f6620696e6e65722072696e67206164647265737320616e64207075626c6963206b65 -... -c46a00c36a59c37c6592fd6476006a52c36a5ac36a59c3ad6469006a58c38b6a587bc4006a5b7bc46a5bc36a57c3c09f6444006a57c36a5bc3c36a59c387642b00156475706c6963617465207075626c6963206b657973680f4e656f2e52756e74696d652e4c6f67f06a5bc38b6a5b7bc462b7ff6a57c36 -a59c3787cc86a577bc46a53c30161936a537bc46234ff6a58c36a56c3a2640700516c75661e6e6f7420656e6f756768207665726966696564207369676e617475726573680f4e656f2e52756e74696d652e4c6f6761006c7566 -$ ls neofs_contract.avm -neofs_contract.avm +$ make build +neo-go contract compile -i neofs_contract.go -c neofs_config.yml -m config.json +$ ls neofs_contract.nef config.json +config.json neofs_contract.nef ``` 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 ``` -## 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 This project is licensed under the GPLv3 License - see the diff --git a/neofs_contract_test.go b/neofs_contract_test.go deleted file mode 100644 index e9a2736..0000000 --- a/neofs_contract_test.go +++ /dev/null @@ -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]) - } -}