forked from TrueCloudLab/neoneo-go
native: implement basis for oracle contract
This commit is contained in:
parent
ef7c0dbd78
commit
141d6e325f
5 changed files with 696 additions and 1 deletions
|
@ -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)
|
||||
|
|
412
pkg/core/native/oracle.go
Normal file
412
pkg/core/native/oracle.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
246
pkg/core/native_oracle_test.go
Normal file
246
pkg/core/native_oracle_test.go
Normal file
|
@ -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)
|
||||
}
|
Loading…
Reference in a new issue