forked from TrueCloudLab/frostfs-contract
Merge pull request #14 from nspcc-dev/neo3/docs
Remove unused functions and update docs
This commit is contained in:
commit
5c2d82a0b6
3 changed files with 71 additions and 711 deletions
75
README.md
75
README.md
|
@ -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
|
||||||
|
|
|
@ -1,5 +1,42 @@
|
||||||
package smart_contract
|
package smart_contract
|
||||||
|
|
||||||
|
/*
|
||||||
|
NeoFS Smart Contract for NEO3.0.
|
||||||
|
|
||||||
|
Utility operations, executed once in deploy stage:
|
||||||
|
- Init(pubKey, ... ) - setup initial inner ring nodes
|
||||||
|
- InitConfig(key, value, key, value...) - setup initial NeoFS configuration
|
||||||
|
|
||||||
|
User operations:
|
||||||
|
- Deposit(script-hash, amount, script-hash(?)) - deposit gas assets to this script-hash address to NeoFS balance
|
||||||
|
- Withdraw(script-hash, amount) - initialize gas asset withdraw from NeoFS balance
|
||||||
|
- Bind(script-hash, pubKeys...) - bind public key with user's account to use it in NeoFS requests
|
||||||
|
- Unbind(script-hash, pubKeys...) - unbind public key from user's account
|
||||||
|
|
||||||
|
Inner ring list operations:
|
||||||
|
- InnerRingList() - returns array of inner ring node keys
|
||||||
|
- InnerRingCandidates() - returns array of inner ring candidate node keys
|
||||||
|
- IsInnerRing(pubKey) - returns 'true' if key is inside of inner ring list
|
||||||
|
- InnerRingCandidateAdd(pubKey) - adds key to the list of inner ring candidates
|
||||||
|
- InnerRingCandidateRemove(pubKey) - removes key from the list of inner ring candidates
|
||||||
|
- InnerRingUpdate(id, pubKeys...) - updates list of inner ring nodes with provided list of public keys
|
||||||
|
|
||||||
|
Config operations:
|
||||||
|
- Config(key) - returns value of NeoFS configuration with key 'key'
|
||||||
|
- ListConfig() - returns array of all key-value pairs of NeoFS configuration
|
||||||
|
- SetConfig(id, key, value) - set key-value pair as a NeoFS runtime configuration value
|
||||||
|
|
||||||
|
Other utility operations:
|
||||||
|
- Version - returns contract version
|
||||||
|
- Cheque(id, script- hash, amount, script-hash) - sends gas assets back to the user if they were successfully
|
||||||
|
locked in NeoFS balance contract
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- (?) - parameter can be omitted
|
||||||
|
- pubKey - 33 bytes of public key
|
||||||
|
- id - unique byte sequence
|
||||||
|
*/
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/interop/binary"
|
"github.com/nspcc-dev/neo-go/pkg/interop/binary"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/interop/blockchain"
|
"github.com/nspcc-dev/neo-go/pkg/interop/blockchain"
|
||||||
|
@ -34,17 +71,22 @@ type (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
tokenHash = "\x3b\x7d\x37\x11\xc6\xf0\xcc\xf9\xb1\xdc\xa9\x03\xd1\xbf\xa1\xd8\x96\xf1\x23\x8c"
|
// native gas token script hash
|
||||||
|
tokenHash = "\x3b\x7d\x37\x11\xc6\xf0\xcc\xf9\xb1\xdc\xa9\x03\xd1\xbf\xa1\xd8\x96\xf1\x23\x8c"
|
||||||
|
|
||||||
defaultCandidateFee = 100 * 1_0000_0000 // 100 Fixed8 Gas
|
defaultCandidateFee = 100 * 1_0000_0000 // 100 Fixed8 Gas
|
||||||
candidateFeeConfigKey = "InnerRingCandidateFee"
|
candidateFeeConfigKey = "InnerRingCandidateFee"
|
||||||
version = 2
|
|
||||||
innerRingKey = "innerring"
|
version = 2
|
||||||
voteKey = "ballots"
|
|
||||||
candidatesKey = "candidates"
|
innerRingKey = "innerring"
|
||||||
cashedChequesKey = "cheques"
|
voteKey = "ballots"
|
||||||
blockDiff = 20 // change base on performance evaluation
|
candidatesKey = "candidates"
|
||||||
publicKeySize = 33
|
cashedChequesKey = "cheques"
|
||||||
minInnerRingSize = 3
|
|
||||||
|
blockDiff = 20 // change base on performance evaluation
|
||||||
|
publicKeySize = 33
|
||||||
|
minInnerRingSize = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -58,29 +100,6 @@ func Main(op string, args []interface{}) interface{} {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
Utility operations - they will be changed in production:
|
|
||||||
- Deploy(params: address, pubKey, ... ) - setup initial inner ring state
|
|
||||||
|
|
||||||
User operations:
|
|
||||||
- InnerRingList() - get list of inner ring nodes addresses and public keys
|
|
||||||
- InnerRingCandidateRemove(params: pubKey) - remove node with given public key from the inner ring candidate queue
|
|
||||||
- InnerRingCandidateAdd(params: pubKey) - add node to the inner ring candidate queue
|
|
||||||
- Deposit(params: pubKey, amount) - deposit GAS to the NeoFS account
|
|
||||||
- Withdraw(params: withdrawCheque) - withdraw GAS from the NeoFS account
|
|
||||||
- InnerRingUpdate(params: irCheque) - change list of inner ring nodes
|
|
||||||
- IsInnerRing(params: pubKey) - returns true if pubKey presented in inner ring list
|
|
||||||
- Version() - get version of the NeoFS smart-contract
|
|
||||||
|
|
||||||
Params:
|
|
||||||
- address - string of the valid multiaddress (github.com/multiformats/multiaddr)
|
|
||||||
- pubKey - 33 byte public key
|
|
||||||
- withdrawCheque - serialized structure, that confirms GAS transfer;
|
|
||||||
contains inner ring signatures
|
|
||||||
- irCheque - serialized structure, that confirms new inner ring node list;
|
|
||||||
contains inner ring signatures
|
|
||||||
*/
|
|
||||||
|
|
||||||
ctx := storage.GetContext()
|
ctx := storage.GetContext()
|
||||||
|
|
||||||
switch op {
|
switch op {
|
||||||
|
@ -277,6 +296,7 @@ func Main(op string, args []interface{}) interface{} {
|
||||||
}
|
}
|
||||||
|
|
||||||
var keys [][]byte
|
var keys [][]byte
|
||||||
|
|
||||||
for i := 1; i < len(args); i++ {
|
for i := 1; i < len(args); i++ {
|
||||||
pub := args[i].([]byte)
|
pub := args[i].([]byte)
|
||||||
if len(pub) != publicKeySize {
|
if len(pub) != publicKeySize {
|
||||||
|
@ -367,7 +387,7 @@ func Main(op string, args []interface{}) interface{} {
|
||||||
}
|
}
|
||||||
|
|
||||||
key := args[0].([]byte)
|
key := args[0].([]byte)
|
||||||
if len(key) != 33 {
|
if len(key) != publicKeySize {
|
||||||
panic("isInnerRing: incorrect public key")
|
panic("isInnerRing: incorrect public key")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -473,119 +493,6 @@ func Main(op string, args []interface{}) interface{} {
|
||||||
panic("unknown operation")
|
panic("unknown operation")
|
||||||
}
|
}
|
||||||
|
|
||||||
// fixme: use strict type deserialization wrappers
|
|
||||||
func getSerialized(ctx storage.Context, key string) interface{} {
|
|
||||||
data := storage.Get(ctx, key).([]byte)
|
|
||||||
if len(data) != 0 {
|
|
||||||
return binary.Deserialize(data)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func delSerialized(ctx storage.Context, key string, value []byte) bool {
|
|
||||||
data := storage.Get(ctx, key).([]byte)
|
|
||||||
deleted := false
|
|
||||||
|
|
||||||
var newList [][]byte
|
|
||||||
if len(data) != 0 {
|
|
||||||
lst := binary.Deserialize(data).([][]byte)
|
|
||||||
for i := 0; i < len(lst); i++ {
|
|
||||||
if util.Equals(value, lst[i]) {
|
|
||||||
deleted = true
|
|
||||||
} else {
|
|
||||||
newList = append(newList, lst[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if deleted {
|
|
||||||
if len(newList) != 0 {
|
|
||||||
data := binary.Serialize(newList)
|
|
||||||
storage.Put(ctx, key, data)
|
|
||||||
} else {
|
|
||||||
storage.Delete(ctx, key)
|
|
||||||
}
|
|
||||||
runtime.Log("target element has been removed")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
runtime.Log("target element has not been removed")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func putSerialized(ctx storage.Context, key string, value interface{}) bool {
|
|
||||||
data := storage.Get(ctx, key).([]byte)
|
|
||||||
|
|
||||||
var lst []interface{}
|
|
||||||
if len(data) != 0 {
|
|
||||||
lst = binary.Deserialize(data).([]interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
lst = append(lst, value)
|
|
||||||
data = binary.Serialize(lst)
|
|
||||||
storage.Put(ctx, key, data)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func pubToScriptHash(pkey []byte) []byte {
|
|
||||||
// pre := []byte{0x21}
|
|
||||||
// buf := append(pre, pkey...)
|
|
||||||
// buf = append(buf, 0xac)
|
|
||||||
// h := crypto.Hash160(buf)
|
|
||||||
//
|
|
||||||
// return h
|
|
||||||
|
|
||||||
// fixme: someday ripemd syscall will appear
|
|
||||||
// or simply store script-hashes along with public key
|
|
||||||
return []byte{0x0F, 0xED}
|
|
||||||
}
|
|
||||||
|
|
||||||
func containsCheck(lst []cheque, c cheque) bool {
|
|
||||||
for i := 0; i < len(lst); i++ {
|
|
||||||
if util.Equals(c, lst[i]) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
func containsPub(lst []node, elem []byte) bool {
|
|
||||||
for i := 0; i < len(lst); i++ {
|
|
||||||
e := lst[i]
|
|
||||||
if util.Equals(elem, e.pub) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func delSerializedIR(ctx storage.Context, key string, value []byte) bool {
|
|
||||||
data := storage.Get(ctx, key).([]byte)
|
|
||||||
deleted := false
|
|
||||||
|
|
||||||
newList := []node{}
|
|
||||||
if len(data) != 0 {
|
|
||||||
lst := binary.Deserialize(data).([]node)
|
|
||||||
for i := 0; i < len(lst); i++ {
|
|
||||||
n := lst[i]
|
|
||||||
if util.Equals(value, n.pub) {
|
|
||||||
deleted = true
|
|
||||||
} else {
|
|
||||||
newList = append(newList, n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if deleted {
|
|
||||||
data := binary.Serialize(newList)
|
|
||||||
storage.Put(ctx, key, data)
|
|
||||||
runtime.Log("target element has been removed")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runtime.Log("target element has not been removed")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// innerRingInvoker returns public key of inner ring node that invoked contract.
|
// innerRingInvoker returns public key of inner ring node that invoked contract.
|
||||||
func innerRingInvoker(ir []node) []byte {
|
func innerRingInvoker(ir []node) []byte {
|
||||||
for i := 0; i < len(ir); i++ {
|
for i := 0; i < len(ir); i++ {
|
||||||
|
@ -598,9 +505,11 @@ func innerRingInvoker(ir []node) []byte {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// vote adds ballot for the decision with specific 'id' and returns amount
|
||||||
|
// on unique voters for that decision.
|
||||||
func vote(ctx storage.Context, id, from []byte) int {
|
func vote(ctx storage.Context, id, from []byte) int {
|
||||||
var (
|
var (
|
||||||
newCandidates []ballot
|
newCandidates = []ballot{} // it is explicit declaration of empty slice, not nil
|
||||||
candidates = getBallots(ctx)
|
candidates = getBallots(ctx)
|
||||||
found = -1
|
found = -1
|
||||||
blockHeight = blockchain.GetHeight()
|
blockHeight = blockchain.GetHeight()
|
||||||
|
@ -643,9 +552,11 @@ func vote(ctx storage.Context, id, from []byte) int {
|
||||||
return found
|
return found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// removeVotes clears ballots of the decision that has benn aceepted by
|
||||||
|
// inner ring nodes.
|
||||||
func removeVotes(ctx storage.Context, id []byte) {
|
func removeVotes(ctx storage.Context, id []byte) {
|
||||||
var (
|
var (
|
||||||
newCandidates []ballot
|
newCandidates = []ballot{} // it is explicit declaration of empty slice, not nil
|
||||||
candidates = getBallots(ctx)
|
candidates = getBallots(ctx)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -721,6 +632,7 @@ func addCheque(lst []cheque, c cheque) ([]cheque, bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
lst = append(lst, c)
|
lst = append(lst, c)
|
||||||
|
|
||||||
return lst, true
|
return lst, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -734,6 +646,7 @@ func addNode(lst []node, n node) ([]node, bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
lst = append(lst, n)
|
lst = append(lst, n)
|
||||||
|
|
||||||
return lst, true
|
return lst, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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])
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue