From 141d6e325f0a1f5b56c2a6082c93d2c8b2ba8c9a Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 18 Sep 2020 16:26:36 +0300 Subject: [PATCH] native: implement basis for oracle contract --- pkg/core/native/contract.go | 9 +- pkg/core/native/oracle.go | 412 +++++++++++++++++++++++++++ pkg/core/native/oracle_types.go | 14 + pkg/core/native/oracle_types_test.go | 16 ++ pkg/core/native_oracle_test.go | 246 ++++++++++++++++ 5 files changed, 696 insertions(+), 1 deletion(-) create mode 100644 pkg/core/native/oracle.go create mode 100644 pkg/core/native_oracle_test.go diff --git a/pkg/core/native/contract.go b/pkg/core/native/contract.go index 1dbcd5517..45490b7f7 100644 --- a/pkg/core/native/contract.go +++ b/pkg/core/native/contract.go @@ -16,6 +16,7 @@ type Contracts struct { NEO *NEO GAS *GAS Policy *Policy + Oracle *Oracle Contracts []interop.Contract // persistScript is vm script which executes "onPersist" method of every native contract. persistScript []byte @@ -51,6 +52,12 @@ func NewContracts() *Contracts { policy := newPolicy() cs.Policy = policy cs.Contracts = append(cs.Contracts, policy) + + oracle := newOracle() + oracle.GAS = gas + oracle.NEO = neo + cs.Oracle = oracle + cs.Contracts = append(cs.Contracts, oracle) return cs } @@ -64,7 +71,7 @@ func (cs *Contracts) GetPersistScript() []byte { md := cs.Contracts[i].Metadata() // Not every contract is persisted: // https://github.com/neo-project/neo/blob/master/src/neo/Ledger/Blockchain.cs#L90 - if md.ContractID == policyContractID { + if md.ContractID == policyContractID || md.ContractID == oracleContractID { continue } emit.Int(w.BinWriter, 0) diff --git a/pkg/core/native/oracle.go b/pkg/core/native/oracle.go new file mode 100644 index 000000000..fbbdb8859 --- /dev/null +++ b/pkg/core/native/oracle.go @@ -0,0 +1,412 @@ +package native + +import ( + "encoding/binary" + "errors" + "math/big" + "sort" + + "github.com/nspcc-dev/neo-go/pkg/core/dao" + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/core/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" + "github.com/nspcc-dev/neo-go/pkg/core/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/core/storage" + "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/manifest" + "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/vm/stackitem" +) + +// Oracle represents Oracle native contract. +type Oracle struct { + interop.ContractMD + GAS *GAS + NEO *NEO +} + +const ( + oracleContractID = -4 + oracleName = "Oracle" +) + +const ( + maxURLLength = 256 + maxFilterLength = 128 + maxCallbackLength = 32 + maxUserDataLength = 512 + + oracleRequestPrice = 5000_0000 +) + +var oracleScript []byte + +func init() { + w := io.NewBufBinWriter() + emit.String(w.BinWriter, oracleName) + emit.Syscall(w.BinWriter, interopnames.NeoNativeCall) + h := hash.Hash160(w.Bytes()) + + w.Reset() + emit.Int(w.BinWriter, 0) + emit.Opcode(w.BinWriter, opcode.NEWARRAY) + emit.String(w.BinWriter, "finish") + emit.Bytes(w.BinWriter, h.BytesBE()) + emit.Syscall(w.BinWriter, interopnames.SystemContractCall) + oracleScript = w.Bytes() +} + +var ( + prefixIDList = []byte{6} + prefixRequest = []byte{7} + prefixNodeList = []byte{8} + prefixRequestID = []byte{9} +) + +// Various validation errors. +var ( + ErrBigArgument = errors.New("some of the arguments are invalid") + ErrEmptyNodeList = errors.New("oracle nodes list is empty") + ErrInvalidWitness = errors.New("witness check failed") + ErrNotEnoughGas = errors.New("gas limit exceeded") + ErrRequestNotFound = errors.New("oracle request not found") + ErrResponseNotFound = errors.New("oracle response not found") +) + +// GetOracleResponseScript returns script for transaction with oracle response. +func GetOracleResponseScript() []byte { + b := make([]byte, len(oracleScript)) + copy(b, oracleScript) + return b +} + +func newOracle() *Oracle { + o := &Oracle{ContractMD: *interop.NewContractMD(oracleName)} + o.ContractID = oracleContractID + o.Manifest.Features = smartcontract.HasStorage + + desc := newDescriptor("request", smartcontract.VoidType, + 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)) + md := newMethodAndPrice(o.request, oracleRequestPrice, smartcontract.AllowModifyStates) + o.AddMethod(md, desc, false) + + desc = newDescriptor("finish", smartcontract.VoidType) + md = newMethodAndPrice(o.finish, 0, smartcontract.AllowModifyStates) + o.AddMethod(md, desc, false) + + desc = newDescriptor("getOracleNodes", smartcontract.ArrayType) + md = newMethodAndPrice(o.getOracleNodes, 100_0000, smartcontract.AllowStates) + o.AddMethod(md, desc, true) + + desc = newDescriptor("setOracleNodes", smartcontract.VoidType) + md = newMethodAndPrice(o.setOracleNodes, 0, smartcontract.AllowModifyStates) + o.AddMethod(md, desc, false) + + desc = newDescriptor("verify", smartcontract.BoolType) + md = newMethodAndPrice(o.verify, 100_0000, smartcontract.NoneFlag) + o.AddMethod(md, desc, false) + + pp := chainOnPersist(postPersistBase, o.PostPersist) + desc = newDescriptor("postPersist", smartcontract.VoidType) + md = newMethodAndPrice(getOnPersistWrapper(pp), 0, smartcontract.AllowModifyStates) + o.AddMethod(md, desc, false) + + return o +} + +// PostPersist represents `postPersist` method. +func (o *Oracle) PostPersist(ic *interop.Context) error { + var nodes keys.PublicKeys + var reward []big.Int + single := new(big.Int).SetUint64(oracleRequestPrice) + for _, tx := range ic.Block.Transactions { + resp := getResponse(tx) + if resp == nil { + continue + } + reqKey := makeRequestKey(resp.ID) + req := new(OracleRequest) + if err := o.getSerializableFromDAO(ic.DAO, reqKey, req); err != nil { + return err + } + if err := ic.DAO.DeleteStorageItem(o.ContractID, reqKey); err != nil { + return err + } + + idKey := makeIDListKey(req.URL) + idList := new(IDList) + if err := o.getSerializableFromDAO(ic.DAO, idKey, idList); err != nil { + return err + } + if !idList.Remove(resp.ID) { + return errors.New("response ID wasn't found") + } + + var err error + if len(*idList) == 0 { + err = ic.DAO.DeleteStorageItem(o.ContractID, idKey) + } else { + si := &state.StorageItem{Value: idList.Bytes()} + err = ic.DAO.PutStorageItem(o.ContractID, idKey, si) + } + if err != nil { + return err + } + + if nodes == nil { + nodes, err = o.GetOracleNodes(ic.DAO) + if err != nil { + return err + } + reward = make([]big.Int, len(nodes)) + } + + if len(reward) > 0 { + index := resp.ID % uint64(len(nodes)) + reward[index].Add(&reward[index], single) + } + } + for i := range reward { + o.GAS.mint(ic, nodes[i].GetScriptHash(), &reward[i]) + } + return nil +} + +// Metadata returns contract metadata. +func (o *Oracle) Metadata() *interop.ContractMD { + return &o.ContractMD +} + +// Initialize initializes Oracle contract. +func (o *Oracle) Initialize(ic *interop.Context) error { + si := &state.StorageItem{Value: NodeList{}.Bytes()} + if err := ic.DAO.PutStorageItem(o.ContractID, prefixNodeList, si); err != nil { + return err + } + si = &state.StorageItem{Value: make([]byte, 8)} // uint64(0) LE + return ic.DAO.PutStorageItem(o.ContractID, prefixRequestID, si) +} + +func getResponse(tx *transaction.Transaction) *transaction.OracleResponse { + for i := range tx.Attributes { + if tx.Attributes[i].Type == transaction.OracleResponseT { + return tx.Attributes[i].Value.(*transaction.OracleResponse) + } + } + return nil +} + +func (o *Oracle) finish(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + err := o.FinishInternal(ic) + if err != nil { + panic(err) + } + return stackitem.Null{} +} + +// FinishInternal processes oracle response. +func (o *Oracle) FinishInternal(ic *interop.Context) error { + resp := getResponse(ic.Tx) + if resp == nil { + return ErrResponseNotFound + } + req, err := o.GetRequestInternal(ic.DAO, resp.ID) + if err != nil { + return ErrRequestNotFound + } + + r := io.NewBinReaderFromBuf(req.UserData) + userData := stackitem.DecodeBinaryStackItem(r) + args := stackitem.NewArray([]stackitem.Item{ + stackitem.Make(req.URL), + stackitem.Make(userData), + stackitem.Make(resp.Code), + stackitem.Make(resp.Result), + }) + ic.VM.Estack().PushVal(args) + ic.VM.Estack().PushVal(req.CallbackMethod) + ic.VM.Estack().PushVal(req.CallbackContract.BytesBE()) + return contract.Call(ic) +} + +func (o *Oracle) request(ic *interop.Context, args []stackitem.Item) stackitem.Item { + url, err := stackitem.ToString(args[0]) + if err != nil { + panic(err) + } + filter, err := stackitem.ToString(args[1]) + if err != nil { + panic(err) + } + cb, err := stackitem.ToString(args[2]) + if err != nil { + panic(err) + } + userData := args[3] + gas, err := args[4].TryInteger() + if err != nil { + panic(err) + } + if err := o.RequestInternal(ic, url, filter, cb, userData, gas); err != nil { + panic(err) + } + return stackitem.Null{} +} + +// RequestInternal processes oracle request. +func (o *Oracle) RequestInternal(ic *interop.Context, url, filter, cb string, userData stackitem.Item, gas *big.Int) error { + if len(url) > maxURLLength || len(filter) > maxFilterLength || len(cb) > maxCallbackLength || gas.Uint64() < 1000_0000 { + return ErrBigArgument + } + + if !ic.VM.AddGas(gas.Int64()) { + return ErrNotEnoughGas + } + o.GAS.mint(ic, o.Hash, gas) + si := ic.DAO.GetStorageItem(o.ContractID, prefixRequestID) + id := binary.LittleEndian.Uint64(si.Value) + 1 + binary.LittleEndian.PutUint64(si.Value, id) + if err := ic.DAO.PutStorageItem(o.ContractID, prefixRequestID, si); err != nil { + return err + } + + // Should be executed from contract. + _, err := ic.DAO.GetContractState(ic.VM.GetCallingScriptHash()) + if err != nil { + return err + } + + w := io.NewBufBinWriter() + stackitem.EncodeBinaryStackItem(userData, w.BinWriter) + if w.Err != nil { + return w.Err + } + data := w.Bytes() + if len(data) > maxUserDataLength { + return ErrBigArgument + } + + req := &OracleRequest{ + OriginalTxID: o.getOriginalTxID(ic.DAO, ic.Tx), + GasForResponse: gas.Uint64(), + URL: url, + Filter: &filter, + CallbackContract: ic.VM.GetCallingScriptHash(), + CallbackMethod: cb, + UserData: data, + } + reqItem := &state.StorageItem{Value: req.Bytes()} + reqKey := makeRequestKey(id) + if err = ic.DAO.PutStorageItem(o.ContractID, reqKey, reqItem); err != nil { + return err + } + + // Add request ID to the id list. + lst := new(IDList) + key := makeIDListKey(url) + if err := o.getSerializableFromDAO(ic.DAO, key, lst); err != nil && !errors.Is(err, storage.ErrKeyNotFound) { + return err + } + *lst = append(*lst, id) + si = &state.StorageItem{Value: lst.Bytes()} + return ic.DAO.PutStorageItem(o.ContractID, key, si) +} + +func (o *Oracle) getOracleNodes(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + pubs, err := o.GetOracleNodes(ic.DAO) + if err != nil { + panic(err) + } + return pubsToArray(pubs) +} + +// GetOracleNodes returns public keys of oracle nodes. +func (o *Oracle) GetOracleNodes(d dao.DAO) (keys.PublicKeys, error) { + ns := new(NodeList) + return keys.PublicKeys(*ns), o.getSerializableFromDAO(d, prefixNodeList, ns) +} + +func (o *Oracle) setOracleNodes(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + var pubs keys.PublicKeys + err := o.SetOracleNodes(ic, pubs) + if err != nil { + panic(err) + } + return pubsToArray(pubs) +} + +// SetOracleNodes sets oracle node public keys to pubs. +func (o *Oracle) SetOracleNodes(ic *interop.Context, pubs keys.PublicKeys) error { + if len(pubs) == 0 { + return ErrEmptyNodeList + } + h := o.NEO.GetCommitteeAddress() + if ok, err := runtime.CheckHashedWitness(ic, h); err != nil || !ok { + return ErrInvalidWitness + } + + sort.Sort(pubs) + si := &state.StorageItem{Value: NodeList(pubs).Bytes()} + return ic.DAO.PutStorageItem(o.ContractID, prefixNodeList, si) +} + +// GetRequestInternal returns request by ID and key under which it is stored. +func (o *Oracle) GetRequestInternal(d dao.DAO, id uint64) (*OracleRequest, error) { + key := makeRequestKey(id) + req := new(OracleRequest) + return req, o.getSerializableFromDAO(d, key, req) +} + +// GetIDListInternal returns request by ID and key under which it is stored. +func (o *Oracle) GetIDListInternal(d dao.DAO, url string) (*IDList, error) { + key := makeIDListKey(url) + idList := new(IDList) + return idList, o.getSerializableFromDAO(d, key, idList) +} + +func (o *Oracle) verify(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + return stackitem.NewBool(ic.Tx.HasAttribute(transaction.OracleResponseT)) +} + +func (o *Oracle) getOriginalTxID(d dao.DAO, tx *transaction.Transaction) util.Uint256 { + for i := range tx.Attributes { + if tx.Attributes[i].Type == transaction.OracleResponseT { + id := tx.Attributes[i].Value.(*transaction.OracleResponse).ID + req, _ := o.GetRequestInternal(d, id) + return req.OriginalTxID + } + } + return tx.Hash() +} + +func makeRequestKey(id uint64) []byte { + k := make([]byte, 9) + k[0] = prefixRequest[0] + binary.LittleEndian.PutUint64(k[1:], id) + return k +} + +func makeIDListKey(url string) []byte { + return append(prefixIDList, hash.Hash160([]byte(url)).BytesBE()...) +} + +func (o *Oracle) getSerializableFromDAO(d dao.DAO, key []byte, item io.Serializable) error { + si := d.GetStorageItem(o.ContractID, key) + if si == nil { + return storage.ErrKeyNotFound + } + r := io.NewBinReaderFromBuf(si.Value) + item.DecodeBinary(r) + return r.Err +} diff --git a/pkg/core/native/oracle_types.go b/pkg/core/native/oracle_types.go index 075eab3a3..af66068e2 100644 --- a/pkg/core/native/oracle_types.go +++ b/pkg/core/native/oracle_types.go @@ -74,6 +74,20 @@ func (l *IDList) fromStackItem(it stackitem.Item) error { return nil } +// Remove removes id from list. +func (l *IDList) Remove(id uint64) bool { + for i := range *l { + if id == (*l)[i] { + if i < len(*l) { + copy((*l)[i:], (*l)[i+1:]) + } + *l = (*l)[:len(*l)-1] + return true + } + } + return false +} + // Bytes return l serizalized to a byte-slice. func (l NodeList) Bytes() []byte { w := io.NewBufBinWriter() diff --git a/pkg/core/native/oracle_types_test.go b/pkg/core/native/oracle_types_test.go index 1f0b70630..85cc7ee10 100644 --- a/pkg/core/native/oracle_types_test.go +++ b/pkg/core/native/oracle_types_test.go @@ -36,6 +36,22 @@ func TestIDList_EncodeBinary(t *testing.T) { }) } +func TestIDList_Remove(t *testing.T) { + l := IDList{1, 4, 5} + + // missing + require.False(t, l.Remove(2)) + require.Equal(t, IDList{1, 4, 5}, l) + + // middle + require.True(t, l.Remove(4)) + require.Equal(t, IDList{1, 5}, l) + + // last + require.True(t, l.Remove(5)) + require.Equal(t, IDList{1}, l) +} + func TestNodeList_EncodeBinary(t *testing.T) { priv, err := keys.NewPrivateKey() require.NoError(t, err) diff --git a/pkg/core/native_oracle_test.go b/pkg/core/native_oracle_test.go new file mode 100644 index 000000000..7c4ad3164 --- /dev/null +++ b/pkg/core/native_oracle_test.go @@ -0,0 +1,246 @@ +package core + +import ( + "errors" + "math/big" + "testing" + + "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" + "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/internal/testchain" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "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) *state.Contract { + w := io.NewBufBinWriter() + emit.Int(w.BinWriter, 5) + emit.Opcode(w.BinWriter, opcode.PACK) + emit.String(w.BinWriter, "request") + emit.Bytes(w.BinWriter, h.BytesBE()) + emit.Syscall(w.BinWriter, interopnames.SystemContractCall) + emit.Opcode(w.BinWriter, opcode.RET) + + // `handle` method aborts if len(userData) == 2 + offset := w.Len() + emit.Opcode(w.BinWriter, opcode.OVER) + emit.Opcode(w.BinWriter, opcode.SIZE) + emit.Int(w.BinWriter, 2) + emit.Instruction(w.BinWriter, opcode.JMPNE, []byte{3}) + emit.Opcode(w.BinWriter, opcode.ABORT) + emit.Int(w.BinWriter, 4) // url, userData, code, result + emit.Opcode(w.BinWriter, opcode.PACK) + emit.Syscall(w.BinWriter, interopnames.SystemBinarySerialize) + emit.String(w.BinWriter, "lastOracleResponse") + emit.Syscall(w.BinWriter, interopnames.SystemStorageGetContext) + emit.Syscall(w.BinWriter, interopnames.SystemStoragePut) + emit.Opcode(w.BinWriter, opcode.RET) + + m := manifest.NewManifest(h) + m.Features = smartcontract.HasStorage + 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) + + return &state.Contract{ + Script: w.Bytes(), + Manifest: *m, + ID: 42, + } +} + +func putOracleRequest(t *testing.T, h util.Uint160, bc *Blockchain, + url, filter string, userData []byte, gas int64) util.Uint256 { + w := io.NewBufBinWriter() + emit.AppCallWithOperationAndArgs(w.BinWriter, h, "requestURL", + url, filter, "handle", userData, gas) + require.NoError(t, w.Err) + + gas += 50_000_000 + 5_000_000 // request + contract call with args + tx := transaction.New(netmode.UnitTestNet, w.Bytes(), gas) + tx.ValidUntilBlock = bc.BlockHeight() + 1 + tx.NetworkFee = 1_000_000 + setSigner(tx, testchain.MultisigScriptHash()) + require.NoError(t, signTx(bc, tx)) + require.NoError(t, bc.AddBlock(bc.newBlock(tx))) + return tx.Hash() +} + +func TestOracle_Request(t *testing.T) { + bc := newTestChain(t) + defer bc.Close() + + orc := bc.contracts.Oracle + cs := getOracleContractState(orc.Hash) + require.NoError(t, bc.dao.PutContractState(cs)) + + gasForResponse := int64(2000_1234) + userData := []byte("custom info") + txHash := putOracleRequest(t, cs.ScriptHash(), bc, "url", "flt", userData, gasForResponse) + + req, err := orc.GetRequestInternal(bc.dao, 1) + require.NotNil(t, req) + require.NoError(t, err) + require.Equal(t, txHash, req.OriginalTxID) + require.Equal(t, "url", req.URL) + require.Equal(t, "flt", *req.Filter) + require.Equal(t, cs.ScriptHash(), 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{1}, idList) + + // Finish. + priv, err := keys.NewPrivateKey() + require.NoError(t, err) + pub := priv.PublicKey() + + tx := transaction.New(netmode.UnitTestNet, []byte{}, 0) + setSigner(tx, testchain.CommitteeScriptHash()) + ic := bc.newInteropContext(trigger.Application, bc.dao, nil, tx) + err = orc.SetOracleNodes(ic, keys.PublicKeys{pub}) + require.NoError(t, err) + + tx = transaction.New(netmode.UnitTestNet, native.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: 13, + 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 = 1 + ic.VM = ic.SpawnVM() + ic.VM.LoadScriptWithFlags(tx.Script, smartcontract.All) + require.NoError(t, ic.VM.Run()) + + si := ic.DAO.GetStorageItem(cs.ID, []byte("lastOracleResponse")) + require.NotNil(t, si) + item, err := stackitem.DeserializeItem(si.Value) + 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, 1) + require.NoError(t, err) + + require.NoError(t, orc.PostPersist(ic)) + _, err = orc.GetRequestInternal(ic.DAO, 1) + require.Error(t, err) + + t.Run("ErrorOnFinish", func(t *testing.T) { + const reqID = 2 + + putOracleRequest(t, cs.ScriptHash(), bc, "url", "flt", []byte{1, 2}, gasForResponse) + _, err := orc.GetRequestInternal(bc.dao, reqID) // ensure ID is 2 + require.NoError(t, err) + + tx = transaction.New(netmode.UnitTestNet, native.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, smartcontract.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) + }) +} + +func TestOracle_SetOracleNodes(t *testing.T) { + bc := newTestChain(t) + defer bc.Close() + + orc := bc.contracts.Oracle + tx := transaction.New(netmode.UnitTestNet, []byte{}, 0) + ic := bc.newInteropContext(trigger.System, bc.dao, nil, tx) + ic.VM = vm.New() + + pubs, err := orc.GetOracleNodes(ic.DAO) + require.NoError(t, err) + require.Equal(t, 0, len(pubs)) + + err = orc.SetOracleNodes(ic, keys.PublicKeys{}) + require.True(t, errors.Is(err, native.ErrEmptyNodeList), "got: %v", err) + + priv, err := keys.NewPrivateKey() + require.NoError(t, err) + + pub := priv.PublicKey() + err = orc.SetOracleNodes(ic, keys.PublicKeys{pub}) + require.True(t, errors.Is(err, native.ErrInvalidWitness), "got: %v", err) + + setSigner(tx, testchain.CommitteeScriptHash()) + require.NoError(t, orc.SetOracleNodes(ic, keys.PublicKeys{pub})) + + pubs, err = orc.GetOracleNodes(ic.DAO) + require.NoError(t, err) + require.Equal(t, keys.PublicKeys{pub}, pubs) +}