diff --git a/src/neo/Oracle/OracleExecutionCache.cs b/src/neo/Oracle/OracleExecutionCache.cs index 231d60d83e..4adf95eefb 100644 --- a/src/neo/Oracle/OracleExecutionCache.cs +++ b/src/neo/Oracle/OracleExecutionCache.cs @@ -1,4 +1,5 @@ using Neo.IO; +using Neo.SmartContract; using System; using System.Collections; using System.Collections.Generic; @@ -29,8 +30,42 @@ public class OracleExecutionCache : IEnumerable public long FilterCost { get; private set; } + /// + /// Responses + /// + public OracleResponse[] Responses => _cache.Values.ToArray(); + public int Size => IO.Helper.GetVarSize(Count) + _cache.Values.Sum(u => u.Size); + private UInt160 _hash; + + /// + /// Hash + /// + public UInt160 Hash + { + get + { + if (_hash != null) return _hash; + + using (var stream = new MemoryStream()) + { + foreach (var entry in _cache) + { + // Request Hash + stream.Write(entry.Key.ToArray()); + + // Response Hash + stream.Write(entry.Value.Hash.ToArray()); + } + + _hash = stream.ToArray().ToScriptHash(); + } + + return _hash; + } + } + /// /// Constructor for oracles /// @@ -45,6 +80,7 @@ public OracleExecutionCache(Func oracle = null) : /// public OracleExecutionCache() { + _hash = null; FilterCost = 0; } @@ -54,9 +90,11 @@ public OracleExecutionCache() /// Results public OracleExecutionCache(params OracleResponse[] results) { - _oracle = null; FilterCost = 0; + _hash = null; + _oracle = null; + foreach (var result in results) { _cache[result.RequestHash] = result; @@ -109,12 +147,13 @@ public void Serialize(BinaryWriter writer) public void Deserialize(BinaryReader reader) { - var results = reader.ReadSerializableArray(byte.MaxValue); - FilterCost = 0; + _hash = null; + + var entries = reader.ReadSerializableArray(byte.MaxValue); _cache.Clear(); - foreach (var result in results) + foreach (var result in entries) { _cache[result.RequestHash] = result; FilterCost += result.FilterCost; diff --git a/src/neo/Oracle/OracleWalletBehaviour.cs b/src/neo/Oracle/OracleWalletBehaviour.cs new file mode 100644 index 0000000000..08adac91f9 --- /dev/null +++ b/src/neo/Oracle/OracleWalletBehaviour.cs @@ -0,0 +1,20 @@ +namespace Neo.Oracle +{ + public enum OracleWalletBehaviour + { + /// + /// If an Oracle syscall was found, the tx will fault + /// + WithoutOracle, + + /// + /// If an Oracle syscall was found, the tx will be relayed without any check (The gas cost could be more if the result it's different) + /// + OracleWithoutAssert, + + /// + /// If an Oracle syscall was found, it will be added an asert at the begining of the script + /// + OracleWithAssert + } +} diff --git a/src/neo/SmartContract/InteropService.Oracle.cs b/src/neo/SmartContract/InteropService.Oracle.cs index 9e6fbf0b08..9eaee169d8 100644 --- a/src/neo/SmartContract/InteropService.Oracle.cs +++ b/src/neo/SmartContract/InteropService.Oracle.cs @@ -1,3 +1,4 @@ +using Neo.IO; using Neo.Network.P2P.Payloads; using Neo.Oracle; using Neo.Oracle.Protocols.Https; @@ -13,6 +14,24 @@ partial class InteropService public static class Oracle { public static readonly uint Neo_Oracle_Get = Register("Neo.Oracle.Get", Oracle_Get, 0, TriggerType.Application, CallFlags.None); + public static readonly uint Neo_Oracle_Hash = Register("Neo.Oracle.Hash", Oracle_Hash, 0, TriggerType.Application, CallFlags.None); + + /// + /// Oracle get the hash of the current OracleFlow [Request/Response] + /// + private static bool Oracle_Hash(ApplicationEngine engine) + { + if (engine.OracleCache == null) + { + engine.Push(StackItem.Null); + } + else + { + engine.Push(engine.OracleCache.Hash.ToArray()); + } + + return true; + } /// /// Oracle Get diff --git a/src/neo/SmartContract/Native/Oracle/OracleContract.cs b/src/neo/SmartContract/Native/Oracle/OracleContract.cs index 5db7971fb0..c33ed5d0e4 100644 --- a/src/neo/SmartContract/Native/Oracle/OracleContract.cs +++ b/src/neo/SmartContract/Native/Oracle/OracleContract.cs @@ -76,7 +76,9 @@ private StackItem SetOracleResponse(ApplicationEngine engine, Array args) public OracleExecutionCache ConsumeOracleResponse(StoreView snapshot, UInt256 txHash) { StorageKey key = CreateStorageKey(Prefix_OracleResponse, txHash.ToArray()); - StorageItem storage = snapshot.Storages.GetAndChange(key); + StorageItem storage = snapshot.Storages.TryGet(key); + if (storage == null) return null; + OracleExecutionCache ret = storage.Value.AsSerializable(); // It should be cached by the ApplicationEngine so we can save space removing it diff --git a/src/neo/Wallets/Wallet.cs b/src/neo/Wallets/Wallet.cs index 722c4dadea..7b79af6b3c 100644 --- a/src/neo/Wallets/Wallet.cs +++ b/src/neo/Wallets/Wallet.cs @@ -2,6 +2,8 @@ using Neo.IO; using Neo.Ledger; using Neo.Network.P2P.Payloads; +using Neo.Oracle; +using Neo.Oracle.Protocols.Https; using Neo.Persistence; using Neo.SmartContract; using Neo.SmartContract.Native; @@ -209,7 +211,7 @@ public virtual WalletAccount Import(string nep2, string passphrase, int N = 1638 return account; } - public Transaction MakeTransaction(TransferOutput[] outputs, UInt160 from = null) + public Transaction MakeTransaction(TransferOutput[] outputs, UInt160 from = null, OracleWalletBehaviour oracle = OracleWalletBehaviour.OracleWithAssert) { UInt160[] accounts; if (from is null) @@ -274,11 +276,11 @@ public Transaction MakeTransaction(TransferOutput[] outputs, UInt160 from = null Account = new UInt160(p.ToArray()) }).ToArray(); - return MakeTransaction(snapshot, script, new TransactionAttribute[0], cosigners, balances_gas); + return MakeTransaction(snapshot, script, new TransactionAttribute[0], cosigners, balances_gas, oracle); } } - public Transaction MakeTransaction(byte[] script, UInt160 sender = null, TransactionAttribute[] attributes = null, Cosigner[] cosigners = null) + public Transaction MakeTransaction(byte[] script, UInt160 sender = null, TransactionAttribute[] attributes = null, Cosigner[] cosigners = null, OracleWalletBehaviour oracle = OracleWalletBehaviour.OracleWithAssert) { UInt160[] accounts; if (sender is null) @@ -294,18 +296,36 @@ public Transaction MakeTransaction(byte[] script, UInt160 sender = null, Transac using (SnapshotView snapshot = Blockchain.Singleton.GetSnapshot()) { var balances_gas = accounts.Select(p => (Account: p, Value: NativeContract.GAS.BalanceOf(snapshot, p))).Where(p => p.Value.Sign > 0).ToList(); - return MakeTransaction(snapshot, script, attributes ?? new TransactionAttribute[0], cosigners ?? new Cosigner[0], balances_gas); + return MakeTransaction(snapshot, script, attributes ?? new TransactionAttribute[0], cosigners ?? new Cosigner[0], balances_gas, oracle); } } - private Transaction MakeTransaction(StoreView snapshot, byte[] script, TransactionAttribute[] attributes, Cosigner[] cosigners, List<(UInt160 Account, BigInteger Value)> balances_gas) + private Transaction MakeTransaction(StoreView snapshot, byte[] script, TransactionAttribute[] attributes, Cosigner[] cosigners, List<(UInt160 Account, BigInteger Value)> balances_gas, OracleWalletBehaviour oracle = OracleWalletBehaviour.OracleWithAssert) { + OracleExecutionCache oracleCache = null; + List oracleRequests = null; + + if (oracle != OracleWalletBehaviour.WithoutOracle) + { + // Wee need the full request in order to duplicate the call for asserts + + oracleRequests = new List(); + oracleCache = new OracleExecutionCache((request) => + { + oracleRequests.Add(request); + return OracleService.Process(request); + }); + } + + Start: + Random rand = new Random(); foreach (var (account, value) in balances_gas) { Transaction tx = new Transaction { Version = 0, + //TODO: x.Version = TransactionType.Oracle; <- if oracleQueries.Count != 0 Nonce = (uint)rand.Next(), Script = script, Sender = account, @@ -313,8 +333,9 @@ private Transaction MakeTransaction(StoreView snapshot, byte[] script, Transacti Attributes = attributes, Cosigners = cosigners }; + // will try to execute 'transfer' script to check if it works - using (ApplicationEngine engine = ApplicationEngine.Run(script, snapshot.Clone(), tx, testMode: true)) + using (ApplicationEngine engine = ApplicationEngine.Run(script, snapshot.Clone(), tx, testMode: true, oracle: oracleCache)) { if (engine.State.HasFlag(VMState.FAULT)) throw new InvalidOperationException($"Failed execution for '{script.ToHexString()}'"); @@ -328,6 +349,52 @@ private Transaction MakeTransaction(StoreView snapshot, byte[] script, Transacti else if (remainder < 0) tx.SystemFee -= remainder; } + + // Change the Transaction type because it's an oracle request + + if (oracleRequests.Count > 0 && oracle == OracleWalletBehaviour.OracleWithAssert) + { + // If we want the same result for accept the response, we need to create asserts at the begining of the script + + var assertScript = new ScriptBuilder(); + + foreach (var oracleRequest in oracleRequests) + { + // Do the request in order to cache the result + + if (oracleRequest is OracleHttpsRequest https) + { + assertScript.EmitSysCall(InteropService.Oracle.Neo_Oracle_Get, https.URL.ToString(), https.Filter?.ContractHash, https.Filter?.FilterMethod); + } + else + { + throw new NotImplementedException(); + } + } + + // Clear the stack + + assertScript.Emit(OpCode.CLEAR); + + // Check that the hash of the whole responses are exactly the same + + assertScript.EmitSysCall(InteropService.Oracle.Neo_Oracle_Hash); + assertScript.EmitPush(oracleCache.Hash.ToArray()); + assertScript.Emit(OpCode.EQUAL); + assertScript.Emit(OpCode.ASSERT); + + // Concat two scripts [OracleAsserts+Script] + + script = assertScript.ToArray().Concat(script).ToArray(); + oracle = OracleWalletBehaviour.OracleWithoutAssert; + + // We need to remove new oracle calls (OracleService.Process) + + oracleCache = new OracleExecutionCache(oracleCache.Responses); + + // We need to compute the gas again with the right script + goto Start; + } } UInt160[] hashes = tx.GetScriptHashesForVerifying(snapshot); diff --git a/tests/neo.UnitTests/Oracle/UT_OracleService.cs b/tests/neo.UnitTests/Oracle/UT_OracleService.cs index 83734e3385..0d563e474e 100644 --- a/tests/neo.UnitTests/Oracle/UT_OracleService.cs +++ b/tests/neo.UnitTests/Oracle/UT_OracleService.cs @@ -1,15 +1,19 @@ using Akka.TestKit; using Akka.TestKit.Xunit2; +using FluentAssertions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Ledger; using Neo.Network.P2P.Payloads; using Neo.Oracle; using Neo.Oracle.Protocols.Https; using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.SmartContract.Native.Tokens; using Neo.VM; using System; using System.IO; @@ -216,6 +220,77 @@ private Transaction CreateTx(string url, OracleFilter filter) }; } + [TestMethod] + public void TestOracleTx() + { + var port = 8443; + using var server = CreateServer(port); + + var wallet = TestUtils.GenerateTestWallet(); + var snapshot = Blockchain.Singleton.GetSnapshot(); + + // no password on this wallet + using (var unlock = wallet.Unlock("")) + { + var acc = wallet.CreateAccount(); + + // Fake balance + + var key = NativeContract.GAS.CreateStorageKey(20, acc.ScriptHash); + + var entry = snapshot.Storages.GetAndChange(key, () => new StorageItem + { + Value = new Nep5AccountState().ToByteArray() + }); + + entry.Value = new Nep5AccountState() + { + Balance = 10000 * NativeContract.GAS.Factor + } + .ToByteArray(); + + snapshot.Commit(); + + // Manually creating script + + byte[] script; + using (ScriptBuilder sb = new ScriptBuilder()) + { + // self-transfer of 1e-8 GAS + System.Numerics.BigInteger value = (new BigDecimal(1, 8)).Value; + sb.EmitSysCall(InteropService.Oracle.Neo_Oracle_Get, $"https://127.0.0.1:{port}/ping", null, null); + sb.Emit(OpCode.UNPACK); + script = sb.ToArray(); + } + + // WithoutOracle + + Assert.ThrowsException(() => + { + _ = wallet.MakeTransaction(script, acc.ScriptHash, new TransactionAttribute[0], new Cosigner[0], oracle: OracleWalletBehaviour.WithoutOracle); + }); + + // OracleWithoutAssert + + var txWithout = wallet.MakeTransaction(script, acc.ScriptHash, new TransactionAttribute[0], new Cosigner[0], oracle: OracleWalletBehaviour.OracleWithoutAssert); + + Assert.IsNotNull(txWithout); + Assert.IsNull(txWithout.Witnesses); + + // OracleWithoutAssert + + var txWith = wallet.MakeTransaction(script, acc.ScriptHash, new TransactionAttribute[0], new Cosigner[0], oracle: OracleWalletBehaviour.OracleWithAssert); + + Assert.IsNotNull(txWith); + Assert.IsNull(txWith.Witnesses); + + // Check that has more fee and the script is longer + + Assert.IsTrue(txWith.Script.Length > txWithout.Script.Length); + Assert.IsTrue(txWith.NetworkFee > txWithout.NetworkFee); + } + } + [TestMethod] public void TestOracleHttpsRequest() {