diff --git a/pkg/core/native/native_test/oracle_test.go b/pkg/core/native/native_test/oracle_test.go new file mode 100644 index 000000000..17cc8df75 --- /dev/null +++ b/pkg/core/native/native_test/oracle_test.go @@ -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 '_')") + }) + }) +} diff --git a/pkg/core/native_oracle_test.go b/pkg/core/native_oracle_test.go deleted file mode 100644 index 28f883879..000000000 --- a/pkg/core/native_oracle_test.go +++ /dev/null @@ -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) -} diff --git a/pkg/core/oracle_test.go b/pkg/core/oracle_test.go index 6461d08bf..c07b0b545 100644 --- a/pkg/core/oracle_test.go +++ b/pkg/core/oracle_test.go @@ -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() diff --git a/pkg/core/test_data/oracle_contract/README.md b/pkg/core/test_data/oracle_contract/README.md new file mode 100644 index 000000000..83fad42a7 --- /dev/null +++ b/pkg/core/test_data/oracle_contract/README.md @@ -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`. \ No newline at end of file diff --git a/pkg/core/test_data/oracle_contract/oracle.manifest.json b/pkg/core/test_data/oracle_contract/oracle.manifest.json new file mode 100755 index 000000000..85ed7ffc0 --- /dev/null +++ b/pkg/core/test_data/oracle_contract/oracle.manifest.json @@ -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} \ No newline at end of file diff --git a/pkg/core/test_data/oracle_contract/oracle.nef b/pkg/core/test_data/oracle_contract/oracle.nef new file mode 100755 index 000000000..a09f68cc7 Binary files /dev/null and b/pkg/core/test_data/oracle_contract/oracle.nef differ