github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/pkg/core/native/native_test/oracle_test.go (about)

     1  package native_test
     2  
     3  import (
     4  	"encoding/json"
     5  	"math"
     6  	"math/big"
     7  	"path/filepath"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/nspcc-dev/neo-go/internal/contracts"
    12  	"github.com/nspcc-dev/neo-go/pkg/core/native"
    13  	"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
    14  	"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
    15  	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
    16  	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
    17  	"github.com/nspcc-dev/neo-go/pkg/neotest"
    18  	"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
    19  	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
    20  	"github.com/stretchr/testify/require"
    21  )
    22  
    23  var pathToInternalContracts = filepath.Join("..", "..", "..", "..", "internal", "contracts")
    24  
    25  func newOracleClient(t *testing.T) *neotest.ContractInvoker {
    26  	return newNativeClient(t, nativenames.Oracle)
    27  }
    28  
    29  func TestOracle_GetSetPrice(t *testing.T) {
    30  	testGetSet(t, newOracleClient(t), "Price", native.DefaultOracleRequestPrice, 1, math.MaxInt64)
    31  }
    32  
    33  func TestOracle_GetSetPriceCache(t *testing.T) {
    34  	testGetSetCache(t, newOracleClient(t), "Price", native.DefaultOracleRequestPrice)
    35  }
    36  
    37  func putOracleRequest(t *testing.T, oracleInvoker *neotest.ContractInvoker,
    38  	url string, filter *string, cb string, userData []byte, gas int64, errStr ...string) {
    39  	var filtItem any
    40  	if filter != nil {
    41  		filtItem = *filter
    42  	}
    43  	if len(errStr) == 0 {
    44  		oracleInvoker.Invoke(t, stackitem.Null{}, "requestURL", url, filtItem, cb, userData, gas)
    45  		return
    46  	}
    47  	oracleInvoker.InvokeFail(t, errStr[0], "requestURL", url, filtItem, cb, userData, gas)
    48  }
    49  
    50  func TestOracle_Request(t *testing.T) {
    51  	oracleCommitteeInvoker := newOracleClient(t)
    52  	e := oracleCommitteeInvoker.Executor
    53  	managementCommitteeInvoker := e.CommitteeInvoker(e.NativeHash(t, nativenames.Management))
    54  	designationCommitteeInvoker := e.CommitteeInvoker(e.NativeHash(t, nativenames.Designation))
    55  	gasCommitteeInvoker := e.CommitteeInvoker(e.NativeHash(t, nativenames.Gas))
    56  
    57  	cs := contracts.GetOracleContractState(t, pathToInternalContracts, e.Validator.ScriptHash(), 1)
    58  	nBytes, err := cs.NEF.Bytes()
    59  	require.NoError(t, err)
    60  	mBytes, err := json.Marshal(cs.Manifest)
    61  	require.NoError(t, err)
    62  	expected, err := cs.ToStackItem()
    63  	require.NoError(t, err)
    64  	managementCommitteeInvoker.Invoke(t, expected, "deploy", nBytes, mBytes)
    65  	helperValidatorInvoker := e.ValidatorInvoker(cs.Hash)
    66  
    67  	gasForResponse := int64(2000_1234)
    68  	var filter = "flt"
    69  	userData := []byte("custom info")
    70  	putOracleRequest(t, helperValidatorInvoker, "url", &filter, "handle", userData, gasForResponse)
    71  
    72  	// Designate single Oracle node.
    73  	oracleNode := e.NewAccount(t)
    74  	designationCommitteeInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", int(noderoles.Oracle), []any{oracleNode.(neotest.SingleSigner).Account().PublicKey().Bytes()})
    75  	err = oracleNode.(neotest.SingleSigner).Account().ConvertMultisig(1, []*keys.PublicKey{oracleNode.(neotest.SingleSigner).Account().PublicKey()})
    76  	require.NoError(t, err)
    77  	oracleNodeMulti := neotest.NewMultiSigner(oracleNode.(neotest.SingleSigner).Account())
    78  	gasCommitteeInvoker.Invoke(t, true, "transfer", gasCommitteeInvoker.CommitteeHash, oracleNodeMulti.ScriptHash(), 100_0000_0000, nil)
    79  
    80  	// Finish.
    81  	prepareResponseTx := func(t *testing.T, requestID uint64) *transaction.Transaction {
    82  		script := native.CreateOracleResponseScript(oracleCommitteeInvoker.Hash)
    83  
    84  		tx := transaction.New(script, 1000_0000)
    85  		tx.Nonce = neotest.Nonce()
    86  		tx.ValidUntilBlock = e.Chain.BlockHeight() + 1
    87  		tx.Attributes = []transaction.Attribute{{
    88  			Type: transaction.OracleResponseT,
    89  			Value: &transaction.OracleResponse{
    90  				ID:     requestID,
    91  				Code:   transaction.Success,
    92  				Result: []byte{4, 8, 15, 16, 23, 42},
    93  			},
    94  		}}
    95  		tx.Signers = []transaction.Signer{
    96  			{
    97  				Account: oracleNodeMulti.ScriptHash(),
    98  				Scopes:  transaction.None,
    99  			},
   100  			{
   101  				Account: oracleCommitteeInvoker.Hash,
   102  				Scopes:  transaction.None,
   103  			},
   104  		}
   105  		tx.NetworkFee = 1000_1234
   106  		tx.Scripts = []transaction.Witness{
   107  			{
   108  				InvocationScript:   oracleNodeMulti.SignHashable(uint32(e.Chain.GetConfig().Magic), tx),
   109  				VerificationScript: oracleNodeMulti.Script(),
   110  			},
   111  			{
   112  				InvocationScript:   []byte{},
   113  				VerificationScript: []byte{},
   114  			},
   115  		}
   116  		return tx
   117  	}
   118  	tx := prepareResponseTx(t, 0)
   119  	e.AddNewBlock(t, tx)
   120  	e.CheckHalt(t, tx.Hash(), stackitem.Null{})
   121  
   122  	// Ensure that callback was called.
   123  	si := e.Chain.GetStorageItem(cs.ID, []byte("lastOracleResponse"))
   124  	require.NotNil(t, si)
   125  	actual, err := stackitem.Deserialize(si)
   126  	require.NoError(t, err)
   127  	require.Equal(t, stackitem.NewArray([]stackitem.Item{
   128  		stackitem.NewByteArray([]byte("url")),
   129  		stackitem.NewByteArray(userData),
   130  		stackitem.NewBigInteger(big.NewInt(int64(tx.Attributes[0].Value.(*transaction.OracleResponse).Code))),
   131  		stackitem.NewByteArray(tx.Attributes[0].Value.(*transaction.OracleResponse).Result),
   132  	}), actual)
   133  
   134  	// Check that the processed request is removed. We can't access GetRequestInternal directly,
   135  	// but adding a response to this request should fail due to invalid request error.
   136  	tx = prepareResponseTx(t, 0)
   137  	err = e.Chain.VerifyTx(tx)
   138  	require.Error(t, err)
   139  	require.True(t, strings.Contains(err.Error(), "oracle tx points to invalid request"))
   140  
   141  	t.Run("ErrorOnFinish", func(t *testing.T) {
   142  		putOracleRequest(t, helperValidatorInvoker, "url", nil, "handle", []byte{1, 2}, gasForResponse)
   143  		tx := prepareResponseTx(t, 1)
   144  		e.AddNewBlock(t, tx)
   145  		e.CheckFault(t, tx.Hash(), "ABORT")
   146  
   147  		// Check that the processed request is cleaned up even if callback failed. We can't
   148  		// access GetRequestInternal directly, but adding a response to this request
   149  		// should fail due to invalid request error.
   150  		tx = prepareResponseTx(t, 1)
   151  		err = e.Chain.VerifyTx(tx)
   152  		require.Error(t, err)
   153  		require.True(t, strings.Contains(err.Error(), "oracle tx points to invalid request"))
   154  	})
   155  	t.Run("Reentrant", func(t *testing.T) {
   156  		putOracleRequest(t, helperValidatorInvoker, "url", nil, "handleRecursive", []byte{}, gasForResponse)
   157  		tx := prepareResponseTx(t, 2)
   158  		e.AddNewBlock(t, tx)
   159  		e.CheckFault(t, tx.Hash(), "Oracle.finish called from non-entry script")
   160  		aer, err := e.Chain.GetAppExecResults(tx.Hash(), trigger.Application)
   161  		require.NoError(t, err)
   162  		require.Equal(t, 2, len(aer[0].Events)) // OracleResponse + Invocation
   163  	})
   164  	t.Run("BadRequest", func(t *testing.T) {
   165  		t.Run("non-UTF8 url", func(t *testing.T) {
   166  			putOracleRequest(t, helperValidatorInvoker, "\xff", nil, "", []byte{1, 2}, gasForResponse, "invalid value: not UTF-8")
   167  		})
   168  		t.Run("non-UTF8 filter", func(t *testing.T) {
   169  			var f = "\xff"
   170  			putOracleRequest(t, helperValidatorInvoker, "url", &f, "", []byte{1, 2}, gasForResponse, "invalid value: not UTF-8")
   171  		})
   172  		t.Run("not enough gas", func(t *testing.T) {
   173  			putOracleRequest(t, helperValidatorInvoker, "url", nil, "", nil, 1000, "not enough gas for response")
   174  		})
   175  		t.Run("disallowed callback", func(t *testing.T) {
   176  			putOracleRequest(t, helperValidatorInvoker, "url", nil, "_deploy", nil, 1000_0000, "disallowed callback method (starts with '_')")
   177  		})
   178  	})
   179  }