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 }