mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-11-26 09:42:22 +00:00
nativetest: migrate Oracle contract tests to neotest
This commit is contained in:
parent
0e1f85b2bf
commit
df6a7d4258
6 changed files with 366 additions and 266 deletions
205
pkg/core/native/native_test/oracle_test.go
Normal file
205
pkg/core/native/native_test/oracle_test.go
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
package native_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"math"
|
||||||
|
"math/big"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/native"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/neotest"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/nef"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newOracleClient(t *testing.T) *neotest.ContractInvoker {
|
||||||
|
return newNativeClient(t, nativenames.Oracle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSetPrice(t *testing.T) {
|
||||||
|
testGetSet(t, newOracleClient(t), "Price", native.DefaultOracleRequestPrice, 1, math.MaxInt64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getOracleContractState reads pre-compiled oracle contract generated by
|
||||||
|
// TestGenerateOracleContract and returns its state.
|
||||||
|
func getOracleContractState(t *testing.T, sender util.Uint160, id int32) *state.Contract {
|
||||||
|
var (
|
||||||
|
oracleContractNEFPath = filepath.Join("..", "..", "test_data", "oracle_contract", "oracle.nef")
|
||||||
|
oracleContractManifestPath = filepath.Join("..", "..", "test_data", "oracle_contract", "oracle.manifest.json")
|
||||||
|
)
|
||||||
|
errNotFound := errors.New("auto-generated oracle contract is not found, use TestGenerateOracleContract to regenerate")
|
||||||
|
|
||||||
|
neBytes, err := ioutil.ReadFile(oracleContractNEFPath)
|
||||||
|
require.NoError(t, err, fmt.Errorf("nef: %w", errNotFound))
|
||||||
|
ne, err := nef.FileFromBytes(neBytes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
mBytes, err := ioutil.ReadFile(oracleContractManifestPath)
|
||||||
|
require.NoError(t, err, fmt.Errorf("manifest: %w", errNotFound))
|
||||||
|
m := &manifest.Manifest{}
|
||||||
|
err = json.Unmarshal(mBytes, m)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return &state.Contract{
|
||||||
|
ContractBase: state.ContractBase{
|
||||||
|
NEF: ne,
|
||||||
|
Hash: state.CreateContractHash(sender, ne.Checksum, m.Name),
|
||||||
|
Manifest: *m,
|
||||||
|
ID: id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func putOracleRequest(t *testing.T, oracleInvoker *neotest.ContractInvoker,
|
||||||
|
url string, filter *string, cb string, userData []byte, gas int64, errStr ...string) {
|
||||||
|
var filtItem interface{}
|
||||||
|
if filter != nil {
|
||||||
|
filtItem = *filter
|
||||||
|
}
|
||||||
|
if len(errStr) == 0 {
|
||||||
|
oracleInvoker.Invoke(t, stackitem.Null{}, "requestURL", url, filtItem, cb, userData, gas)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
oracleInvoker.InvokeFail(t, errStr[0], "requestURL", url, filtItem, cb, userData, gas)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOracle_Request(t *testing.T) {
|
||||||
|
oracleCommitteeInvoker := newOracleClient(t)
|
||||||
|
e := oracleCommitteeInvoker.Executor
|
||||||
|
managementCommitteeInvoker := e.CommitteeInvoker(e.NativeHash(t, nativenames.Management))
|
||||||
|
designationCommitteeInvoker := e.CommitteeInvoker(e.NativeHash(t, nativenames.Designation))
|
||||||
|
gasCommitteeInvoker := e.CommitteeInvoker(e.NativeHash(t, nativenames.Gas))
|
||||||
|
|
||||||
|
cs := getOracleContractState(t, e.Validator.ScriptHash(), 1)
|
||||||
|
nBytes, err := cs.NEF.Bytes()
|
||||||
|
require.NoError(t, err)
|
||||||
|
mBytes, err := json.Marshal(cs.Manifest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
expected, err := cs.ToStackItem()
|
||||||
|
require.NoError(t, err)
|
||||||
|
managementCommitteeInvoker.Invoke(t, expected, "deploy", nBytes, mBytes)
|
||||||
|
helperValidatorInvoker := e.ValidatorInvoker(cs.Hash)
|
||||||
|
|
||||||
|
gasForResponse := int64(2000_1234)
|
||||||
|
var filter = "flt"
|
||||||
|
userData := []byte("custom info")
|
||||||
|
putOracleRequest(t, helperValidatorInvoker, "url", &filter, "handle", userData, gasForResponse)
|
||||||
|
|
||||||
|
// Designate single Oracle node.
|
||||||
|
oracleNode := e.NewAccount(t)
|
||||||
|
designationCommitteeInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", int(noderoles.Oracle), []interface{}{oracleNode.(neotest.SingleSigner).Account().PrivateKey().PublicKey().Bytes()})
|
||||||
|
err = oracleNode.(neotest.SingleSigner).Account().ConvertMultisig(1, []*keys.PublicKey{oracleNode.(neotest.SingleSigner).Account().PrivateKey().PublicKey()})
|
||||||
|
require.NoError(t, err)
|
||||||
|
oracleNodeMulti := neotest.NewMultiSigner(oracleNode.(neotest.SingleSigner).Account())
|
||||||
|
gasCommitteeInvoker.Invoke(t, true, "transfer", gasCommitteeInvoker.CommitteeHash, oracleNodeMulti.ScriptHash(), 100_0000_0000, nil)
|
||||||
|
|
||||||
|
// Finish.
|
||||||
|
prepareResponseTx := func(t *testing.T, requestID uint64) *transaction.Transaction {
|
||||||
|
w := io.NewBufBinWriter()
|
||||||
|
emit.AppCall(w.BinWriter, oracleCommitteeInvoker.Hash, "finish", callflag.All)
|
||||||
|
require.NoError(t, w.Err)
|
||||||
|
|
||||||
|
script := w.Bytes()
|
||||||
|
tx := transaction.New(script, 1000_0000)
|
||||||
|
tx.Nonce = neotest.Nonce()
|
||||||
|
tx.ValidUntilBlock = e.Chain.BlockHeight() + 1
|
||||||
|
tx.Attributes = []transaction.Attribute{{
|
||||||
|
Type: transaction.OracleResponseT,
|
||||||
|
Value: &transaction.OracleResponse{
|
||||||
|
ID: requestID,
|
||||||
|
Code: transaction.Success,
|
||||||
|
Result: []byte{4, 8, 15, 16, 23, 42},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
tx.Signers = []transaction.Signer{
|
||||||
|
{
|
||||||
|
Account: oracleNodeMulti.ScriptHash(),
|
||||||
|
Scopes: transaction.None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Account: oracleCommitteeInvoker.Hash,
|
||||||
|
Scopes: transaction.None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
tx.NetworkFee = 1000_1234
|
||||||
|
tx.Scripts = []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: oracleNodeMulti.SignHashable(uint32(e.Chain.GetConfig().Magic), tx),
|
||||||
|
VerificationScript: oracleNodeMulti.Script(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
InvocationScript: []byte{},
|
||||||
|
VerificationScript: []byte{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return tx
|
||||||
|
}
|
||||||
|
tx := prepareResponseTx(t, 0)
|
||||||
|
e.AddNewBlock(t, tx)
|
||||||
|
e.CheckHalt(t, tx.Hash(), stackitem.Null{})
|
||||||
|
|
||||||
|
// Ensure that callback was called.
|
||||||
|
si := e.Chain.GetStorageItem(cs.ID, []byte("lastOracleResponse"))
|
||||||
|
require.NotNil(t, si)
|
||||||
|
actual, err := stackitem.Deserialize(si)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, stackitem.NewArray([]stackitem.Item{
|
||||||
|
stackitem.NewByteArray([]byte("url")),
|
||||||
|
stackitem.NewByteArray(userData),
|
||||||
|
stackitem.NewBigInteger(big.NewInt(int64(tx.Attributes[0].Value.(*transaction.OracleResponse).Code))),
|
||||||
|
stackitem.NewByteArray(tx.Attributes[0].Value.(*transaction.OracleResponse).Result),
|
||||||
|
}), actual)
|
||||||
|
|
||||||
|
// Check that processed request is removed. We can't access GetRequestInternal directly,
|
||||||
|
// but adding response to this request should fail due to invalid request error.
|
||||||
|
tx = prepareResponseTx(t, 0)
|
||||||
|
err = e.Chain.VerifyTx(tx)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.True(t, strings.Contains(err.Error(), "oracle tx points to invalid request"))
|
||||||
|
|
||||||
|
t.Run("ErrorOnFinish", func(t *testing.T) {
|
||||||
|
putOracleRequest(t, helperValidatorInvoker, "url", nil, "handle", []byte{1, 2}, gasForResponse)
|
||||||
|
tx := prepareResponseTx(t, 1)
|
||||||
|
e.AddNewBlock(t, tx)
|
||||||
|
e.CheckFault(t, tx.Hash(), "ABORT")
|
||||||
|
|
||||||
|
// Check that processed request is cleaned up even if callback failed. We can't
|
||||||
|
// access GetRequestInternal directly, but adding response to this request
|
||||||
|
// should fail due to invalid request error.
|
||||||
|
tx = prepareResponseTx(t, 1)
|
||||||
|
err = e.Chain.VerifyTx(tx)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.True(t, strings.Contains(err.Error(), "oracle tx points to invalid request"))
|
||||||
|
})
|
||||||
|
t.Run("BadRequest", func(t *testing.T) {
|
||||||
|
t.Run("non-UTF8 url", func(t *testing.T) {
|
||||||
|
putOracleRequest(t, helperValidatorInvoker, "\xff", nil, "", []byte{1, 2}, gasForResponse, "invalid value: not UTF-8")
|
||||||
|
})
|
||||||
|
t.Run("non-UTF8 filter", func(t *testing.T) {
|
||||||
|
var f = "\xff"
|
||||||
|
putOracleRequest(t, helperValidatorInvoker, "url", &f, "", []byte{1, 2}, gasForResponse, "invalid value: not UTF-8")
|
||||||
|
})
|
||||||
|
t.Run("not enough gas", func(t *testing.T) {
|
||||||
|
putOracleRequest(t, helperValidatorInvoker, "url", nil, "", nil, 1000, "not enough gas for response")
|
||||||
|
})
|
||||||
|
t.Run("disallowed callback", func(t *testing.T) {
|
||||||
|
putOracleRequest(t, helperValidatorInvoker, "url", nil, "_deploy", nil, 1000_0000, "disallowed callback method (starts with '_')")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,262 +0,0 @@
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"math"
|
|
||||||
"math/big"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/nspcc-dev/neo-go/internal/testchain"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/block"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/native"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
|
|
||||||
"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"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/nef"
|
|
||||||
"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"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// getTestContractState returns test contract which uses oracles.
|
|
||||||
func getOracleContractState(h util.Uint160, stdHash util.Uint160) *state.Contract {
|
|
||||||
w := io.NewBufBinWriter()
|
|
||||||
emit.Int(w.BinWriter, 5)
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.PACK)
|
|
||||||
emit.Int(w.BinWriter, int64(callflag.All))
|
|
||||||
emit.String(w.BinWriter, "request")
|
|
||||||
emit.Bytes(w.BinWriter, h.BytesBE())
|
|
||||||
emit.Syscall(w.BinWriter, interopnames.SystemContractCall)
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.DROP)
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.RET)
|
|
||||||
|
|
||||||
// `handle` method aborts if len(userData) == 2
|
|
||||||
offset := w.Len()
|
|
||||||
emit.Bytes(w.BinWriter, neoOwner.BytesBE())
|
|
||||||
emit.Syscall(w.BinWriter, interopnames.SystemRuntimeCheckWitness)
|
|
||||||
emit.Instruction(w.BinWriter, opcode.JMPIF, []byte{3})
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.ABORT)
|
|
||||||
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.OVER)
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.SIZE)
|
|
||||||
emit.Int(w.BinWriter, 2)
|
|
||||||
emit.Instruction(w.BinWriter, opcode.JMPNE, []byte{3})
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.ABORT)
|
|
||||||
emit.Int(w.BinWriter, 4) // url, userData, code, result
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.PACK)
|
|
||||||
emit.Int(w.BinWriter, 1) // 1 byte (args count for `serialize`)
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.PACK) // 1 byte (pack args into array for `serialize`)
|
|
||||||
emit.AppCallNoArgs(w.BinWriter, stdHash, "serialize", callflag.All) // 39 bytes
|
|
||||||
emit.String(w.BinWriter, "lastOracleResponse")
|
|
||||||
emit.Syscall(w.BinWriter, interopnames.SystemStorageGetContext)
|
|
||||||
emit.Syscall(w.BinWriter, interopnames.SystemStoragePut)
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.RET)
|
|
||||||
|
|
||||||
m := manifest.NewManifest("TestOracle")
|
|
||||||
m.ABI.Methods = []manifest.Method{
|
|
||||||
{
|
|
||||||
Name: "requestURL",
|
|
||||||
Offset: 0,
|
|
||||||
Parameters: []manifest.Parameter{
|
|
||||||
manifest.NewParameter("url", smartcontract.StringType),
|
|
||||||
manifest.NewParameter("filter", smartcontract.StringType),
|
|
||||||
manifest.NewParameter("callback", smartcontract.StringType),
|
|
||||||
manifest.NewParameter("userData", smartcontract.AnyType),
|
|
||||||
manifest.NewParameter("gasForResponse", smartcontract.IntegerType),
|
|
||||||
},
|
|
||||||
ReturnType: smartcontract.VoidType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "handle",
|
|
||||||
Offset: offset,
|
|
||||||
Parameters: []manifest.Parameter{
|
|
||||||
manifest.NewParameter("url", smartcontract.StringType),
|
|
||||||
manifest.NewParameter("userData", smartcontract.AnyType),
|
|
||||||
manifest.NewParameter("code", smartcontract.IntegerType),
|
|
||||||
manifest.NewParameter("result", smartcontract.ByteArrayType),
|
|
||||||
},
|
|
||||||
ReturnType: smartcontract.VoidType,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
perm := manifest.NewPermission(manifest.PermissionHash, h)
|
|
||||||
perm.Methods.Add("request")
|
|
||||||
m.Permissions = append(m.Permissions, *perm)
|
|
||||||
|
|
||||||
script := w.Bytes()
|
|
||||||
ne, err := nef.NewFile(script)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return &state.Contract{
|
|
||||||
ContractBase: state.ContractBase{
|
|
||||||
NEF: *ne,
|
|
||||||
Hash: hash.Hash160(script),
|
|
||||||
Manifest: *m,
|
|
||||||
ID: 42,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func putOracleRequest(t *testing.T, h util.Uint160, bc *Blockchain,
|
|
||||||
url string, filter *string, cb string, userData []byte, gas int64) util.Uint256 {
|
|
||||||
var filtItem interface{}
|
|
||||||
if filter != nil {
|
|
||||||
filtItem = *filter
|
|
||||||
}
|
|
||||||
res, err := invokeContractMethod(bc, gas+50_000_000+5_000_000, h, "requestURL",
|
|
||||||
url, filtItem, cb, userData, gas)
|
|
||||||
require.NoError(t, err)
|
|
||||||
return res.Container
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOracle_Request(t *testing.T) {
|
|
||||||
bc := newTestChain(t)
|
|
||||||
|
|
||||||
orc := bc.contracts.Oracle
|
|
||||||
cs := getOracleContractState(orc.Hash, bc.contracts.Std.Hash)
|
|
||||||
require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs))
|
|
||||||
|
|
||||||
gasForResponse := int64(2000_1234)
|
|
||||||
var filter = "flt"
|
|
||||||
userData := []byte("custom info")
|
|
||||||
txHash := putOracleRequest(t, cs.Hash, bc, "url", &filter, "handle", userData, gasForResponse)
|
|
||||||
|
|
||||||
req, err := orc.GetRequestInternal(bc.dao, 0)
|
|
||||||
require.NotNil(t, req)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, txHash, req.OriginalTxID)
|
|
||||||
require.Equal(t, "url", req.URL)
|
|
||||||
require.Equal(t, filter, *req.Filter)
|
|
||||||
require.Equal(t, cs.Hash, req.CallbackContract)
|
|
||||||
require.Equal(t, "handle", req.CallbackMethod)
|
|
||||||
require.Equal(t, uint64(gasForResponse), req.GasForResponse)
|
|
||||||
|
|
||||||
idList, err := orc.GetIDListInternal(bc.dao, "url")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, &native.IDList{0}, idList)
|
|
||||||
|
|
||||||
// Finish.
|
|
||||||
priv, err := keys.NewPrivateKey()
|
|
||||||
require.NoError(t, err)
|
|
||||||
pub := priv.PublicKey()
|
|
||||||
|
|
||||||
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
|
||||||
bl := block.New(bc.config.StateRootInHeader)
|
|
||||||
bl.Index = bc.BlockHeight() + 1
|
|
||||||
setSigner(tx, testchain.CommitteeScriptHash())
|
|
||||||
ic := bc.newInteropContext(trigger.Application, bc.dao, bl, tx)
|
|
||||||
ic.SpawnVM()
|
|
||||||
ic.VM.LoadScript([]byte{byte(opcode.RET)})
|
|
||||||
err = bc.contracts.Designate.DesignateAsRole(ic, noderoles.Oracle, keys.PublicKeys{pub})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
tx = transaction.New(orc.GetOracleResponseScript(), 0)
|
|
||||||
ic.Tx = tx
|
|
||||||
ic.Block = bc.newBlock(tx)
|
|
||||||
|
|
||||||
err = orc.FinishInternal(ic)
|
|
||||||
require.True(t, errors.Is(err, native.ErrResponseNotFound), "got: %v", err)
|
|
||||||
|
|
||||||
resp := &transaction.OracleResponse{
|
|
||||||
ID: 12,
|
|
||||||
Code: transaction.Success,
|
|
||||||
Result: []byte{4, 8, 15, 16, 23, 42},
|
|
||||||
}
|
|
||||||
tx.Attributes = []transaction.Attribute{{
|
|
||||||
Type: transaction.OracleResponseT,
|
|
||||||
Value: resp,
|
|
||||||
}}
|
|
||||||
err = orc.FinishInternal(ic)
|
|
||||||
require.True(t, errors.Is(err, native.ErrRequestNotFound), "got: %v", err)
|
|
||||||
|
|
||||||
// We need to ensure that callback is called thus, executing full script is necessary.
|
|
||||||
resp.ID = 0
|
|
||||||
ic.VM.LoadScriptWithFlags(tx.Script, callflag.All)
|
|
||||||
require.NoError(t, ic.VM.Run())
|
|
||||||
|
|
||||||
si := ic.DAO.GetStorageItem(cs.ID, []byte("lastOracleResponse"))
|
|
||||||
require.NotNil(t, si)
|
|
||||||
item, err := stackitem.Deserialize(si)
|
|
||||||
require.NoError(t, err)
|
|
||||||
arr, ok := item.Value().([]stackitem.Item)
|
|
||||||
require.True(t, ok)
|
|
||||||
require.Equal(t, []byte("url"), arr[0].Value())
|
|
||||||
require.Equal(t, userData, arr[1].Value())
|
|
||||||
require.Equal(t, big.NewInt(int64(resp.Code)), arr[2].Value())
|
|
||||||
require.Equal(t, resp.Result, arr[3].Value())
|
|
||||||
|
|
||||||
// Check that processed request is removed during `postPersist`.
|
|
||||||
_, err = orc.GetRequestInternal(ic.DAO, 0)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.NoError(t, orc.PostPersist(ic))
|
|
||||||
_, err = orc.GetRequestInternal(ic.DAO, 0)
|
|
||||||
require.Error(t, err)
|
|
||||||
|
|
||||||
t.Run("ErrorOnFinish", func(t *testing.T) {
|
|
||||||
const reqID = 1
|
|
||||||
|
|
||||||
putOracleRequest(t, cs.Hash, bc, "url", nil, "handle", []byte{1, 2}, gasForResponse)
|
|
||||||
_, err := orc.GetRequestInternal(bc.dao, reqID) // ensure ID is 1
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
tx = transaction.New(orc.GetOracleResponseScript(), 0)
|
|
||||||
tx.Attributes = []transaction.Attribute{{
|
|
||||||
Type: transaction.OracleResponseT,
|
|
||||||
Value: &transaction.OracleResponse{
|
|
||||||
ID: reqID,
|
|
||||||
Code: transaction.Success,
|
|
||||||
Result: []byte{4, 8, 15, 16, 23, 42},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
ic := bc.newInteropContext(trigger.Application, bc.dao, bc.newBlock(tx), tx)
|
|
||||||
ic.VM = ic.SpawnVM()
|
|
||||||
ic.VM.LoadScriptWithFlags(tx.Script, callflag.All)
|
|
||||||
require.Error(t, ic.VM.Run())
|
|
||||||
|
|
||||||
// Request is cleaned up even if callback failed.
|
|
||||||
require.NoError(t, orc.PostPersist(ic))
|
|
||||||
_, err = orc.GetRequestInternal(ic.DAO, reqID)
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
t.Run("BadRequest", func(t *testing.T) {
|
|
||||||
var doBadRequest = func(t *testing.T, h util.Uint160, url string, filter *string, cb string, userData []byte, gas int64) {
|
|
||||||
txHash := putOracleRequest(t, h, bc, url, filter, cb, userData, gas)
|
|
||||||
aer, err := bc.GetAppExecResults(txHash, trigger.Application)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 1, len(aer))
|
|
||||||
require.Equal(t, vm.FaultState, aer[0].VMState)
|
|
||||||
}
|
|
||||||
t.Run("non-UTF8 url", func(t *testing.T) {
|
|
||||||
doBadRequest(t, cs.Hash, "\xff", nil, "", []byte{1, 2}, gasForResponse)
|
|
||||||
})
|
|
||||||
t.Run("non-UTF8 filter", func(t *testing.T) {
|
|
||||||
var f = "\xff"
|
|
||||||
doBadRequest(t, cs.Hash, "url", &f, "", []byte{1, 2}, gasForResponse)
|
|
||||||
})
|
|
||||||
t.Run("not enough gas", func(t *testing.T) {
|
|
||||||
doBadRequest(t, cs.Hash, "url", nil, "", nil, 1000)
|
|
||||||
})
|
|
||||||
t.Run("disallowed callback", func(t *testing.T) {
|
|
||||||
doBadRequest(t, cs.Hash, "url", nil, "_deploy", nil, 1000_0000)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetSetPrice(t *testing.T) {
|
|
||||||
bc := newTestChain(t)
|
|
||||||
testGetSet(t, bc, bc.contracts.Oracle.Hash, "Price",
|
|
||||||
native.DefaultOracleRequestPrice, 1, math.MaxInt64)
|
|
||||||
}
|
|
|
@ -2,11 +2,14 @@ package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
gio "io"
|
gio "io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -16,20 +19,164 @@ import (
|
||||||
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/config"
|
"github.com/nspcc-dev/neo-go/pkg/config"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
|
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/services/oracle"
|
"github.com/nspcc-dev/neo-go/pkg/services/oracle"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/nef"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.uber.org/zap/zaptest"
|
"go.uber.org/zap/zaptest"
|
||||||
)
|
)
|
||||||
|
|
||||||
var oracleModulePath = filepath.Join("..", "services", "oracle")
|
var (
|
||||||
|
oracleModulePath = filepath.Join("..", "services", "oracle")
|
||||||
|
oracleContractNEFPath = filepath.Join("test_data", "oracle_contract", "oracle.nef")
|
||||||
|
oracleContractManifestPath = filepath.Join("test_data", "oracle_contract", "oracle.manifest.json")
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestGenerateOracleContract generates helper contract that is able to call
|
||||||
|
// native Oracle contract and has callback method. It uses test chain to define
|
||||||
|
// Oracle and StdLib native hashes and saves generated NEF and manifest to ... folder.
|
||||||
|
// Set `saveState` flag to true and run the test to rewrite NEF and manifest files.
|
||||||
|
func TestGenerateOracleContract(t *testing.T) {
|
||||||
|
const saveState = false
|
||||||
|
|
||||||
|
bc := newTestChain(t)
|
||||||
|
oracleHash := bc.contracts.Oracle.Hash
|
||||||
|
stdHash := bc.contracts.Std.Hash
|
||||||
|
|
||||||
|
w := io.NewBufBinWriter()
|
||||||
|
emit.Int(w.BinWriter, 5)
|
||||||
|
emit.Opcodes(w.BinWriter, opcode.PACK)
|
||||||
|
emit.Int(w.BinWriter, int64(callflag.All))
|
||||||
|
emit.String(w.BinWriter, "request")
|
||||||
|
emit.Bytes(w.BinWriter, oracleHash.BytesBE())
|
||||||
|
emit.Syscall(w.BinWriter, interopnames.SystemContractCall)
|
||||||
|
emit.Opcodes(w.BinWriter, opcode.DROP)
|
||||||
|
emit.Opcodes(w.BinWriter, opcode.RET)
|
||||||
|
|
||||||
|
// `handle` method aborts if len(userData) == 2 and does NOT perform witness checks
|
||||||
|
// for the sake of contract code simplicity (the contract is used in multiple testchains).
|
||||||
|
offset := w.Len()
|
||||||
|
|
||||||
|
emit.Opcodes(w.BinWriter, opcode.OVER)
|
||||||
|
emit.Opcodes(w.BinWriter, opcode.SIZE)
|
||||||
|
emit.Int(w.BinWriter, 2)
|
||||||
|
emit.Instruction(w.BinWriter, opcode.JMPNE, []byte{3})
|
||||||
|
emit.Opcodes(w.BinWriter, opcode.ABORT)
|
||||||
|
emit.Int(w.BinWriter, 4) // url, userData, code, result
|
||||||
|
emit.Opcodes(w.BinWriter, opcode.PACK)
|
||||||
|
emit.Int(w.BinWriter, 1) // 1 byte (args count for `serialize`)
|
||||||
|
emit.Opcodes(w.BinWriter, opcode.PACK) // 1 byte (pack args into array for `serialize`)
|
||||||
|
emit.AppCallNoArgs(w.BinWriter, stdHash, "serialize", callflag.All) // 39 bytes
|
||||||
|
emit.String(w.BinWriter, "lastOracleResponse")
|
||||||
|
emit.Syscall(w.BinWriter, interopnames.SystemStorageGetContext)
|
||||||
|
emit.Syscall(w.BinWriter, interopnames.SystemStoragePut)
|
||||||
|
emit.Opcodes(w.BinWriter, opcode.RET)
|
||||||
|
|
||||||
|
m := manifest.NewManifest("TestOracle")
|
||||||
|
m.ABI.Methods = []manifest.Method{
|
||||||
|
{
|
||||||
|
Name: "requestURL",
|
||||||
|
Offset: 0,
|
||||||
|
Parameters: []manifest.Parameter{
|
||||||
|
manifest.NewParameter("url", smartcontract.StringType),
|
||||||
|
manifest.NewParameter("filter", smartcontract.StringType),
|
||||||
|
manifest.NewParameter("callback", smartcontract.StringType),
|
||||||
|
manifest.NewParameter("userData", smartcontract.AnyType),
|
||||||
|
manifest.NewParameter("gasForResponse", smartcontract.IntegerType),
|
||||||
|
},
|
||||||
|
ReturnType: smartcontract.VoidType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "handle",
|
||||||
|
Offset: offset,
|
||||||
|
Parameters: []manifest.Parameter{
|
||||||
|
manifest.NewParameter("url", smartcontract.StringType),
|
||||||
|
manifest.NewParameter("userData", smartcontract.AnyType),
|
||||||
|
manifest.NewParameter("code", smartcontract.IntegerType),
|
||||||
|
manifest.NewParameter("result", smartcontract.ByteArrayType),
|
||||||
|
},
|
||||||
|
ReturnType: smartcontract.VoidType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
perm := manifest.NewPermission(manifest.PermissionHash, oracleHash)
|
||||||
|
perm.Methods.Add("request")
|
||||||
|
m.Permissions = append(m.Permissions, *perm)
|
||||||
|
|
||||||
|
// Generate NEF file.
|
||||||
|
script := w.Bytes()
|
||||||
|
ne, err := nef.NewFile(script)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Write NEF file.
|
||||||
|
bytes, err := ne.Bytes()
|
||||||
|
require.NoError(t, err)
|
||||||
|
if saveState {
|
||||||
|
err = ioutil.WriteFile(oracleContractNEFPath, bytes, os.ModePerm)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write manifest file.
|
||||||
|
mData, err := json.Marshal(m)
|
||||||
|
require.NoError(t, err)
|
||||||
|
if saveState {
|
||||||
|
err = ioutil.WriteFile(oracleContractManifestPath, mData, os.ModePerm)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.False(t, saveState)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getOracleContractState reads pre-compiled oracle contract generated by
|
||||||
|
// TestGenerateOracleContract and returns its state.
|
||||||
|
func getOracleContractState(t *testing.T, sender util.Uint160, id int32) *state.Contract {
|
||||||
|
errNotFound := errors.New("auto-generated oracle contract is not found, use TestGenerateOracleContract to regenerate")
|
||||||
|
|
||||||
|
neBytes, err := ioutil.ReadFile(oracleContractNEFPath)
|
||||||
|
require.NoError(t, err, fmt.Errorf("nef: %w", errNotFound))
|
||||||
|
ne, err := nef.FileFromBytes(neBytes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
mBytes, err := ioutil.ReadFile(oracleContractManifestPath)
|
||||||
|
require.NoError(t, err, fmt.Errorf("manifest: %w", errNotFound))
|
||||||
|
m := &manifest.Manifest{}
|
||||||
|
err = json.Unmarshal(mBytes, m)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return &state.Contract{
|
||||||
|
ContractBase: state.ContractBase{
|
||||||
|
NEF: ne,
|
||||||
|
Hash: state.CreateContractHash(sender, ne.Checksum, m.Name),
|
||||||
|
Manifest: *m,
|
||||||
|
ID: id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func putOracleRequest(t *testing.T, h util.Uint160, bc *Blockchain,
|
||||||
|
url string, filter *string, cb string, userData []byte, gas int64) util.Uint256 {
|
||||||
|
var filtItem interface{}
|
||||||
|
if filter != nil {
|
||||||
|
filtItem = *filter
|
||||||
|
}
|
||||||
|
res, err := invokeContractMethod(bc, gas+50_000_000+5_000_000, h, "requestURL",
|
||||||
|
url, filtItem, cb, userData, gas)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return res.Container
|
||||||
|
}
|
||||||
|
|
||||||
func getOracleConfig(t *testing.T, bc *Blockchain, w, pass string) oracle.Config {
|
func getOracleConfig(t *testing.T, bc *Blockchain, w, pass string) oracle.Config {
|
||||||
return oracle.Config{
|
return oracle.Config{
|
||||||
|
@ -133,7 +280,7 @@ func TestOracle(t *testing.T) {
|
||||||
orc1.UpdateNativeContract(orcNative.NEF.Script, orcNative.GetOracleResponseScript(), orcNative.Hash, md.MD.Offset)
|
orc1.UpdateNativeContract(orcNative.NEF.Script, orcNative.GetOracleResponseScript(), orcNative.Hash, md.MD.Offset)
|
||||||
orc2.UpdateNativeContract(orcNative.NEF.Script, orcNative.GetOracleResponseScript(), orcNative.Hash, md.MD.Offset)
|
orc2.UpdateNativeContract(orcNative.NEF.Script, orcNative.GetOracleResponseScript(), orcNative.Hash, md.MD.Offset)
|
||||||
|
|
||||||
cs := getOracleContractState(bc.contracts.Oracle.Hash, bc.contracts.Std.Hash)
|
cs := getOracleContractState(t, util.Uint160{}, 42)
|
||||||
require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs))
|
require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs))
|
||||||
|
|
||||||
putOracleRequest(t, cs.Hash, bc, "https://get.1234", nil, "handle", []byte{}, 10_000_000)
|
putOracleRequest(t, cs.Hash, bc, "https://get.1234", nil, "handle", []byte{}, 10_000_000)
|
||||||
|
@ -301,7 +448,7 @@ func TestOracleFull(t *testing.T) {
|
||||||
orc.OnTransaction = func(tx *transaction.Transaction) { _ = mp.Add(tx, bc) }
|
orc.OnTransaction = func(tx *transaction.Transaction) { _ = mp.Add(tx, bc) }
|
||||||
bc.SetOracle(orc)
|
bc.SetOracle(orc)
|
||||||
|
|
||||||
cs := getOracleContractState(bc.contracts.Oracle.Hash, bc.contracts.Std.Hash)
|
cs := getOracleContractState(t, util.Uint160{}, 42)
|
||||||
require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs))
|
require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs))
|
||||||
|
|
||||||
go bc.Run()
|
go bc.Run()
|
||||||
|
@ -326,7 +473,7 @@ func TestNotYetRunningOracle(t *testing.T) {
|
||||||
orc.OnTransaction = func(tx *transaction.Transaction) { _ = mp.Add(tx, bc) }
|
orc.OnTransaction = func(tx *transaction.Transaction) { _ = mp.Add(tx, bc) }
|
||||||
bc.SetOracle(orc)
|
bc.SetOracle(orc)
|
||||||
|
|
||||||
cs := getOracleContractState(bc.contracts.Oracle.Hash, bc.contracts.Std.Hash)
|
cs := getOracleContractState(t, util.Uint160{}, 42)
|
||||||
require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs))
|
require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs))
|
||||||
|
|
||||||
go bc.Run()
|
go bc.Run()
|
||||||
|
|
9
pkg/core/test_data/oracle_contract/README.md
Normal file
9
pkg/core/test_data/oracle_contract/README.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
## Oracle helper contract
|
||||||
|
|
||||||
|
Oracle helper contract NEF and manifest files are generated automatically by
|
||||||
|
`TestGenerateOracleContract` and are used in tests. Do not modify these files manually.
|
||||||
|
To regenerate these files:
|
||||||
|
|
||||||
|
1. Open `TestGenerateOracleContract` and set `saveState` flag to `true`.
|
||||||
|
2. Run `TestGenerateOracleContract`.
|
||||||
|
3. Set `saveState` back to `false`.
|
1
pkg/core/test_data/oracle_contract/oracle.manifest.json
Executable file
1
pkg/core/test_data/oracle_contract/oracle.manifest.json
Executable file
|
@ -0,0 +1 @@
|
||||||
|
{"name":"TestOracle","abi":{"methods":[{"name":"requestURL","offset":0,"parameters":[{"name":"url","type":"String"},{"name":"filter","type":"String"},{"name":"callback","type":"String"},{"name":"userData","type":"Any"},{"name":"gasForResponse","type":"Integer"}],"returntype":"Void","safe":false},{"name":"handle","offset":41,"parameters":[{"name":"url","type":"String"},{"name":"userData","type":"Any"},{"name":"code","type":"Integer"},{"name":"result","type":"ByteArray"}],"returntype":"Void","safe":false}],"events":[]},"features":{},"groups":[],"permissions":[{"contract":"0xfe924b7cfe89ddd271abaf7210a80a7e11178758","methods":["request"]}],"supportedstandards":[],"trusts":[],"extra":null}
|
BIN
pkg/core/test_data/oracle_contract/oracle.nef
Executable file
BIN
pkg/core/test_data/oracle_contract/oracle.nef
Executable file
Binary file not shown.
Loading…
Reference in a new issue