mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-11-27 03:58:06 +00:00
Merge pull request #1409 from nspcc-dev/oracle/state
Implement Oracle contract
This commit is contained in:
commit
75753afc33
16 changed files with 1336 additions and 118 deletions
|
@ -1,6 +1,7 @@
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
@ -650,6 +651,7 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
bc.contracts.Policy.OnPersistEnd(bc.dao)
|
bc.contracts.Policy.OnPersistEnd(bc.dao)
|
||||||
|
bc.contracts.Oracle.OnPersistEnd(bc.dao)
|
||||||
bc.dao.MPT.Flush()
|
bc.dao.MPT.Flush()
|
||||||
// Every persist cycle we also compact our in-memory MPT.
|
// Every persist cycle we also compact our in-memory MPT.
|
||||||
persistedHeight := atomic.LoadUint32(&bc.persistedHeight)
|
persistedHeight := atomic.LoadUint32(&bc.persistedHeight)
|
||||||
|
@ -1243,18 +1245,41 @@ func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error {
|
||||||
for i := range tx.Attributes {
|
for i := range tx.Attributes {
|
||||||
switch tx.Attributes[i].Type {
|
switch tx.Attributes[i].Type {
|
||||||
case transaction.HighPriority:
|
case transaction.HighPriority:
|
||||||
pubs := bc.contracts.NEO.GetCommitteeMembers()
|
h := bc.contracts.NEO.GetCommitteeAddress()
|
||||||
s, err := smartcontract.CreateMajorityMultiSigRedeemScript(pubs)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
h := hash.Hash160(s)
|
|
||||||
for i := range tx.Signers {
|
for i := range tx.Signers {
|
||||||
if tx.Signers[i].Account.Equals(h) {
|
if tx.Signers[i].Account.Equals(h) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fmt.Errorf("%w: high priority tx is not signed by committee", ErrInvalidAttribute)
|
return fmt.Errorf("%w: high priority tx is not signed by committee", ErrInvalidAttribute)
|
||||||
|
case transaction.OracleResponseT:
|
||||||
|
h, err := bc.contracts.Oracle.GetScriptHash()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", ErrInvalidAttribute, err)
|
||||||
|
}
|
||||||
|
hasOracle := false
|
||||||
|
for i := range tx.Signers {
|
||||||
|
if tx.Signers[i].Scopes != transaction.FeeOnly {
|
||||||
|
return fmt.Errorf("%w: oracle tx has invalid signer scope", ErrInvalidAttribute)
|
||||||
|
}
|
||||||
|
if tx.Signers[i].Account.Equals(h) {
|
||||||
|
hasOracle = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasOracle {
|
||||||
|
return fmt.Errorf("%w: oracle tx is not signed by oracle nodes", ErrInvalidAttribute)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(tx.Script, native.GetOracleResponseScript()) {
|
||||||
|
return fmt.Errorf("%w: oracle tx has invalid script", ErrInvalidAttribute)
|
||||||
|
}
|
||||||
|
resp := tx.Attributes[i].Value.(*transaction.OracleResponse)
|
||||||
|
req, err := bc.contracts.Oracle.GetRequestInternal(bc.dao, resp.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: oracle tx points to invalid request: %v", ErrInvalidAttribute, err)
|
||||||
|
}
|
||||||
|
if uint64(tx.NetworkFee+tx.SystemFee) < req.GasForResponse {
|
||||||
|
return fmt.Errorf("%w: oracle tx has insufficient gas", ErrInvalidAttribute)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -11,12 +11,15 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/block"
|
"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/interop/interopnames"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/mempool"
|
"github.com/nspcc-dev/neo-go/pkg/core/mempool"
|
||||||
|
"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/state"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/storage"
|
"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/core/transaction"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
|
"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/internal/testchain"
|
"github.com/nspcc-dev/neo-go/pkg/internal/testchain"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm"
|
"github.com/nspcc-dev/neo-go/pkg/vm"
|
||||||
|
@ -212,13 +215,17 @@ func TestVerifyTx(t *testing.T) {
|
||||||
bc := newTestChain(t)
|
bc := newTestChain(t)
|
||||||
defer bc.Close()
|
defer bc.Close()
|
||||||
|
|
||||||
accs := make([]*wallet.Account, 2)
|
accs := make([]*wallet.Account, 3)
|
||||||
for i := range accs {
|
for i := range accs {
|
||||||
var err error
|
var err error
|
||||||
accs[i], err = wallet.NewAccount()
|
accs[i], err = wallet.NewAccount()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oracleAcc := accs[2]
|
||||||
|
oraclePubs := keys.PublicKeys{oracleAcc.PrivateKey().PublicKey()}
|
||||||
|
require.NoError(t, oracleAcc.ConvertMultisig(1, oraclePubs))
|
||||||
|
|
||||||
neoHash := bc.contracts.NEO.Hash
|
neoHash := bc.contracts.NEO.Hash
|
||||||
gasHash := bc.contracts.GAS.Hash
|
gasHash := bc.contracts.GAS.Hash
|
||||||
w := io.NewBufBinWriter()
|
w := io.NewBufBinWriter()
|
||||||
|
@ -229,7 +236,7 @@ func TestVerifyTx(t *testing.T) {
|
||||||
amount = 1_000_000_000
|
amount = 1_000_000_000
|
||||||
}
|
}
|
||||||
emit.AppCallWithOperationAndArgs(w.BinWriter, sc, "transfer",
|
emit.AppCallWithOperationAndArgs(w.BinWriter, sc, "transfer",
|
||||||
neoOwner, a.PrivateKey().GetScriptHash(), amount)
|
neoOwner, a.Contract.ScriptHash(), amount)
|
||||||
emit.Opcode(w.BinWriter, opcode.ASSERT)
|
emit.Opcode(w.BinWriter, opcode.ASSERT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -376,6 +383,95 @@ func TestVerifyTx(t *testing.T) {
|
||||||
}}
|
}}
|
||||||
require.NoError(t, bc.VerifyTx(tx))
|
require.NoError(t, bc.VerifyTx(tx))
|
||||||
})
|
})
|
||||||
|
t.Run("Oracle", func(t *testing.T) {
|
||||||
|
orc := bc.contracts.Oracle
|
||||||
|
req := &native.OracleRequest{GasForResponse: 1000_0000}
|
||||||
|
require.NoError(t, orc.PutRequestInternal(1, req, bc.dao))
|
||||||
|
|
||||||
|
oracleScript, err := smartcontract.CreateMajorityMultiSigRedeemScript(oraclePubs)
|
||||||
|
require.NoError(t, err)
|
||||||
|
oracleHash := hash.Hash160(oracleScript)
|
||||||
|
|
||||||
|
// We need to create new transaction,
|
||||||
|
// because hashes are cached after signing.
|
||||||
|
getOracleTx := func(t *testing.T) *transaction.Transaction {
|
||||||
|
tx := bc.newTestTx(h, native.GetOracleResponseScript())
|
||||||
|
resp := &transaction.OracleResponse{
|
||||||
|
ID: 1,
|
||||||
|
Code: transaction.Success,
|
||||||
|
Result: []byte{1, 2, 3},
|
||||||
|
}
|
||||||
|
tx.Attributes = []transaction.Attribute{{
|
||||||
|
Type: transaction.OracleResponseT,
|
||||||
|
Value: resp,
|
||||||
|
}}
|
||||||
|
tx.NetworkFee += 4_000_000 // multisig check
|
||||||
|
tx.SystemFee = int64(req.GasForResponse - uint64(tx.NetworkFee))
|
||||||
|
tx.Signers = []transaction.Signer{{
|
||||||
|
Account: oracleHash,
|
||||||
|
Scopes: transaction.FeeOnly,
|
||||||
|
}}
|
||||||
|
size := io.GetVarSize(tx)
|
||||||
|
netFee, sizeDelta := CalculateNetworkFee(oracleScript)
|
||||||
|
tx.NetworkFee += netFee
|
||||||
|
tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte()
|
||||||
|
return tx
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("NoOracleNodes", func(t *testing.T) {
|
||||||
|
tx := getOracleTx(t)
|
||||||
|
require.NoError(t, oracleAcc.SignTx(tx))
|
||||||
|
checkErr(t, ErrInvalidAttribute, tx)
|
||||||
|
})
|
||||||
|
|
||||||
|
txSetOracle := transaction.New(netmode.UnitTestNet, []byte{}, 0)
|
||||||
|
setSigner(txSetOracle, testchain.CommitteeScriptHash())
|
||||||
|
txSetOracle.Scripts = []transaction.Witness{{
|
||||||
|
InvocationScript: testchain.SignCommittee(txSetOracle.GetSignedPart()),
|
||||||
|
VerificationScript: testchain.CommitteeVerificationScript(),
|
||||||
|
}}
|
||||||
|
ic := bc.newInteropContext(trigger.All, bc.dao, nil, txSetOracle)
|
||||||
|
require.NoError(t, bc.contracts.Oracle.SetOracleNodes(ic, oraclePubs))
|
||||||
|
bc.contracts.Oracle.OnPersistEnd(ic.DAO)
|
||||||
|
_, err = ic.DAO.Persist()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("Valid", func(t *testing.T) {
|
||||||
|
tx := getOracleTx(t)
|
||||||
|
require.NoError(t, oracleAcc.SignTx(tx))
|
||||||
|
require.NoError(t, bc.VerifyTx(tx))
|
||||||
|
})
|
||||||
|
t.Run("InvalidRequestID", func(t *testing.T) {
|
||||||
|
tx := getOracleTx(t)
|
||||||
|
tx.Attributes[0].Value.(*transaction.OracleResponse).ID = 2
|
||||||
|
require.NoError(t, oracleAcc.SignTx(tx))
|
||||||
|
checkErr(t, ErrInvalidAttribute, tx)
|
||||||
|
})
|
||||||
|
t.Run("InvalidScope", func(t *testing.T) {
|
||||||
|
tx := getOracleTx(t)
|
||||||
|
tx.Signers[0].Scopes = transaction.Global
|
||||||
|
require.NoError(t, oracleAcc.SignTx(tx))
|
||||||
|
checkErr(t, ErrInvalidAttribute, tx)
|
||||||
|
})
|
||||||
|
t.Run("InvalidScript", func(t *testing.T) {
|
||||||
|
tx := getOracleTx(t)
|
||||||
|
tx.Script[0] = ^tx.Script[0]
|
||||||
|
require.NoError(t, oracleAcc.SignTx(tx))
|
||||||
|
checkErr(t, ErrInvalidAttribute, tx)
|
||||||
|
})
|
||||||
|
t.Run("InvalidSigner", func(t *testing.T) {
|
||||||
|
tx := getOracleTx(t)
|
||||||
|
tx.Signers[0].Account = accs[0].Contract.ScriptHash()
|
||||||
|
require.NoError(t, accs[0].SignTx(tx))
|
||||||
|
checkErr(t, ErrInvalidAttribute, tx)
|
||||||
|
})
|
||||||
|
t.Run("SmallFee", func(t *testing.T) {
|
||||||
|
tx := getOracleTx(t)
|
||||||
|
tx.SystemFee = 0
|
||||||
|
require.NoError(t, oracleAcc.SignTx(tx))
|
||||||
|
checkErr(t, ErrInvalidAttribute, tx)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,6 +82,7 @@ type Method = func(ic *Context, args []stackitem.Item) stackitem.Item
|
||||||
// MethodAndPrice is a native-contract method descriptor.
|
// MethodAndPrice is a native-contract method descriptor.
|
||||||
type MethodAndPrice struct {
|
type MethodAndPrice struct {
|
||||||
Func Method
|
Func Method
|
||||||
|
MD *manifest.Method
|
||||||
Price int64
|
Price int64
|
||||||
RequiredFlags smartcontract.CallFlag
|
RequiredFlags smartcontract.CallFlag
|
||||||
}
|
}
|
||||||
|
@ -123,6 +124,7 @@ func NewContractMD(name string) *ContractMD {
|
||||||
// AddMethod adds new method to a native contract.
|
// AddMethod adds new method to a native contract.
|
||||||
func (c *ContractMD) AddMethod(md *MethodAndPrice, desc *manifest.Method, safe bool) {
|
func (c *ContractMD) AddMethod(md *MethodAndPrice, desc *manifest.Method, safe bool) {
|
||||||
c.Manifest.ABI.Methods = append(c.Manifest.ABI.Methods, *desc)
|
c.Manifest.ABI.Methods = append(c.Manifest.ABI.Methods, *desc)
|
||||||
|
md.MD = desc
|
||||||
c.Methods[desc.Name] = *md
|
c.Methods[desc.Name] = *md
|
||||||
if safe {
|
if safe {
|
||||||
c.Manifest.SafeMethods.Add(desc.Name)
|
c.Manifest.SafeMethods.Add(desc.Name)
|
||||||
|
|
89
pkg/core/interop/contract/call.go
Normal file
89
pkg/core/interop/contract/call.go
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
package contract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/interop"
|
||||||
|
"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/stackitem"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Call calls a contract.
|
||||||
|
func Call(ic *interop.Context) error {
|
||||||
|
h := ic.VM.Estack().Pop().Bytes()
|
||||||
|
method := ic.VM.Estack().Pop().String()
|
||||||
|
args := ic.VM.Estack().Pop().Array()
|
||||||
|
return callExInternal(ic, h, method, args, smartcontract.All)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CallEx calls a contract with flags.
|
||||||
|
func CallEx(ic *interop.Context) error {
|
||||||
|
h := ic.VM.Estack().Pop().Bytes()
|
||||||
|
method := ic.VM.Estack().Pop().String()
|
||||||
|
args := ic.VM.Estack().Pop().Array()
|
||||||
|
flags := smartcontract.CallFlag(int32(ic.VM.Estack().Pop().BigInt().Int64()))
|
||||||
|
if flags&^smartcontract.All != 0 {
|
||||||
|
return errors.New("call flags out of range")
|
||||||
|
}
|
||||||
|
return callExInternal(ic, h, method, args, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func callExInternal(ic *interop.Context, h []byte, name string, args []stackitem.Item, f smartcontract.CallFlag) error {
|
||||||
|
u, err := util.Uint160DecodeBytesBE(h)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("invalid contract hash")
|
||||||
|
}
|
||||||
|
cs, err := ic.DAO.GetContractState(u)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("contract not found")
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(name, "_") {
|
||||||
|
return errors.New("invalid method name (starts with '_')")
|
||||||
|
}
|
||||||
|
md := cs.Manifest.ABI.GetMethod(name)
|
||||||
|
if md == nil {
|
||||||
|
return fmt.Errorf("method '%s' not found", name)
|
||||||
|
}
|
||||||
|
curr, err := ic.DAO.GetContractState(ic.VM.GetCurrentScriptHash())
|
||||||
|
if err == nil {
|
||||||
|
if !curr.Manifest.CanCall(&cs.Manifest, name) {
|
||||||
|
return errors.New("disallowed method call")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) != len(md.Parameters) {
|
||||||
|
return fmt.Errorf("invalid argument count: %d (expected %d)", len(args), len(md.Parameters))
|
||||||
|
}
|
||||||
|
|
||||||
|
ic.Invocations[u]++
|
||||||
|
ic.VM.LoadScriptWithHash(cs.Script, u, ic.VM.Context().GetCallFlags()&f)
|
||||||
|
var isNative bool
|
||||||
|
for i := range ic.Natives {
|
||||||
|
if ic.Natives[i].Metadata().Hash.Equals(u) {
|
||||||
|
isNative = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isNative {
|
||||||
|
ic.VM.Estack().PushVal(args)
|
||||||
|
ic.VM.Estack().PushVal(name)
|
||||||
|
} else {
|
||||||
|
for i := len(args) - 1; i >= 0; i-- {
|
||||||
|
ic.VM.Estack().PushVal(args[i])
|
||||||
|
}
|
||||||
|
// use Jump not Call here because context was loaded in LoadScript above.
|
||||||
|
ic.VM.Jump(ic.VM.Context(), md.Offset)
|
||||||
|
}
|
||||||
|
ic.VM.Context().CheckReturn = true
|
||||||
|
|
||||||
|
md = cs.Manifest.ABI.GetMethod(manifest.MethodInit)
|
||||||
|
if md != nil {
|
||||||
|
ic.VM.Call(ic.VM.Context(), md.Offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"math/big"
|
"math/big"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/block"
|
"github.com/nspcc-dev/neo-go/pkg/core/block"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/blockchainer"
|
"github.com/nspcc-dev/neo-go/pkg/core/blockchainer"
|
||||||
|
@ -15,8 +14,6 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/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/util"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm"
|
"github.com/nspcc-dev/neo-go/pkg/vm"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||||
|
@ -454,82 +451,6 @@ func storageContextAsReadOnly(ic *interop.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// contractCall calls a contract.
|
|
||||||
func contractCall(ic *interop.Context) error {
|
|
||||||
h := ic.VM.Estack().Pop().Bytes()
|
|
||||||
method := ic.VM.Estack().Pop().String()
|
|
||||||
args := ic.VM.Estack().Pop().Array()
|
|
||||||
return contractCallExInternal(ic, h, method, args, smartcontract.All)
|
|
||||||
}
|
|
||||||
|
|
||||||
// contractCallEx calls a contract with flags.
|
|
||||||
func contractCallEx(ic *interop.Context) error {
|
|
||||||
h := ic.VM.Estack().Pop().Bytes()
|
|
||||||
method := ic.VM.Estack().Pop().String()
|
|
||||||
args := ic.VM.Estack().Pop().Array()
|
|
||||||
flags := smartcontract.CallFlag(int32(ic.VM.Estack().Pop().BigInt().Int64()))
|
|
||||||
if flags&^smartcontract.All != 0 {
|
|
||||||
return errors.New("call flags out of range")
|
|
||||||
}
|
|
||||||
return contractCallExInternal(ic, h, method, args, flags)
|
|
||||||
}
|
|
||||||
|
|
||||||
func contractCallExInternal(ic *interop.Context, h []byte, name string, args []stackitem.Item, f smartcontract.CallFlag) error {
|
|
||||||
u, err := util.Uint160DecodeBytesBE(h)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("invalid contract hash")
|
|
||||||
}
|
|
||||||
cs, err := ic.DAO.GetContractState(u)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("contract not found")
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(name, "_") {
|
|
||||||
return errors.New("invalid method name (starts with '_')")
|
|
||||||
}
|
|
||||||
md := cs.Manifest.ABI.GetMethod(name)
|
|
||||||
if md == nil {
|
|
||||||
return fmt.Errorf("method '%s' not found", name)
|
|
||||||
}
|
|
||||||
curr, err := ic.DAO.GetContractState(ic.VM.GetCurrentScriptHash())
|
|
||||||
if err == nil {
|
|
||||||
if !curr.Manifest.CanCall(&cs.Manifest, name) {
|
|
||||||
return errors.New("disallowed method call")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) != len(md.Parameters) {
|
|
||||||
return fmt.Errorf("invalid argument count: %d (expected %d)", len(args), len(md.Parameters))
|
|
||||||
}
|
|
||||||
|
|
||||||
ic.Invocations[u]++
|
|
||||||
ic.VM.LoadScriptWithHash(cs.Script, u, ic.VM.Context().GetCallFlags()&f)
|
|
||||||
var isNative bool
|
|
||||||
for i := range ic.Natives {
|
|
||||||
if ic.Natives[i].Metadata().Hash.Equals(u) {
|
|
||||||
isNative = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if isNative {
|
|
||||||
ic.VM.Estack().PushVal(args)
|
|
||||||
ic.VM.Estack().PushVal(name)
|
|
||||||
} else {
|
|
||||||
for i := len(args) - 1; i >= 0; i-- {
|
|
||||||
ic.VM.Estack().PushVal(args[i])
|
|
||||||
}
|
|
||||||
// use Jump not Call here because context was loaded in LoadScript above.
|
|
||||||
ic.VM.Jump(ic.VM.Context(), md.Offset)
|
|
||||||
ic.VM.Context().CheckReturn = true
|
|
||||||
}
|
|
||||||
|
|
||||||
md = cs.Manifest.ABI.GetMethod(manifest.MethodInit)
|
|
||||||
if md != nil {
|
|
||||||
ic.VM.Call(ic.VM.Context(), md.Offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// contractDestroy destroys a contract.
|
// contractDestroy destroys a contract.
|
||||||
func contractDestroy(ic *interop.Context) error {
|
func contractDestroy(ic *interop.Context) error {
|
||||||
hash := ic.VM.GetCurrentScriptHash()
|
hash := ic.VM.GetCurrentScriptHash()
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop"
|
"github.com/nspcc-dev/neo-go/pkg/core/interop"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/callback"
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/callback"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/contract"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/runtime"
|
"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/state"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||||
|
@ -441,7 +442,7 @@ func TestContractCall(t *testing.T) {
|
||||||
ic.VM.Estack().PushVal(addArgs)
|
ic.VM.Estack().PushVal(addArgs)
|
||||||
ic.VM.Estack().PushVal("add")
|
ic.VM.Estack().PushVal("add")
|
||||||
ic.VM.Estack().PushVal(h.BytesBE())
|
ic.VM.Estack().PushVal(h.BytesBE())
|
||||||
require.NoError(t, contractCall(ic))
|
require.NoError(t, contract.Call(ic))
|
||||||
require.NoError(t, ic.VM.Run())
|
require.NoError(t, ic.VM.Run())
|
||||||
require.Equal(t, 2, ic.VM.Estack().Len())
|
require.Equal(t, 2, ic.VM.Estack().Len())
|
||||||
require.Equal(t, big.NewInt(3), ic.VM.Estack().Pop().Value())
|
require.Equal(t, big.NewInt(3), ic.VM.Estack().Pop().Value())
|
||||||
|
@ -454,7 +455,7 @@ func TestContractCall(t *testing.T) {
|
||||||
ic.VM.Estack().PushVal(addArgs)
|
ic.VM.Estack().PushVal(addArgs)
|
||||||
ic.VM.Estack().PushVal("add")
|
ic.VM.Estack().PushVal("add")
|
||||||
ic.VM.Estack().PushVal(h.BytesBE())
|
ic.VM.Estack().PushVal(h.BytesBE())
|
||||||
require.Error(t, contractCallEx(ic))
|
require.Error(t, contract.CallEx(ic))
|
||||||
})
|
})
|
||||||
|
|
||||||
runInvalid := func(args ...interface{}) func(t *testing.T) {
|
runInvalid := func(args ...interface{}) func(t *testing.T) {
|
||||||
|
@ -466,7 +467,7 @@ func TestContractCall(t *testing.T) {
|
||||||
// interops can both return error and panic,
|
// interops can both return error and panic,
|
||||||
// we don't care which kind of error has occurred
|
// we don't care which kind of error has occurred
|
||||||
require.Panics(t, func() {
|
require.Panics(t, func() {
|
||||||
err := contractCall(ic)
|
err := contract.Call(ic)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -491,7 +492,7 @@ func TestContractCall(t *testing.T) {
|
||||||
ic.VM.Estack().PushVal(stackitem.NewArray(nil))
|
ic.VM.Estack().PushVal(stackitem.NewArray(nil))
|
||||||
ic.VM.Estack().PushVal("invalidReturn")
|
ic.VM.Estack().PushVal("invalidReturn")
|
||||||
ic.VM.Estack().PushVal(h.BytesBE())
|
ic.VM.Estack().PushVal(h.BytesBE())
|
||||||
require.NoError(t, contractCall(ic))
|
require.NoError(t, contract.Call(ic))
|
||||||
require.Error(t, ic.VM.Run())
|
require.Error(t, ic.VM.Run())
|
||||||
})
|
})
|
||||||
t.Run("Void", func(t *testing.T) {
|
t.Run("Void", func(t *testing.T) {
|
||||||
|
@ -499,7 +500,7 @@ func TestContractCall(t *testing.T) {
|
||||||
ic.VM.Estack().PushVal(stackitem.NewArray(nil))
|
ic.VM.Estack().PushVal(stackitem.NewArray(nil))
|
||||||
ic.VM.Estack().PushVal("justReturn")
|
ic.VM.Estack().PushVal("justReturn")
|
||||||
ic.VM.Estack().PushVal(h.BytesBE())
|
ic.VM.Estack().PushVal(h.BytesBE())
|
||||||
require.NoError(t, contractCall(ic))
|
require.NoError(t, contract.Call(ic))
|
||||||
require.NoError(t, ic.VM.Run())
|
require.NoError(t, ic.VM.Run())
|
||||||
require.Equal(t, 2, ic.VM.Estack().Len())
|
require.Equal(t, 2, ic.VM.Estack().Len())
|
||||||
require.Equal(t, stackitem.Null{}, ic.VM.Estack().Pop().Item())
|
require.Equal(t, stackitem.Null{}, ic.VM.Estack().Pop().Item())
|
||||||
|
@ -512,7 +513,7 @@ func TestContractCall(t *testing.T) {
|
||||||
ic.VM.Estack().PushVal(stackitem.NewArray(nil))
|
ic.VM.Estack().PushVal(stackitem.NewArray(nil))
|
||||||
ic.VM.Estack().PushVal("drop")
|
ic.VM.Estack().PushVal("drop")
|
||||||
ic.VM.Estack().PushVal(h.BytesBE())
|
ic.VM.Estack().PushVal(h.BytesBE())
|
||||||
require.NoError(t, contractCall(ic))
|
require.NoError(t, contract.Call(ic))
|
||||||
require.Error(t, ic.VM.Run())
|
require.Error(t, ic.VM.Run())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -523,7 +524,7 @@ func TestContractCall(t *testing.T) {
|
||||||
ic.VM.Estack().PushVal(stackitem.NewArray([]stackitem.Item{stackitem.Make(5)}))
|
ic.VM.Estack().PushVal(stackitem.NewArray([]stackitem.Item{stackitem.Make(5)}))
|
||||||
ic.VM.Estack().PushVal("add3")
|
ic.VM.Estack().PushVal("add3")
|
||||||
ic.VM.Estack().PushVal(h.BytesBE())
|
ic.VM.Estack().PushVal(h.BytesBE())
|
||||||
require.NoError(t, contractCall(ic))
|
require.NoError(t, contract.Call(ic))
|
||||||
require.NoError(t, ic.VM.Run())
|
require.NoError(t, ic.VM.Run())
|
||||||
require.Equal(t, 2, ic.VM.Estack().Len())
|
require.Equal(t, 2, ic.VM.Estack().Len())
|
||||||
require.Equal(t, big.NewInt(8), ic.VM.Estack().Pop().Value())
|
require.Equal(t, big.NewInt(8), ic.VM.Estack().Pop().Value())
|
||||||
|
|
|
@ -10,6 +10,7 @@ package core
|
||||||
import (
|
import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop"
|
"github.com/nspcc-dev/neo-go/pkg/core/interop"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/callback"
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/callback"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/contract"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/crypto"
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/crypto"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/enumerator"
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/enumerator"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
|
||||||
|
@ -53,9 +54,9 @@ var systemInterops = []interop.Function{
|
||||||
{Name: interopnames.SystemCallbackCreateFromMethod, Func: callback.CreateFromMethod, Price: 1000000, ParamCount: 2, DisallowCallback: true},
|
{Name: interopnames.SystemCallbackCreateFromMethod, Func: callback.CreateFromMethod, Price: 1000000, ParamCount: 2, DisallowCallback: true},
|
||||||
{Name: interopnames.SystemCallbackCreateFromSyscall, Func: callback.CreateFromSyscall, Price: 400, ParamCount: 1, DisallowCallback: true},
|
{Name: interopnames.SystemCallbackCreateFromSyscall, Func: callback.CreateFromSyscall, Price: 400, ParamCount: 1, DisallowCallback: true},
|
||||||
{Name: interopnames.SystemCallbackInvoke, Func: callback.Invoke, Price: 1000000, ParamCount: 2, DisallowCallback: true},
|
{Name: interopnames.SystemCallbackInvoke, Func: callback.Invoke, Price: 1000000, ParamCount: 2, DisallowCallback: true},
|
||||||
{Name: interopnames.SystemContractCall, Func: contractCall, Price: 1000000,
|
{Name: interopnames.SystemContractCall, Func: contract.Call, Price: 1000000,
|
||||||
RequiredFlags: smartcontract.AllowCall, ParamCount: 3, DisallowCallback: true},
|
RequiredFlags: smartcontract.AllowCall, ParamCount: 3, DisallowCallback: true},
|
||||||
{Name: interopnames.SystemContractCallEx, Func: contractCallEx, Price: 1000000,
|
{Name: interopnames.SystemContractCallEx, Func: contract.CallEx, Price: 1000000,
|
||||||
RequiredFlags: smartcontract.AllowCall, ParamCount: 4, DisallowCallback: true},
|
RequiredFlags: smartcontract.AllowCall, ParamCount: 4, DisallowCallback: true},
|
||||||
{Name: interopnames.SystemContractCreate, Func: contractCreate, Price: 0,
|
{Name: interopnames.SystemContractCreate, Func: contractCreate, Price: 0,
|
||||||
RequiredFlags: smartcontract.AllowModifyStates, ParamCount: 2, DisallowCallback: true},
|
RequiredFlags: smartcontract.AllowModifyStates, ParamCount: 2, DisallowCallback: true},
|
||||||
|
|
|
@ -16,6 +16,7 @@ type Contracts struct {
|
||||||
NEO *NEO
|
NEO *NEO
|
||||||
GAS *GAS
|
GAS *GAS
|
||||||
Policy *Policy
|
Policy *Policy
|
||||||
|
Oracle *Oracle
|
||||||
Contracts []interop.Contract
|
Contracts []interop.Contract
|
||||||
// persistScript is vm script which executes "onPersist" method of every native contract.
|
// persistScript is vm script which executes "onPersist" method of every native contract.
|
||||||
persistScript []byte
|
persistScript []byte
|
||||||
|
@ -51,6 +52,12 @@ func NewContracts() *Contracts {
|
||||||
policy := newPolicy()
|
policy := newPolicy()
|
||||||
cs.Policy = policy
|
cs.Policy = policy
|
||||||
cs.Contracts = append(cs.Contracts, 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
|
return cs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +71,7 @@ func (cs *Contracts) GetPersistScript() []byte {
|
||||||
md := cs.Contracts[i].Metadata()
|
md := cs.Contracts[i].Metadata()
|
||||||
// Not every contract is persisted:
|
// Not every contract is persisted:
|
||||||
// https://github.com/neo-project/neo/blob/master/src/neo/Ledger/Blockchain.cs#L90
|
// 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
|
continue
|
||||||
}
|
}
|
||||||
emit.Int(w.BinWriter, 0)
|
emit.Int(w.BinWriter, 0)
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop"
|
"github.com/nspcc-dev/neo-go/pkg/core/interop"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Deploy deploys native contract.
|
// Deploy deploys native contract.
|
||||||
|
@ -62,6 +63,8 @@ func Call(ic *interop.Context) error {
|
||||||
return errors.New("gas limit exceeded")
|
return errors.New("gas limit exceeded")
|
||||||
}
|
}
|
||||||
result := m.Func(ic, args)
|
result := m.Func(ic, args)
|
||||||
|
if m.MD.ReturnType != smartcontract.VoidType {
|
||||||
ic.VM.Estack().PushVal(result)
|
ic.VM.Estack().PushVal(result)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,8 @@ type NEO struct {
|
||||||
// (every 28 blocks for mainnet). It's value
|
// (every 28 blocks for mainnet). It's value
|
||||||
// is always equal to value stored by `prefixCommittee`.
|
// is always equal to value stored by `prefixCommittee`.
|
||||||
committee atomic.Value
|
committee atomic.Value
|
||||||
|
// committeeHash contains script hash of the committee.
|
||||||
|
committeeHash atomic.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
// keyWithVotes is a serialized key with votes balance. It's not deserialized
|
// keyWithVotes is a serialized key with votes balance. It's not deserialized
|
||||||
|
@ -99,6 +101,7 @@ func NewNEO() *NEO {
|
||||||
n.nextValidators.Store(keys.PublicKeys(nil))
|
n.nextValidators.Store(keys.PublicKeys(nil))
|
||||||
n.validators.Store(keys.PublicKeys(nil))
|
n.validators.Store(keys.PublicKeys(nil))
|
||||||
n.committee.Store(keys.PublicKeys(nil))
|
n.committee.Store(keys.PublicKeys(nil))
|
||||||
|
n.committeeHash.Store(util.Uint160{})
|
||||||
|
|
||||||
onp := n.Methods["onPersist"]
|
onp := n.Methods["onPersist"]
|
||||||
onp.Func = getOnPersistWrapper(n.onPersist)
|
onp.Func = getOnPersistWrapper(n.onPersist)
|
||||||
|
@ -166,9 +169,14 @@ func (n *NEO) Initialize(ic *interop.Context) error {
|
||||||
|
|
||||||
committee := ic.Chain.GetStandByCommittee()
|
committee := ic.Chain.GetStandByCommittee()
|
||||||
n.committee.Store(committee)
|
n.committee.Store(committee)
|
||||||
|
script, err := smartcontract.CreateMajorityMultiSigRedeemScript(committee.Copy())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
n.committeeHash.Store(hash.Hash160(script))
|
||||||
n.updateNextValidators(committee, ic.Chain)
|
n.updateNextValidators(committee, ic.Chain)
|
||||||
|
|
||||||
err := ic.DAO.PutStorageItem(n.ContractID, prefixCommittee, &state.StorageItem{Value: committee.Bytes()})
|
err = ic.DAO.PutStorageItem(n.ContractID, prefixCommittee, &state.StorageItem{Value: committee.Bytes()})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -212,6 +220,11 @@ func (n *NEO) updateCommittee(ic *interop.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
n.committee.Store(committee)
|
n.committee.Store(committee)
|
||||||
|
script, err := smartcontract.CreateMajorityMultiSigRedeemScript(committee.Copy())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
n.committeeHash.Store(hash.Hash160(script))
|
||||||
n.updateNextValidators(committee, ic.Chain)
|
n.updateNextValidators(committee, ic.Chain)
|
||||||
n.votesChanged.Store(false)
|
n.votesChanged.Store(false)
|
||||||
si := &state.StorageItem{Value: committee.Bytes()}
|
si := &state.StorageItem{Value: committee.Bytes()}
|
||||||
|
@ -332,13 +345,8 @@ func (n *NEO) GetGASPerBlock(ic *interop.Context, index uint32) (*big.Int, error
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCommitteeAddress returns address of the committee.
|
// GetCommitteeAddress returns address of the committee.
|
||||||
func (n *NEO) GetCommitteeAddress(bc blockchainer.Blockchainer, d dao.DAO) (util.Uint160, error) {
|
func (n *NEO) GetCommitteeAddress() util.Uint160 {
|
||||||
pubs := n.GetCommitteeMembers()
|
return n.committeeHash.Load().(util.Uint160)
|
||||||
script, err := smartcontract.CreateMajorityMultiSigRedeemScript(pubs)
|
|
||||||
if err != nil {
|
|
||||||
return util.Uint160{}, err
|
|
||||||
}
|
|
||||||
return hash.Hash160(script), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NEO) setGASPerBlock(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
func (n *NEO) setGASPerBlock(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||||
|
@ -355,10 +363,7 @@ func (n *NEO) SetGASPerBlock(ic *interop.Context, index uint32, gas *big.Int) (b
|
||||||
if gas.Sign() == -1 || gas.Cmp(big.NewInt(10*GASFactor)) == 1 {
|
if gas.Sign() == -1 || gas.Cmp(big.NewInt(10*GASFactor)) == 1 {
|
||||||
return false, errors.New("invalid value for GASPerBlock")
|
return false, errors.New("invalid value for GASPerBlock")
|
||||||
}
|
}
|
||||||
h, err := n.GetCommitteeAddress(ic.Chain, ic.DAO)
|
h := n.GetCommitteeAddress()
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
ok, err := runtime.CheckHashedWitness(ic, h)
|
ok, err := runtime.CheckHashedWitness(ic, h)
|
||||||
if err != nil || !ok {
|
if err != nil || !ok {
|
||||||
return ok, err
|
return ok, err
|
||||||
|
|
445
pkg/core/native/oracle.go
Normal file
445
pkg/core/native/oracle.go
Normal file
|
@ -0,0 +1,445 @@
|
||||||
|
package native
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"math/big"
|
||||||
|
"sort"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"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
|
||||||
|
|
||||||
|
// nodesChanged is true if `SetOracleNodes` was called.
|
||||||
|
nodesChanged atomic.Value
|
||||||
|
// nodes contains cached list of oracle nodes.
|
||||||
|
nodes atomic.Value
|
||||||
|
// oracleHash contains cached oracle script hash.
|
||||||
|
oracleHash atomic.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = o.GetOracleNodes()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
o.nodes.Store(keys.PublicKeys(nil))
|
||||||
|
o.nodesChanged.Store(false)
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
return o.PutRequestInternal(id, req, ic.DAO)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutRequestInternal puts oracle request with the specified id to d.
|
||||||
|
func (o *Oracle) PutRequestInternal(id uint64, req *OracleRequest, d dao.DAO) error {
|
||||||
|
reqItem := &state.StorageItem{Value: req.Bytes()}
|
||||||
|
reqKey := makeRequestKey(id)
|
||||||
|
if err := d.PutStorageItem(o.ContractID, reqKey, reqItem); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add request ID to the id list.
|
||||||
|
lst := new(IDList)
|
||||||
|
key := makeIDListKey(req.URL)
|
||||||
|
if err := o.getSerializableFromDAO(d, key, lst); err != nil && !errors.Is(err, storage.ErrKeyNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*lst = append(*lst, id)
|
||||||
|
si := &state.StorageItem{Value: lst.Bytes()}
|
||||||
|
return d.PutStorageItem(o.ContractID, key, si)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Oracle) getOracleNodes(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
|
||||||
|
pubs := o.GetOracleNodes()
|
||||||
|
return pubsToArray(pubs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOracleNodes returns public keys of oracle nodes.
|
||||||
|
func (o *Oracle) GetOracleNodes() keys.PublicKeys {
|
||||||
|
return o.nodes.Load().(keys.PublicKeys).Copy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScriptHash returns script hash or oracle nodes.
|
||||||
|
func (o *Oracle) GetScriptHash() (util.Uint160, error) {
|
||||||
|
h := o.oracleHash.Load()
|
||||||
|
if h == nil {
|
||||||
|
return util.Uint160{}, storage.ErrKeyNotFound
|
||||||
|
}
|
||||||
|
return h.(util.Uint160), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
o.nodesChanged.Store(true)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnPersistEnd updates cached Oracle values if they've been changed
|
||||||
|
func (o *Oracle) OnPersistEnd(d dao.DAO) {
|
||||||
|
if !o.nodesChanged.Load().(bool) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ns := new(NodeList)
|
||||||
|
_ = o.getSerializableFromDAO(d, prefixNodeList, ns)
|
||||||
|
o.nodes.Store(keys.PublicKeys(*ns))
|
||||||
|
script, _ := smartcontract.CreateMajorityMultiSigRedeemScript(keys.PublicKeys(*ns).Copy())
|
||||||
|
o.oracleHash.Store(hash.Hash160(script))
|
||||||
|
o.nodesChanged.Store(false)
|
||||||
|
return
|
||||||
|
}
|
239
pkg/core/native/oracle_types.go
Normal file
239
pkg/core/native/oracle_types.go
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
package native
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/elliptic"
|
||||||
|
"errors"
|
||||||
|
"math/big"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IDList is a list of oracle request IDs.
|
||||||
|
type IDList []uint64
|
||||||
|
|
||||||
|
// NodeList represents list or oracle nodes.
|
||||||
|
type NodeList keys.PublicKeys
|
||||||
|
|
||||||
|
// OracleRequest represents oracle request.
|
||||||
|
type OracleRequest struct {
|
||||||
|
OriginalTxID util.Uint256
|
||||||
|
GasForResponse uint64
|
||||||
|
URL string
|
||||||
|
Filter *string
|
||||||
|
CallbackContract util.Uint160
|
||||||
|
CallbackMethod string
|
||||||
|
UserData []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes return l serizalized to a byte-slice.
|
||||||
|
func (l IDList) Bytes() []byte {
|
||||||
|
w := io.NewBufBinWriter()
|
||||||
|
l.EncodeBinary(w.BinWriter)
|
||||||
|
return w.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeBinary implements io.Serializable.
|
||||||
|
func (l IDList) EncodeBinary(w *io.BinWriter) {
|
||||||
|
stackitem.EncodeBinaryStackItem(l.toStackItem(), w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeBinary implements io.Serializable.
|
||||||
|
func (l *IDList) DecodeBinary(r *io.BinReader) {
|
||||||
|
item := stackitem.DecodeBinaryStackItem(r)
|
||||||
|
if r.Err != nil || item == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.Err = l.fromStackItem(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l IDList) toStackItem() stackitem.Item {
|
||||||
|
arr := make([]stackitem.Item, len(l))
|
||||||
|
for i := range l {
|
||||||
|
arr[i] = stackitem.NewBigInteger(new(big.Int).SetUint64(l[i]))
|
||||||
|
}
|
||||||
|
return stackitem.NewArray(arr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *IDList) fromStackItem(it stackitem.Item) error {
|
||||||
|
arr, ok := it.Value().([]stackitem.Item)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("not an array")
|
||||||
|
}
|
||||||
|
*l = make(IDList, len(arr))
|
||||||
|
for i := range arr {
|
||||||
|
bi, err := arr[i].TryInteger()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
(*l)[i] = bi.Uint64()
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
l.EncodeBinary(w.BinWriter)
|
||||||
|
return w.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeBinary implements io.Serializable.
|
||||||
|
func (l NodeList) EncodeBinary(w *io.BinWriter) {
|
||||||
|
stackitem.EncodeBinaryStackItem(l.toStackItem(), w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeBinary implements io.Serializable.
|
||||||
|
func (l *NodeList) DecodeBinary(r *io.BinReader) {
|
||||||
|
item := stackitem.DecodeBinaryStackItem(r)
|
||||||
|
if r.Err != nil || item == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.Err = l.fromStackItem(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l NodeList) toStackItem() stackitem.Item {
|
||||||
|
arr := make([]stackitem.Item, len(l))
|
||||||
|
for i := range l {
|
||||||
|
arr[i] = stackitem.NewByteArray(l[i].Bytes())
|
||||||
|
}
|
||||||
|
return stackitem.NewArray(arr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *NodeList) fromStackItem(it stackitem.Item) error {
|
||||||
|
arr, ok := it.Value().([]stackitem.Item)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("not an array")
|
||||||
|
}
|
||||||
|
*l = make(NodeList, len(arr))
|
||||||
|
for i := range arr {
|
||||||
|
bs, err := arr[i].TryBytes()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pub, err := keys.NewPublicKeyFromBytes(bs, elliptic.P256())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
(*l)[i] = pub
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes return o serizalized to a byte-slice.
|
||||||
|
func (o *OracleRequest) Bytes() []byte {
|
||||||
|
w := io.NewBufBinWriter()
|
||||||
|
o.EncodeBinary(w.BinWriter)
|
||||||
|
return w.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeBinary implements io.Serializable.
|
||||||
|
func (o *OracleRequest) EncodeBinary(w *io.BinWriter) {
|
||||||
|
stackitem.EncodeBinaryStackItem(o.toStackItem(), w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeBinary implements io.Serializable.
|
||||||
|
func (o *OracleRequest) DecodeBinary(r *io.BinReader) {
|
||||||
|
item := stackitem.DecodeBinaryStackItem(r)
|
||||||
|
if r.Err != nil || item == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.Err = o.fromStackItem(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OracleRequest) toStackItem() stackitem.Item {
|
||||||
|
filter := stackitem.Item(stackitem.Null{})
|
||||||
|
if o.Filter != nil {
|
||||||
|
filter = stackitem.Make(*o.Filter)
|
||||||
|
}
|
||||||
|
return stackitem.NewArray([]stackitem.Item{
|
||||||
|
stackitem.NewByteArray(o.OriginalTxID.BytesBE()),
|
||||||
|
stackitem.NewBigInteger(new(big.Int).SetUint64(o.GasForResponse)),
|
||||||
|
stackitem.Make(o.URL),
|
||||||
|
filter,
|
||||||
|
stackitem.NewByteArray(o.CallbackContract.BytesBE()),
|
||||||
|
stackitem.Make(o.CallbackMethod),
|
||||||
|
stackitem.NewByteArray(o.UserData),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OracleRequest) fromStackItem(it stackitem.Item) error {
|
||||||
|
arr, ok := it.Value().([]stackitem.Item)
|
||||||
|
if !ok || len(arr) < 7 {
|
||||||
|
return errors.New("not an array of needed length")
|
||||||
|
}
|
||||||
|
bs, err := arr[0].TryBytes()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.OriginalTxID, err = util.Uint256DecodeBytesBE(bs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
gas, err := arr[1].TryInteger()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.GasForResponse = gas.Uint64()
|
||||||
|
|
||||||
|
s, isNull, ok := itemToString(arr[2])
|
||||||
|
if !ok || isNull {
|
||||||
|
return errors.New("invalid URL")
|
||||||
|
}
|
||||||
|
o.URL = s
|
||||||
|
|
||||||
|
s, isNull, ok = itemToString(arr[3])
|
||||||
|
if !ok {
|
||||||
|
return errors.New("invalid filter")
|
||||||
|
} else if !isNull {
|
||||||
|
filter := s
|
||||||
|
o.Filter = &filter
|
||||||
|
}
|
||||||
|
|
||||||
|
bs, err = arr[4].TryBytes()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.CallbackContract, err = util.Uint160DecodeBytesBE(bs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.CallbackMethod, isNull, ok = itemToString(arr[5])
|
||||||
|
if !ok || isNull {
|
||||||
|
return errors.New("invalid callback method")
|
||||||
|
}
|
||||||
|
|
||||||
|
o.UserData, err = arr[6].TryBytes()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func itemToString(it stackitem.Item) (string, bool, bool) {
|
||||||
|
_, ok := it.(stackitem.Null)
|
||||||
|
if ok {
|
||||||
|
return "", true, true
|
||||||
|
}
|
||||||
|
bs, err := it.TryBytes()
|
||||||
|
if err != nil || !utf8.Valid(bs) {
|
||||||
|
return "", false, false
|
||||||
|
}
|
||||||
|
return string(bs), false, true
|
||||||
|
}
|
141
pkg/core/native/oracle_types_test.go
Normal file
141
pkg/core/native/oracle_types_test.go
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
package native
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/big"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/internal/random"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/internal/testserdes"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getInvalidTestFunc(actual io.Serializable, value interface{}) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
w := io.NewBufBinWriter()
|
||||||
|
it := stackitem.Make(value)
|
||||||
|
stackitem.EncodeBinaryStackItem(it, w.BinWriter)
|
||||||
|
require.NoError(t, w.Err)
|
||||||
|
require.Error(t, testserdes.DecodeBinary(w.Bytes(), actual))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIDList_EncodeBinary(t *testing.T) {
|
||||||
|
t.Run("Valid", func(t *testing.T) {
|
||||||
|
l := &IDList{1, 4, 5}
|
||||||
|
testserdes.EncodeDecodeBinary(t, l, new(IDList))
|
||||||
|
})
|
||||||
|
t.Run("Invalid", func(t *testing.T) {
|
||||||
|
t.Run("NotArray", getInvalidTestFunc(new(IDList), []byte{}))
|
||||||
|
t.Run("InvalidElement", getInvalidTestFunc(new(IDList), []stackitem.Item{stackitem.Null{}}))
|
||||||
|
t.Run("NotStackItem", func(t *testing.T) {
|
||||||
|
require.Error(t, testserdes.DecodeBinary([]byte{0x77}, new(IDList)))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
pub := priv.PublicKey()
|
||||||
|
|
||||||
|
t.Run("Valid", func(t *testing.T) {
|
||||||
|
l := &NodeList{pub}
|
||||||
|
testserdes.EncodeDecodeBinary(t, l, new(NodeList))
|
||||||
|
})
|
||||||
|
t.Run("Invalid", func(t *testing.T) {
|
||||||
|
t.Run("NotArray", getInvalidTestFunc(new(NodeList), []byte{}))
|
||||||
|
t.Run("InvalidElement", getInvalidTestFunc(new(NodeList), []stackitem.Item{stackitem.Null{}}))
|
||||||
|
t.Run("InvalidKey", getInvalidTestFunc(new(NodeList),
|
||||||
|
[]stackitem.Item{stackitem.NewByteArray([]byte{0x9})}))
|
||||||
|
t.Run("NotStackItem", func(t *testing.T) {
|
||||||
|
require.Error(t, testserdes.DecodeBinary([]byte{0x77}, new(NodeList)))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOracleRequest_EncodeBinary(t *testing.T) {
|
||||||
|
t.Run("Valid", func(t *testing.T) {
|
||||||
|
r := &OracleRequest{
|
||||||
|
OriginalTxID: random.Uint256(),
|
||||||
|
GasForResponse: 12345,
|
||||||
|
URL: "https://get.value",
|
||||||
|
CallbackContract: random.Uint160(),
|
||||||
|
CallbackMethod: "method",
|
||||||
|
UserData: []byte{1, 2, 3},
|
||||||
|
}
|
||||||
|
testserdes.EncodeDecodeBinary(t, r, new(OracleRequest))
|
||||||
|
|
||||||
|
t.Run("WithFilter", func(t *testing.T) {
|
||||||
|
s := "filter"
|
||||||
|
r.Filter = &s
|
||||||
|
testserdes.EncodeDecodeBinary(t, r, new(OracleRequest))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
t.Run("Invalid", func(t *testing.T) {
|
||||||
|
w := io.NewBufBinWriter()
|
||||||
|
t.Run("NotArray", func(t *testing.T) {
|
||||||
|
w.Reset()
|
||||||
|
it := stackitem.NewByteArray([]byte{})
|
||||||
|
stackitem.EncodeBinaryStackItem(it, w.BinWriter)
|
||||||
|
require.Error(t, testserdes.DecodeBinary(w.Bytes(), new(OracleRequest)))
|
||||||
|
})
|
||||||
|
t.Run("NotStackItem", func(t *testing.T) {
|
||||||
|
w.Reset()
|
||||||
|
require.Error(t, testserdes.DecodeBinary([]byte{0x77}, new(OracleRequest)))
|
||||||
|
})
|
||||||
|
|
||||||
|
items := []stackitem.Item{
|
||||||
|
stackitem.NewByteArray(random.Uint256().BytesBE()),
|
||||||
|
stackitem.NewBigInteger(big.NewInt(123)),
|
||||||
|
stackitem.Make("url"),
|
||||||
|
stackitem.Null{},
|
||||||
|
stackitem.NewByteArray(random.Uint160().BytesBE()),
|
||||||
|
stackitem.Make("method"),
|
||||||
|
stackitem.NewByteArray([]byte{1, 2, 3}),
|
||||||
|
}
|
||||||
|
arrItem := stackitem.NewArray(items)
|
||||||
|
runInvalid := func(i int, elem stackitem.Item) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
w.Reset()
|
||||||
|
before := items[i]
|
||||||
|
items[i] = elem
|
||||||
|
stackitem.EncodeBinaryStackItem(arrItem, w.BinWriter)
|
||||||
|
items[i] = before
|
||||||
|
require.Error(t, testserdes.DecodeBinary(w.Bytes(), new(OracleRequest)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Run("TxID", func(t *testing.T) {
|
||||||
|
t.Run("Type", runInvalid(0, stackitem.NewMap()))
|
||||||
|
t.Run("Length", runInvalid(0, stackitem.NewByteArray([]byte{0, 1, 2})))
|
||||||
|
})
|
||||||
|
t.Run("Gas", runInvalid(1, stackitem.NewMap()))
|
||||||
|
t.Run("URL", runInvalid(2, stackitem.NewMap()))
|
||||||
|
t.Run("Filter", runInvalid(3, stackitem.NewMap()))
|
||||||
|
t.Run("Contract", func(t *testing.T) {
|
||||||
|
t.Run("Type", runInvalid(4, stackitem.NewMap()))
|
||||||
|
t.Run("Length", runInvalid(4, stackitem.NewByteArray([]byte{0, 1, 2})))
|
||||||
|
})
|
||||||
|
t.Run("Method", runInvalid(5, stackitem.NewMap()))
|
||||||
|
t.Run("UserData", runInvalid(6, stackitem.NewMap()))
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/dao"
|
"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"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/contract"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/native"
|
"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/state"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/storage"
|
"github.com/nspcc-dev/neo-go/pkg/core/storage"
|
||||||
|
@ -131,7 +132,7 @@ func (tn *testNative) callOtherContractWithoutArgs(ic *interop.Context, args []s
|
||||||
vm.Estack().PushVal(stackitem.NewArray([]stackitem.Item{})) // no args
|
vm.Estack().PushVal(stackitem.NewArray([]stackitem.Item{})) // no args
|
||||||
vm.Estack().PushVal(args[1]) // method
|
vm.Estack().PushVal(args[1]) // method
|
||||||
vm.Estack().PushVal(args[0]) // contract hash
|
vm.Estack().PushVal(args[0]) // contract hash
|
||||||
err := contractCall(ic)
|
err := contract.Call(ic)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return stackitem.NewBigInteger(big.NewInt(-1))
|
return stackitem.NewBigInteger(big.NewInt(-1))
|
||||||
}
|
}
|
||||||
|
@ -147,7 +148,7 @@ func (tn *testNative) callOtherContractWithArg(ic *interop.Context, args []stack
|
||||||
vm.Estack().PushVal(stackitem.NewArray([]stackitem.Item{args[2]})) // arg
|
vm.Estack().PushVal(stackitem.NewArray([]stackitem.Item{args[2]})) // arg
|
||||||
vm.Estack().PushVal(args[1]) // method
|
vm.Estack().PushVal(args[1]) // method
|
||||||
vm.Estack().PushVal(args[0]) // contract hash
|
vm.Estack().PushVal(args[0]) // contract hash
|
||||||
err := contractCall(ic)
|
err := contract.Call(ic)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return stackitem.NewBigInteger(big.NewInt(-1))
|
return stackitem.NewBigInteger(big.NewInt(-1))
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,9 +128,7 @@ func TestNEO_SetGasPerBlock(t *testing.T) {
|
||||||
ic := bc.newInteropContext(trigger.System, bc.dao, nil, tx)
|
ic := bc.newInteropContext(trigger.System, bc.dao, nil, tx)
|
||||||
ic.VM = vm.New()
|
ic.VM = vm.New()
|
||||||
|
|
||||||
h, err := neo.GetCommitteeAddress(bc, bc.dao)
|
h := neo.GetCommitteeAddress()
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
t.Run("Default", func(t *testing.T) {
|
t.Run("Default", func(t *testing.T) {
|
||||||
g, err := neo.GetGASPerBlock(ic, 0)
|
g, err := neo.GetGASPerBlock(ic, 0)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -189,9 +187,7 @@ func TestNEO_CalculateBonus(t *testing.T) {
|
||||||
require.EqualValues(t, 0, res.Int64())
|
require.EqualValues(t, 0, res.Int64())
|
||||||
})
|
})
|
||||||
t.Run("ManyBlocks", func(t *testing.T) {
|
t.Run("ManyBlocks", func(t *testing.T) {
|
||||||
h, err := neo.GetCommitteeAddress(bc, bc.dao)
|
setSigner(tx, neo.GetCommitteeAddress())
|
||||||
require.NoError(t, err)
|
|
||||||
setSigner(tx, h)
|
|
||||||
ok, err := neo.SetGASPerBlock(ic, 10, big.NewInt(1*native.GASFactor))
|
ok, err := neo.SetGASPerBlock(ic, 10, big.NewInt(1*native.GASFactor))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
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)
|
||||||
|
orc.OnPersistEnd(ic.DAO)
|
||||||
|
|
||||||
|
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 := orc.GetOracleNodes()
|
||||||
|
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}))
|
||||||
|
orc.OnPersistEnd(ic.DAO)
|
||||||
|
|
||||||
|
pubs = orc.GetOracleNodes()
|
||||||
|
require.Equal(t, keys.PublicKeys{pub}, pubs)
|
||||||
|
}
|
Loading…
Reference in a new issue