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 (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
gio "io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
@ -16,20 +19,164 @@ import (
|
|||
|
||||
"github.com/nspcc-dev/neo-go/pkg/config"
|
||||
"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/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/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/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/opcode"
|
||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"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 {
|
||||
return oracle.Config{
|
||||
|
@ -133,7 +280,7 @@ func TestOracle(t *testing.T) {
|
|||
orc1.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))
|
||||
|
||||
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) }
|
||||
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))
|
||||
|
||||
go bc.Run()
|
||||
|
@ -326,7 +473,7 @@ func TestNotYetRunningOracle(t *testing.T) {
|
|||
orc.OnTransaction = func(tx *transaction.Transaction) { _ = mp.Add(tx, bc) }
|
||||
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))
|
||||
|
||||
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