diff --git a/src/neo/Network/P2P/Payloads/Transaction.cs b/src/neo/Network/P2P/Payloads/Transaction.cs index d2d3e156d2a..b905f391536 100644 --- a/src/neo/Network/P2P/Payloads/Transaction.cs +++ b/src/neo/Network/P2P/Payloads/Transaction.cs @@ -40,6 +40,7 @@ public class Transaction : IEquatable, IInventory, IInteroperable private Cosigner[] cosigners; private byte[] script; private Witness[] witnesses; + private UInt256 oracleRequestTx; public const int HeaderSize = sizeof(TransactionVersion) + //Version @@ -61,6 +62,12 @@ public Cosigner[] Cosigners set { cosigners = value; _hash = null; _size = 0; } } + public UInt256 OracleRequestTx + { + get => oracleRequestTx; + set { oracleRequestTx = value; _hash = null; _size = 0; } + } + /// /// The NetworkFee for the transaction divided by its Size. /// Note that this property must be used with care. Getting the value of this property multiple times will return the same result. The value of this property can only be obtained after the transaction has been completely built (no longer modified). @@ -121,6 +128,11 @@ public int Size Cosigners.GetVarSize() + //Cosigners Script.GetVarSize() + //Script Witnesses.GetVarSize(); //Witnesses + + if (Version == TransactionVersion.OracleResponse) + { + _size += UInt256.Length; + } } return _size; } @@ -181,6 +193,7 @@ public void DeserializeUnsigned(BinaryReader reader) if (Cosigners.Select(u => u.Account).Distinct().Count() != Cosigners.Length) throw new FormatException(); Script = reader.ReadVarBytes(ushort.MaxValue); if (Script.Length == 0) throw new FormatException(); + OracleRequestTx = Version == TransactionVersion.OracleResponse ? reader.ReadSerializable() : null; } public bool Equals(Transaction other) @@ -224,6 +237,10 @@ void IVerifiable.SerializeUnsigned(BinaryWriter writer) writer.Write(Attributes); writer.Write(Cosigners); writer.WriteVarBytes(Script); + if (Version == TransactionVersion.OracleResponse) + { + writer.Write(OracleRequestTx); + } } public JObject ToJson() @@ -241,6 +258,10 @@ public JObject ToJson() json["cosigners"] = Cosigners.Select(p => p.ToJson()).ToArray(); json["script"] = Convert.ToBase64String(Script); json["witnesses"] = Witnesses.Select(p => p.ToJson()).ToArray(); + if (Version == TransactionVersion.OracleResponse) + { + json["oracle_response_tx"] = OracleRequestTx.ToString(); + } return json; } @@ -258,6 +279,14 @@ public static Transaction FromJson(JObject json) tx.Cosigners = ((JArray)json["cosigners"]).Select(p => Cosigner.FromJson(p)).ToArray(); tx.Script = Convert.FromBase64String(json["script"].AsString()); tx.Witnesses = ((JArray)json["witnesses"]).Select(p => Witness.FromJson(p)).ToArray(); + if (tx.Version == TransactionVersion.OracleResponse) + { + tx.OracleRequestTx = UInt256.Parse(json["oracle_response_tx"].AsString()); + } + else + { + tx.OracleRequestTx = null; + } return tx; } diff --git a/src/neo/Oracle/OracleResponse.cs~refs/tags/v3.0.0-preview2 b/src/neo/Oracle/OracleResponse.cs~refs/tags/v3.0.0-preview2 index 3ec12816a65..10b3ebbfe1b 100644 --- a/src/neo/Oracle/OracleResponse.cs~refs/tags/v3.0.0-preview2 +++ b/src/neo/Oracle/OracleResponse.cs~refs/tags/v3.0.0-preview2 @@ -3,16 +3,13 @@ using Neo.IO; using Neo.Network.P2P; using Neo.Network.P2P.Payloads; using Neo.Persistence; -using Neo.SmartContract; -using Neo.VM; -using Neo.VM.Types; using System; using System.IO; using System.Text; namespace Neo.Oracle { - public class OracleResponse : IInteroperable, IVerifiable + public class OracleResponse : IVerifiable { private UInt160 _hash; @@ -22,14 +19,14 @@ namespace Neo.Oracle public UInt160 RequestHash { get; set; } /// - /// Error + /// Result /// - public OracleResultError Error { get; set; } + public byte[] Result { get; set; } /// - /// Result + /// Error /// - public byte[] Result { get; set; } + public bool Error => Result == null; /// /// Filter cost paid by Oracle and must be claimed to the user @@ -69,13 +66,9 @@ namespace Neo.Oracle /// OracleResult public static OracleResponse CreateError(UInt160 requestHash, OracleResultError error, long filterCost = 0) { - return new OracleResponse() - { - RequestHash = requestHash, - Error = error, - Result = new byte[0], - FilterCost = filterCost - }; + // TODO: We should log the error if we want, but in order to reduce the indeterminism, we will only say that the download was unsuccessful + + return CreateResult(requestHash, (byte[])null, filterCost); } /// @@ -102,7 +95,6 @@ namespace Neo.Oracle return new OracleResponse() { RequestHash = requestHash, - Error = OracleResultError.None, Result = result, FilterCost = filterCost }; @@ -111,10 +103,19 @@ namespace Neo.Oracle public void SerializeUnsigned(BinaryWriter writer) { writer.Write(RequestHash); - writer.Write((byte)Error); - if (Error == OracleResultError.None) - writer.WriteVarBytes(Result); writer.Write(FilterCost); + + if (Result != null) + { + writer.Write((byte)0x01); + writer.WriteVarBytes(Result); + } + else + { + // Error result + + writer.Write((byte)0x00); + } } public void Serialize(BinaryWriter writer) @@ -125,10 +126,19 @@ namespace Neo.Oracle public void DeserializeUnsigned(BinaryReader reader) { RequestHash = reader.ReadSerializable(); - Error = (OracleResultError)reader.ReadByte(); - Result = Error == OracleResultError.None ? reader.ReadVarBytes(ushort.MaxValue) : new byte[0]; FilterCost = reader.ReadInt64(); if (FilterCost < 0) throw new FormatException(nameof(FilterCost)); + + if (reader.ReadByte() == 0x01) + { + Result = reader.ReadVarBytes(ushort.MaxValue); + } + else + { + // Error result + + Result = null; + } } public void Deserialize(BinaryReader reader) @@ -140,19 +150,5 @@ namespace Neo.Oracle { return new UInt160[] { new UInt160(Crypto.Hash160(this.GetHashData())) }; } - - /// - /// Get Stack item for IInteroperable - /// - /// StackItem - public StackItem ToStackItem(ReferenceCounter referenceCounter) - { - return new VM.Types.Array(referenceCounter, new StackItem[] - { - new ByteString(RequestHash.ToArray()), - new Integer((byte)Error), - new ByteString(Result) - }); - } } } diff --git a/src/neo/Oracle/OracleService.cs b/src/neo/Oracle/OracleService.cs index 3ecba65ec0e..6fba26d3dd7 100644 --- a/src/neo/Oracle/OracleService.cs +++ b/src/neo/Oracle/OracleService.cs @@ -3,6 +3,7 @@ using Neo.Ledger; using Neo.Network.P2P.Payloads; using Neo.Oracle.Protocols.Https; +using Neo.Persistence; using Neo.SmartContract; using Neo.SmartContract.Native; using Neo.VM; @@ -215,7 +216,7 @@ private void ProcessTransaction(Transaction tx) { // Send oracle result - var responseTx = CreateResponseTransaction(oracle, tx); + var responseTx = CreateResponseTransaction(snapshot, oracle, tx); Sign(responseTx, out var signature); var response = new OracleServiceResponse() @@ -238,27 +239,35 @@ private void ProcessTransaction(Transaction tx) /// Create Oracle response transaction /// We need to create a deterministic TX for this result/oracleRequest /// + /// Snapshot /// Oracle /// Request Hash /// Transaction - public static Transaction CreateResponseTransaction(OracleExecutionCache oracle, Transaction requestTx) + public static Transaction CreateResponseTransaction(StoreView snapshot, OracleExecutionCache oracle, Transaction requestTx) { + var sender = NativeContract.Oracle.GetOracleMultiSigAddress(snapshot); using ScriptBuilder script = new ScriptBuilder(); script.EmitAppCall(NativeContract.Oracle.Hash, "setOracleResponse", requestTx.Hash, oracle.ToArray()); return new Transaction() { - Attributes = new TransactionAttribute[] + Version = TransactionVersion.OracleResponse, + Sender = sender, + Nonce = requestTx.Nonce, + ValidUntilBlock = requestTx.ValidUntilBlock, + OracleRequestTx = requestTx.Hash, + Cosigners = new Cosigner[] { - // DependsOn = hash + new Cosigner() + { + Account = sender, + AllowedContracts = new UInt160[]{ NativeContract.Oracle.Hash }, + Scopes = WitnessScope.CustomContracts + } }, - Cosigners = new Cosigner[0], NetworkFee = 1_000_000, // TODO: Define fee - SystemFee = 1_000_000, - Sender = UInt160.Zero, // <- OracleSender - Nonce = requestTx.Nonce, - ValidUntilBlock = requestTx.ValidUntilBlock, + SystemFee = 1_000_000, // TODO: Define fee Witnesses = new Witness[0], Script = script.ToArray() }; diff --git a/src/neo/SmartContract/InteropService.Oracle.cs b/src/neo/SmartContract/InteropService.Oracle.cs index 9eaee169d81..9f6b1cfc6eb 100644 --- a/src/neo/SmartContract/InteropService.Oracle.cs +++ b/src/neo/SmartContract/InteropService.Oracle.cs @@ -108,7 +108,7 @@ private static bool Oracle_Get(ApplicationEngine engine) if (engine.OracleCache.TryGet(request, out var response)) { - engine.Push(response.ToStackItem(engine.ReferenceCounter)); + engine.Push(response.Result ?? StackItem.Null); return true; } diff --git a/src/neo/Wallets/Wallet.cs b/src/neo/Wallets/Wallet.cs index ec1c635d3dc..f5fc46e4deb 100644 --- a/src/neo/Wallets/Wallet.cs +++ b/src/neo/Wallets/Wallet.cs @@ -325,8 +325,7 @@ private Transaction MakeTransaction(StoreView snapshot, byte[] script, Transacti { Transaction tx = new Transaction { - Version = 0, - //TODO: x.Version = TransactionType.Oracle; <- if oracleQueries.Count != 0 + Version = oracleCache?.Count > 0 ? TransactionVersion.OracleRequest : TransactionVersion.Transaction, Nonce = (uint)rand.Next(), Script = script, Sender = account, @@ -344,48 +343,55 @@ private Transaction MakeTransaction(StoreView snapshot, byte[] script, Transacti // Change the Transaction type because it's an oracle request - if (oracleRequests.Count > 0 && oracle == OracleWalletBehaviour.OracleWithAssert) + if (oracleRequests.Count > 0) { - // 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) + if (oracle == OracleWalletBehaviour.OracleWithAssert) { - // Do the request in order to cache the result + // If we want the same result for accept the response, we need to create asserts at the begining of the script - if (oracleRequest is OracleHttpsRequest https) - { - assertScript.EmitSysCall(InteropService.Oracle.Neo_Oracle_Get, https.URL.ToString(), https.Filter?.ContractHash, https.Filter?.FilterMethod); - } - else + var assertScript = new ScriptBuilder(); + + foreach (var oracleRequest in oracleRequests) { - throw new NotImplementedException(); + // 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 + // Clear the stack - assertScript.Emit(OpCode.CLEAR); + assertScript.Emit(OpCode.CLEAR); - // Check that the hash of the whole responses are exactly the same + // 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); + 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] + // Concat two scripts [OracleAsserts+Script] - script = assertScript.ToArray().Concat(script).ToArray(); - oracle = OracleWalletBehaviour.OracleWithoutAssert; + script = assertScript.ToArray().Concat(script).ToArray(); + oracle = OracleWalletBehaviour.OracleWithoutAssert; - // We need to remove new oracle calls (OracleService.Process) + // We need to remove new oracle calls (OracleService.Process) - oracleCache = new OracleExecutionCache(oracleCache.Responses); + oracleCache = new OracleExecutionCache(oracleCache.Responses); - // We need to compute the gas again with the right script - goto Start; + // We need to compute the gas again with the right script + goto Start; + } + else + { + tx.Version = TransactionVersion.OracleRequest; + } } } diff --git a/tests/neo.UnitTests/Oracle/UT_OracleExecutionCache.cs b/tests/neo.UnitTests/Oracle/UT_OracleExecutionCache.cs index afd3682a83f..edacb551cfc 100644 --- a/tests/neo.UnitTests/Oracle/UT_OracleExecutionCache.cs +++ b/tests/neo.UnitTests/Oracle/UT_OracleExecutionCache.cs @@ -78,7 +78,7 @@ public void TestWithOracle() Assert.AreEqual(2, req.Counter); Assert.AreEqual(1, cache.Count); - Assert.AreEqual(OracleResultError.None, ret.Error); + Assert.IsFalse(ret.Error); CollectionAssert.AreEqual(new byte[] { 0x02, 0x00, 0x00, 0x00 }, ret.Result); // Test cached @@ -87,7 +87,7 @@ public void TestWithOracle() Assert.AreEqual(2, req.Counter); Assert.AreEqual(1, cache.Count); - Assert.AreEqual(OracleResultError.None, ret.Error); + Assert.IsFalse(ret.Error); CollectionAssert.AreEqual(new byte[] { 0x02, 0x00, 0x00, 0x00 }, ret.Result); // Check collection @@ -95,7 +95,7 @@ public void TestWithOracle() var array = cache.ToArray(); Assert.AreEqual(1, array.Length); Assert.AreEqual(req.Hash, array[0].Key); - Assert.AreEqual(OracleResultError.None, array[0].Value.Error); + Assert.IsFalse(array[0].Value.Error); CollectionAssert.AreEqual(new byte[] { 0x02, 0x00, 0x00, 0x00 }, array[0].Value.Result); } @@ -118,8 +118,8 @@ public void TestWithoutOracle() var array = cache.ToArray(); Assert.AreEqual(1, array.Length); Assert.AreEqual(initReq.Hash, array[0].Key); - Assert.AreEqual(OracleResultError.ServerError, array[0].Value.Error); - CollectionAssert.AreEqual(Array.Empty(), array[0].Value.Result); + Assert.IsTrue(array[0].Value.Error); + Assert.AreEqual(null, array[0].Value.Result); // Test without cache diff --git a/tests/neo.UnitTests/Oracle/UT_OracleResponse.cs~refs/tags/v3.0.0-preview2 b/tests/neo.UnitTests/Oracle/UT_OracleResponse.cs~refs/tags/v3.0.0-preview2 index b9b5cd72874..c2bc07203e9 100644 --- a/tests/neo.UnitTests/Oracle/UT_OracleResponse.cs~refs/tags/v3.0.0-preview2 +++ b/tests/neo.UnitTests/Oracle/UT_OracleResponse.cs~refs/tags/v3.0.0-preview2 @@ -21,7 +21,7 @@ namespace Neo.UnitTests.Oracle Assert.AreNotEqual(requestA.Hash, requestB.Hash); requestB = CreateDefault(); - requestB.Error = OracleResultError.FilterError; + requestB.Result = null; Assert.AreNotEqual(requestA.Hash, requestB.Hash); } @@ -45,7 +45,6 @@ namespace Neo.UnitTests.Oracle { return new OracleResponse() { - Error = OracleResultError.None, RequestHash = UInt160.Parse("0xff00ff00ff00ff00ff00ff00ff00ff00ff00ff01"), Result = new byte[] { 0x01, 0x02, 0x03 } }; diff --git a/tests/neo.UnitTests/Oracle/UT_OracleService.cs b/tests/neo.UnitTests/Oracle/UT_OracleService.cs index 0d563e474e1..d35788c02e4 100644 --- a/tests/neo.UnitTests/Oracle/UT_OracleService.cs +++ b/tests/neo.UnitTests/Oracle/UT_OracleService.cs @@ -1,6 +1,5 @@ using Akka.TestKit; using Akka.TestKit.Xunit2; -using FluentAssertions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -195,7 +194,7 @@ public void ProcessTx() Assert.AreEqual(1, response.ExecutionResult.Count); var entry = response.ExecutionResult.First(); - Assert.AreEqual(OracleResultError.None, entry.Value.Error); + Assert.IsFalse(entry.Value.Error); Assert.AreEqual("pong", Encoding.UTF8.GetString(entry.Value.Result)); Assert.AreEqual(tx.Hash, response.UserTxHash); @@ -256,10 +255,7 @@ public void TestOracleTx() 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(); } @@ -276,6 +272,7 @@ public void TestOracleTx() Assert.IsNotNull(txWithout); Assert.IsNull(txWithout.Witnesses); + Assert.AreEqual(TransactionVersion.OracleRequest, txWithout.Version); // OracleWithoutAssert @@ -283,6 +280,7 @@ public void TestOracleTx() Assert.IsNotNull(txWith); Assert.IsNull(txWith.Witnesses); + Assert.AreEqual(TransactionVersion.OracleRequest, txWith.Version); // Check that has more fee and the script is longer @@ -315,8 +313,8 @@ public void TestOracleHttpsRequest() OracleService.HTTPSProtocol.TimeOut = TimeSpan.FromSeconds(5); - Assert.AreEqual(OracleResultError.Timeout, response.Error); - Assert.IsTrue(response.Result.Length == 0); + Assert.IsTrue(response.Error); + Assert.IsTrue(response.Result == null); Assert.AreEqual(request.Hash, response.RequestHash); Assert.AreNotEqual(UInt160.Zero, response.Hash); @@ -330,7 +328,7 @@ public void TestOracleHttpsRequest() response = OracleService.Process(request); - Assert.AreEqual(OracleResultError.None, response.Error); + Assert.IsFalse(response.Error); Assert.AreEqual("pong", Encoding.UTF8.GetString(response.Result)); Assert.AreEqual(request.Hash, response.RequestHash); Assert.AreNotEqual(UInt160.Zero, response.Hash); @@ -345,8 +343,8 @@ public void TestOracleHttpsRequest() response = OracleService.Process(request); - Assert.AreEqual(OracleResultError.ResponseError, response.Error); - Assert.IsTrue(response.Result.Length == 0); + Assert.IsTrue(response.Error); + Assert.IsTrue(response.Result == null); Assert.AreEqual(request.Hash, response.RequestHash); Assert.AreNotEqual(UInt160.Zero, response.Hash); @@ -355,8 +353,8 @@ public void TestOracleHttpsRequest() OracleService.HTTPSProtocol.AllowPrivateHost = false; response = OracleService.Process(request); - Assert.AreEqual(OracleResultError.PolicyError, response.Error); - Assert.IsTrue(response.Result.Length == 0); + Assert.IsTrue(response.Error); + Assert.IsTrue(response.Result == null); Assert.AreEqual(request.Hash, response.RequestHash); Assert.AreNotEqual(UInt160.Zero, response.Hash); } @@ -373,10 +371,10 @@ public void TestOracleErrorRequest() var request = new ErrorRequest(); var response = OracleService.Process(request); - Assert.AreEqual(OracleResultError.ProtocolError, response.Error); - CollectionAssert.AreEqual(new byte[0], response.Result); + Assert.IsTrue(response.Error); + Assert.AreEqual(null, response.Result); Assert.AreEqual(request.Hash, response.RequestHash); - Assert.AreEqual("0x6d53b08489400de6e2d3bf0edfd1385e14dbde68", response.Hash.ToString()); + Assert.AreEqual("0xe62b56e4b43b01411403058ba53fc5e6dbdf8fba", response.Hash.ToString()); } } } diff --git a/tests/neo.UnitTests/SmartContract/UT_Syscalls.cs b/tests/neo.UnitTests/SmartContract/UT_Syscalls.cs index b0829c07575..a5e9f3b3212 100644 --- a/tests/neo.UnitTests/SmartContract/UT_Syscalls.cs +++ b/tests/neo.UnitTests/SmartContract/UT_Syscalls.cs @@ -129,12 +129,7 @@ public void Neo_Oracle_Get() Assert.AreEqual(engine.Execute(), VMState.HALT); Assert.AreEqual(1, engine.ResultStack.Count); - Assert.IsTrue(engine.ResultStack.TryPop(out var array)); - - var type = array[1] as PrimitiveType; - var response = array[2] as ByteString; - - Assert.AreEqual((int)OracleResultError.None, type.GetBigInteger()); + Assert.IsTrue(engine.ResultStack.TryPop(out var response)); Assert.AreEqual("MyResponse", response.GetString()); } } @@ -155,13 +150,7 @@ public void Neo_Oracle_Get() Assert.AreEqual(engine.Execute(), VMState.HALT); Assert.AreEqual(1, engine.ResultStack.Count); - Assert.IsTrue(engine.ResultStack.TryPop(out var array)); - - var type = array[1] as PrimitiveType; - var response = array[2] as ByteString; - - Assert.AreEqual((int)OracleResultError.FilterError, type.GetBigInteger()); - Assert.AreEqual("", response.GetString()); + Assert.IsTrue(engine.ResultStack.TryPop(out var isNull)); } }